import { z } from 'zod'; import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import type { SoapClient } from '../clients/soap-client.js'; import { formatToolError, classifyError, getErrorHint } from '../utils/errors.js'; interface FieldDefinition { name: string; type: string; length: string; label: string; } // @_ prefix: fast-xml-parser attributeNamePrefix config in soap-client.ts function parseFieldDefinitions(data: unknown): FieldDefinition[] { if (!data || typeof data !== 'object') return []; const fields: FieldDefinition[] = []; const fldArray = extractFldArray(data); for (const fld of fldArray) { if (!fld || typeof fld !== 'object') continue; const f = fld as Record; fields.push({ name: String(f['@_NAM'] ?? f['NAM'] ?? ''), type: String(f['@_TYP'] ?? f['TYP'] ?? ''), length: String(f['@_LEN'] ?? f['LEN'] ?? ''), label: String(f['@_C_ENG'] ?? f['C_ENG'] ?? ''), }); } return fields.filter((f) => f.name !== ''); } function extractFldArray(data: unknown): unknown[] { if (!data || typeof data !== 'object') return []; const obj = data as Record; if (Array.isArray(obj['FLD'])) return obj['FLD']; if (obj['FLD'] && typeof obj['FLD'] === 'object') return [obj['FLD']]; for (const key of Object.keys(obj)) { const child = obj[key]; if (child && typeof child === 'object' && !Array.isArray(child)) { const childObj = child as Record; if (Array.isArray(childObj['FLD'])) return childObj['FLD']; if (childObj['FLD'] && typeof childObj['FLD'] === 'object') return [childObj['FLD']]; } } return []; } export function registerDescribeEntityTool( server: McpServer, soapClient: SoapClient, ): void { server.registerTool( 'sage_describe_entity', { description: 'Get field definitions for a Sage X3 SOAP object. Returns field names, types, lengths, and English labels. Essential for understanding X3 field codes.', inputSchema: { publicName: z .string() .describe('SOAP publication name, e.g. SIH, SOH, WSBPC, WITM'), }, annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, }, }, async ({ publicName }): Promise => { try { const result = await soapClient.getDescription(publicName); if (result.status === 1) { const fields = parseFieldDefinitions(result.data); return { content: [ { type: 'text' as const, text: JSON.stringify({ publicName, fields, fieldCount: fields.length }), }, ], }; } const messages = result.messages.map((m) => m.message).join('; '); return formatToolError( new Error(messages || 'Failed to get entity description'), 'Check the publicName. Common SOAP publications: SIH (invoices), SOH (sales orders), WSBPC (customers), WITM (items).', ); } catch (error) { const classification = classifyError(error); return formatToolError(error, getErrorHint(classification)); } }, ); }