fix(mcp): remove schema byte compaction
This commit is contained in:
parent
0f2d01300a
commit
f728295bd0
|
|
@ -23,13 +23,9 @@ const advertisedInputSchema = (schema: z.ZodObject) => ({
|
|||
validate: (input: unknown) => ({ value: input }),
|
||||
jsonSchema: {
|
||||
input: () =>
|
||||
compactJsonSchema(
|
||||
describeProperties(closeObjectSchemas(inputJsonSchema(schema))),
|
||||
),
|
||||
describeProperties(closeObjectSchemas(inputJsonSchema(schema))),
|
||||
output: () =>
|
||||
compactJsonSchema(
|
||||
describeProperties(closeObjectSchemas(inputJsonSchema(schema))),
|
||||
),
|
||||
describeProperties(closeObjectSchemas(inputJsonSchema(schema))),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -45,14 +41,9 @@ const advertisedOutputSchema = (schema: z.ZodObject) => {
|
|||
"~standard": {
|
||||
...standard,
|
||||
jsonSchema: {
|
||||
input: () =>
|
||||
compactJsonSchema(
|
||||
closeObjectSchemas(converter({ target: "draft-2020-12" })),
|
||||
),
|
||||
input: () => closeObjectSchemas(converter({ target: "draft-2020-12" })),
|
||||
output: () =>
|
||||
compactJsonSchema(
|
||||
closeObjectSchemas(converter({ target: "draft-2020-12" })),
|
||||
),
|
||||
closeObjectSchemas(converter({ target: "draft-2020-12" })),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
@ -118,102 +109,3 @@ const describeValue = (value: unknown): unknown => {
|
|||
|
||||
const isObject = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
/** Replace repeated schema fragments with deterministic tool-local definitions. */
|
||||
const compactJsonSchema = (
|
||||
schema: Readonly<Record<string, unknown>>,
|
||||
): Record<string, unknown> => {
|
||||
const { $schema: _dialect, ...schemaBody } = schema;
|
||||
const counts = new Map<string, number>();
|
||||
collectSchemas(schemaBody, counts, true);
|
||||
const candidates = [...counts.entries()]
|
||||
.filter(([, count]) => count > 1)
|
||||
.sort(
|
||||
([left], [right]) =>
|
||||
right.length - left.length || left.localeCompare(right),
|
||||
)
|
||||
.map(([encoded]) => encoded);
|
||||
if (candidates.length === 0) return schemaBody;
|
||||
let compacted: Record<string, unknown> = schemaBody;
|
||||
let compactedBytes = JSON.stringify(compacted).length;
|
||||
const selected: Array<readonly [string, string]> = [];
|
||||
for (const encoded of candidates) {
|
||||
const proposed = [
|
||||
...selected,
|
||||
[encoded, `rea_${String(selected.length)}`] as const,
|
||||
];
|
||||
const candidate = compactWithDefinitions(schemaBody, proposed);
|
||||
const candidateBytes = JSON.stringify(candidate).length;
|
||||
if (candidateBytes >= compactedBytes) continue;
|
||||
selected.push([encoded, `rea_${String(selected.length)}`]);
|
||||
compacted = candidate;
|
||||
compactedBytes = candidateBytes;
|
||||
}
|
||||
return compacted;
|
||||
};
|
||||
|
||||
const compactWithDefinitions = (
|
||||
schemaBody: Readonly<Record<string, unknown>>,
|
||||
candidates: readonly (readonly [string, string])[],
|
||||
): Record<string, unknown> => {
|
||||
const names = new Map(candidates);
|
||||
const definitions = Object.fromEntries(
|
||||
candidates.map(([encoded, name]) => [
|
||||
name,
|
||||
replaceSchemas(JSON.parse(encoded), names, encoded),
|
||||
]),
|
||||
);
|
||||
const compacted = replaceSchemas(schemaBody, names);
|
||||
if (!isObject(compacted))
|
||||
throw new TypeError("Compacted schema root is not an object");
|
||||
return {
|
||||
...compacted,
|
||||
$defs: {
|
||||
...(isObject(schemaBody.$defs) ? schemaBody.$defs : {}),
|
||||
...definitions,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const collectSchemas = (
|
||||
value: unknown,
|
||||
counts: Map<string, number>,
|
||||
root = false,
|
||||
keywordContainer = false,
|
||||
): void => {
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) collectSchemas(item, counts);
|
||||
return;
|
||||
}
|
||||
if (!isObject(value)) return;
|
||||
if (!root && !keywordContainer && value.$ref === undefined) {
|
||||
const encoded = JSON.stringify(value);
|
||||
counts.set(encoded, (counts.get(encoded) ?? 0) + 1);
|
||||
}
|
||||
for (const [key, child] of Object.entries(value))
|
||||
collectSchemas(child, counts, false, schemaMapKeyword(key));
|
||||
};
|
||||
|
||||
const replaceSchemas = (
|
||||
value: unknown,
|
||||
names: ReadonlyMap<string, string>,
|
||||
skipped?: string,
|
||||
keywordContainer = false,
|
||||
): unknown => {
|
||||
if (Array.isArray(value))
|
||||
return value.map((item) => replaceSchemas(item, names, skipped));
|
||||
if (!isObject(value)) return value;
|
||||
const encoded = JSON.stringify(value);
|
||||
const name = names.get(encoded);
|
||||
if (!keywordContainer && name !== undefined && encoded !== skipped)
|
||||
return { $ref: `#/$defs/${name}` };
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, child]) => [
|
||||
key,
|
||||
replaceSchemas(child, names, skipped, schemaMapKeyword(key)),
|
||||
]),
|
||||
);
|
||||
};
|
||||
|
||||
const schemaMapKeyword = (key: string): boolean =>
|
||||
key === "properties" || key === "patternProperties" || key === "$defs";
|
||||
|
|
|
|||
|
|
@ -210,17 +210,10 @@ describe("full MCP integration with multi-tool sequences", () => {
|
|||
expect(names).toContain("binary_overview");
|
||||
expect(names).toContain("batch_decompile");
|
||||
|
||||
expect(
|
||||
Buffer.byteLength(JSON.stringify(listed), "utf8"),
|
||||
).toBeLessThanOrEqual(512 * 1024);
|
||||
const contracts = new Map<string, (typeof TOOL_CONTRACTS)[number]>(
|
||||
TOOL_CONTRACTS.map((contract) => [contract.name, contract]),
|
||||
);
|
||||
for (const tool of listed.tools) {
|
||||
expect(
|
||||
Buffer.byteLength(JSON.stringify(tool), "utf8"),
|
||||
tool.name,
|
||||
).toBeLessThanOrEqual(64 * 1024);
|
||||
const contract = contracts.get(tool.name);
|
||||
expect(contract, tool.name).toBeDefined();
|
||||
expect(tool.title, tool.name).toBe(contract?.title);
|
||||
|
|
|
|||
Loading…
Reference in New Issue