feat: everything
This commit is contained in:
144
src/tools/read-entity.ts
Normal file
144
src/tools/read-entity.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user