145 lines
4.2 KiB
TypeScript
145 lines
4.2 KiB
TypeScript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
import * as z from "zod/v4";
|
|
import { executeGraphQL } from "../graphql/index.js";
|
|
import { getConfig } from "../config.js";
|
|
|
|
/**
|
|
* Build a GraphQL field selection string from an array of field paths.
|
|
* Supports dot notation for nested fields:
|
|
* ["code", "header.name", "header.type"] →
|
|
* "code header { name type }"
|
|
*/
|
|
function buildFieldSelection(fields: string[]): string {
|
|
interface FieldNode {
|
|
[key: string]: FieldNode;
|
|
}
|
|
|
|
const tree: FieldNode = {};
|
|
|
|
for (const field of fields) {
|
|
const parts = field.split(".");
|
|
let current = tree;
|
|
for (const part of parts) {
|
|
current[part] ??= {};
|
|
current = current[part];
|
|
}
|
|
}
|
|
|
|
function render(node: FieldNode): string {
|
|
return Object.entries(node)
|
|
.map(([key, children]) => {
|
|
const childKeys = Object.keys(children);
|
|
return childKeys.length > 0
|
|
? `${key} { ${render(children)} }`
|
|
: key;
|
|
})
|
|
.join(" ");
|
|
}
|
|
|
|
return render(tree);
|
|
}
|
|
|
|
function formatEntity(
|
|
obj: Record<string, unknown>,
|
|
indent: number = 0,
|
|
): string {
|
|
const prefix = " ".repeat(indent);
|
|
const lines: string[] = [];
|
|
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
lines.push(`${prefix}${key}:`);
|
|
lines.push(
|
|
formatEntity(value as Record<string, unknown>, indent + 1),
|
|
);
|
|
} else if (Array.isArray(value)) {
|
|
lines.push(`${prefix}${key}: [${value.map(String).join(", ")}]`);
|
|
} else {
|
|
lines.push(`${prefix}${key}: ${String(value ?? "null")}`);
|
|
}
|
|
}
|
|
|
|
return lines.join("\n");
|
|
}
|
|
|
|
export function registerReadEntityTool(server: McpServer): void {
|
|
server.registerTool(
|
|
"read_entity",
|
|
{
|
|
title: "Read Entity",
|
|
description:
|
|
"Read a single Sage X3 entity by its identifier. " +
|
|
"Returns the requested fields for the specified entity.",
|
|
inputSchema: z.object({
|
|
rootType: z
|
|
.enum([
|
|
"xtremX3Structure",
|
|
"xtremX3MasterData",
|
|
"xtremX3Products",
|
|
"xtremX3Purchasing",
|
|
"xtremX3Stock",
|
|
])
|
|
.describe("Root API module to query"),
|
|
entity: z.string().describe("Entity name (e.g., 'businessPartner')"),
|
|
id: z
|
|
.string()
|
|
.describe("Entity identifier (e.g., 'AE003')"),
|
|
fields: z
|
|
.array(z.string())
|
|
.describe(
|
|
"Fields to return. Use dot notation for nested fields (e.g., 'header.name')",
|
|
),
|
|
}),
|
|
},
|
|
async (args) => {
|
|
const { rootType, entity, id, fields } = args;
|
|
const config = getConfig();
|
|
|
|
const fieldSelection = buildFieldSelection(fields);
|
|
const query = `{ ${rootType} { ${entity} { read(_id: "${id}") { ${fieldSelection} } } } }`;
|
|
|
|
try {
|
|
const result = await executeGraphQL(config, query);
|
|
|
|
if (result.errors?.length) {
|
|
const messages = result.errors.map((e) => e.message).join("; ");
|
|
return {
|
|
content: [{ type: "text" as const, text: `GraphQL errors: ${messages}` }],
|
|
isError: true,
|
|
};
|
|
}
|
|
|
|
const data = result.data as Record<string, unknown> | null;
|
|
const rootData = data?.[rootType] as Record<string, unknown> | undefined;
|
|
const entityData = rootData?.[entity] as Record<string, unknown> | undefined;
|
|
const readResult = entityData?.read as Record<string, unknown> | undefined;
|
|
|
|
if (!readResult) {
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text" as const,
|
|
text: `No data found for ${entity} with id "${id}"`,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
const formatted = formatEntity(readResult);
|
|
const text = `${entity} (${id}):\n${formatted}`;
|
|
|
|
return {
|
|
content: [{ type: "text" as const, text }],
|
|
};
|
|
} catch (error: unknown) {
|
|
const message =
|
|
error instanceof Error ? error.message : String(error);
|
|
return {
|
|
content: [{ type: "text" as const, text: `Error: ${message}` }],
|
|
isError: true,
|
|
};
|
|
}
|
|
},
|
|
);
|
|
}
|