diff --git a/mcp-gateway/erpnext-mcp/src/tools/stock.ts b/mcp-gateway/erpnext-mcp/src/tools/stock.ts new file mode 100644 index 0000000..74f427e --- /dev/null +++ b/mcp-gateway/erpnext-mcp/src/tools/stock.ts @@ -0,0 +1,364 @@ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { ERPNextClient, truncateResponse } from "../services/erpnext-client.js"; +import { z } from "zod"; + +export function registerStockTools(server: McpServer, client: ERPNextClient): void { + + // ─── 37. Get Stock Balance ───────────────────────────── + server.registerTool( + "erpnext_get_stock_balance", + { + title: "Get Stock Balance", + description: `Get current stock balance for an item in a warehouse. + +Args: + - item_code: Item code + - warehouse: Warehouse name + - posting_date: Optional as-of date + - posting_time: Optional as-of time`, + inputSchema: z.object({ + item_code: z.string(), + warehouse: z.string(), + posting_date: z.string().optional(), + posting_time: z.string().optional(), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("erpnext.stock.utils.get_stock_balance", params); + return { content: [{ type: "text", text: `Stock balance for ${params.item_code} in ${params.warehouse}: ${result}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 38. Get Latest Stock Qty ────────────────────────── + server.registerTool( + "erpnext_get_latest_stock_qty", + { + title: "Get Latest Stock Quantity", + description: `Get the latest available stock quantity for an item across all or a specific warehouse.`, + inputSchema: z.object({ + item_code: z.string(), + warehouse: z.string().optional(), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("erpnext.stock.utils.get_latest_stock_qty", params); + return { content: [{ type: "text", text: `Latest stock qty for ${params.item_code}: ${result}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 39. Make Stock Entry ────────────────────────────── + server.registerTool( + "erpnext_make_stock_entry", + { + title: "Make Stock Entry", + description: `Create a stock entry for material transfer, receipt, issue, etc. + +Args: + - purpose: "Material Transfer", "Material Receipt", "Material Issue", "Manufacture", "Repack", "Send to Subcontractor" + - company: Company name + - items: Array of {item_code, qty, s_warehouse? (source), t_warehouse? (target), basic_rate?} + - posting_date: Optional date`, + inputSchema: z.object({ + purpose: z.enum(["Material Transfer", "Material Receipt", "Material Issue", "Manufacture", "Repack", "Send to Subcontractor"]), + company: z.string(), + items: z.array(z.object({ + item_code: z.string(), + qty: z.number(), + s_warehouse: z.string().optional(), + t_warehouse: z.string().optional(), + basic_rate: z.number().optional(), + })), + posting_date: z.string().optional(), + from_warehouse: z.string().optional(), + to_warehouse: z.string().optional(), + }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const doc = await client.createDocument("Stock Entry", { + stock_entry_type: params.purpose, + company: params.company, + posting_date: params.posting_date, + from_warehouse: params.from_warehouse, + to_warehouse: params.to_warehouse, + items: params.items, + }); + return { content: [{ type: "text", text: `Created Stock Entry: ${doc.name} (${params.purpose})` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 40. Scan Barcode ────────────────────────────────── + server.registerTool( + "erpnext_scan_barcode", + { + title: "Scan Barcode", + description: `Look up an item by barcode, serial number, or batch number.`, + inputSchema: z.object({ search_value: z.string().describe("Barcode, serial no, or batch no") }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("erpnext.stock.utils.scan_barcode", { + search_value: params.search_value, + }); + return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 41. Get Projected Qty ───────────────────────────── + server.registerTool( + "erpnext_get_projected_qty", + { + title: "Get Projected Quantity", + description: `Get the projected (planned) quantity for an item in a warehouse, accounting for pending orders.`, + inputSchema: z.object({ + item_code: z.string(), + warehouse: z.string(), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("erpnext.stock.get_item_details.get_projected_qty", params); + return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 42. Get Bin Details ─────────────────────────────── + server.registerTool( + "erpnext_get_bin_details", + { + title: "Get Bin Details", + description: `Get detailed stock bin information (actual qty, planned qty, ordered qty, reserved qty, etc.) for an item in a warehouse.`, + inputSchema: z.object({ + item_code: z.string(), + warehouse: z.string(), + company: z.string().optional(), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("erpnext.stock.get_item_details.get_bin_details", params); + return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 43. Item Dashboard Data ─────────────────────────── + server.registerTool( + "erpnext_get_item_dashboard_data", + { + title: "Get Item Dashboard Data", + description: `Get warehouse-wise stock data for items, showing actual qty, planned qty, and reserved qty.`, + inputSchema: z.object({ + item_code: z.string().optional(), + warehouse: z.string().optional(), + item_group: z.string().optional(), + start: z.number().default(0), + sort_by: z.string().default("actual_qty"), + sort_order: z.enum(["asc", "desc"]).default("desc"), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("erpnext.stock.dashboard.item_dashboard.get_data", params); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 44. Get Batch Qty ───────────────────────────────── + server.registerTool( + "erpnext_get_batch_qty", + { + title: "Get Batch Quantity", + description: `Get available quantity for a specific batch in a warehouse.`, + inputSchema: z.object({ + batch_no: z.string(), + warehouse: z.string(), + item_code: z.string().optional(), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("erpnext.stock.doctype.batch.batch.get_batch_qty", params); + return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 45. Make Sales Invoice from DN ──────────────────── + server.registerTool( + "erpnext_make_sales_invoice_from_dn", + { + title: "Make Sales Invoice from Delivery Note", + description: `Create a Sales Invoice from a Delivery Note.`, + inputSchema: z.object({ source_name: z.string() }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice", + { source_name: params.source_name } + ); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 46. Make Purchase Invoice from PR ───────────────── + server.registerTool( + "erpnext_make_purchase_invoice_from_pr", + { + title: "Make Purchase Invoice from Purchase Receipt", + description: `Create a Purchase Invoice from a Purchase Receipt.`, + inputSchema: z.object({ source_name: z.string() }).strict(), + annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_invoice", + { source_name: params.source_name } + ); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 47. Warehouse Capacity Dashboard ────────────────── + server.registerTool( + "erpnext_warehouse_capacity", + { + title: "Get Warehouse Capacity Dashboard", + description: `Get warehouse capacity utilization data.`, + inputSchema: z.object({ + warehouse: z.string().optional(), + item_code: z.string().optional(), + parent_warehouse: z.string().optional(), + start: z.number().default(0), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.stock.dashboard.warehouse_capacity_dashboard.get_data", + params + ); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 48. Get Stock Reconciliation Items ──────────────── + server.registerTool( + "erpnext_get_stock_reconciliation_items", + { + title: "Get Items for Stock Reconciliation", + description: `Get items and their current stock for stock reconciliation.`, + inputSchema: z.object({ + warehouse: z.string(), + posting_date: z.string(), + posting_time: z.string().optional(), + company: z.string(), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items", + params + ); + return { content: [{ type: "text", text: truncateResponse(JSON.stringify(result, null, 2)) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 49. Get Valuation Rate ──────────────────────────── + server.registerTool( + "erpnext_get_valuation_rate", + { + title: "Get Item Valuation Rate", + description: `Get the valuation rate for an item.`, + inputSchema: z.object({ + item_code: z.string(), + company: z.string(), + warehouse: z.string().optional(), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod( + "erpnext.stock.get_item_details.get_valuation_rate", + params + ); + return { content: [{ type: "text", text: `Valuation rate for ${params.item_code}: ${result}` }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); + + // ─── 50. Get Conversion Factor ───────────────────────── + server.registerTool( + "erpnext_get_conversion_factor", + { + title: "Get UOM Conversion Factor", + description: `Get the conversion factor between a UOM and the stock UOM for an item.`, + inputSchema: z.object({ + item_code: z.string(), + uom: z.string(), + }).strict(), + annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, + }, + async (params) => { + try { + const result = await client.callMethod("erpnext.stock.get_item_details.get_conversion_factor", params); + return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; + } catch (error) { + return { isError: true, content: [{ type: "text", text: `Error: ${(error as Error).message}` }] }; + } + } + ); +}