Files
openclaw/src/gateway/mcp-http.schema.ts
2026-04-04 15:34:13 +09:00

88 lines
2.8 KiB
TypeScript

import { logWarn } from "../logger.js";
import { resolveGatewayScopedTools } from "./tool-resolution.js";
export type McpLoopbackTool = ReturnType<typeof resolveGatewayScopedTools>["tools"][number];
export type McpToolSchemaEntry = {
name: string;
description: string | undefined;
inputSchema: Record<string, unknown>;
};
function flattenUnionSchema(raw: Record<string, unknown>): Record<string, unknown> {
const variants = (raw.anyOf ?? raw.oneOf) as Record<string, unknown>[] | undefined;
if (!Array.isArray(variants) || variants.length === 0) {
return raw;
}
const mergedProps: Record<string, unknown> = {};
const requiredSets: Set<string>[] = [];
for (const variant of variants) {
const props = variant.properties as Record<string, unknown> | undefined;
if (props) {
for (const [key, schema] of Object.entries(props)) {
if (!(key in mergedProps)) {
mergedProps[key] = schema;
continue;
}
const existing = mergedProps[key] as Record<string, unknown>;
const incoming = schema as Record<string, unknown>;
if (Array.isArray(existing.enum) && Array.isArray(incoming.enum)) {
mergedProps[key] = {
...existing,
enum: [...new Set([...(existing.enum as unknown[]), ...(incoming.enum as unknown[])])],
};
continue;
}
if ("const" in existing && "const" in incoming && existing.const !== incoming.const) {
const merged: Record<string, unknown> = {
...existing,
enum: [existing.const, incoming.const],
};
delete merged.const;
mergedProps[key] = merged;
continue;
}
logWarn(
`mcp loopback: conflicting schema definitions for "${key}", keeping the first variant`,
);
}
}
requiredSets.push(
new Set(Array.isArray(variant.required) ? (variant.required as string[]) : []),
);
}
const required =
requiredSets.length > 0
? [
...requiredSets.reduce(
(left, right) => new Set([...left].filter((key) => right.has(key))),
),
]
: [];
const { anyOf: _anyOf, oneOf: _oneOf, ...rest } = raw;
return { ...rest, type: "object", properties: mergedProps, required };
}
export function buildMcpToolSchema(tools: McpLoopbackTool[]): McpToolSchemaEntry[] {
return tools.map((tool) => {
let raw =
tool.parameters && typeof tool.parameters === "object"
? { ...(tool.parameters as Record<string, unknown>) }
: {};
if (raw.anyOf || raw.oneOf) {
raw = flattenUnionSchema(raw);
}
if (raw.type !== "object") {
raw.type = "object";
if (!raw.properties) {
raw.properties = {};
}
}
return {
name: tool.name,
description: tool.description,
inputSchema: raw,
};
});
}