fix(mcp): remove schema byte compaction

This commit is contained in:
morluto 2026-07-16 20:30:33 +08:00
parent 0f2d01300a
commit f728295bd0
2 changed files with 4 additions and 119 deletions

View File

@ -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";

View File

@ -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);