feat(tools): add SOAP tools (sage_soap_read, sage_soap_query, sage_describe_entity)
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-Claude) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
102
src/tools/sage-describe-entity.ts
Normal file
102
src/tools/sage-describe-entity.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
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<string, unknown>;
|
||||
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<string, unknown>;
|
||||
|
||||
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<string, unknown>;
|
||||
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<CallToolResult> => {
|
||||
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));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user