perf(mcp): advertise each tool JSON Schema once per target (#1059)

The MCP SDK reconverts every registered Zod schema on each tools/list.
With 139 tools that is ~45 ms of Zod JSON Schema work per listing, for a
catalog that cannot change after registration. Memoize the advertised
input and output projections per target on the registration clones.

Listings are byte-identical; repeated tools/list drops from ~62 ms to
~18 ms on the stdio server.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822 2026-10-08 02:31:08 -07:00 committed by GitHub
parent 163ee09a74
commit 5ccb9241be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 108 additions and 15 deletions

View File

@ -0,0 +1,55 @@
import { describe, expect, it } from "vitest";
import { TOOL_CONTRACTS, toolContract } from "./toolContracts.js";
import {
toolInputSchemaWithMetadata,
toolOutputSchemaWithMetadata,
} from "./toolSchemaMetadata.js";
const target = "draft-2020-12";
describe("advertised tool JSON Schema", () => {
it("projects each canonical contract once per target", () => {
for (const contract of TOOL_CONTRACTS) {
const input = toolInputSchemaWithMetadata(contract)["~standard"];
const output = toolOutputSchemaWithMetadata(contract)["~standard"];
const advertisedInput = input.jsonSchema.input({ target });
const advertisedOutput = output.jsonSchema.output({ target });
expect(input.jsonSchema.input({ target }), contract.name).toBe(
advertisedInput,
);
expect(output.jsonSchema.output({ target }), contract.name).toBe(
advertisedOutput,
);
expect(advertisedInput.examples, contract.name).toEqual(
contract.examples.map(({ input: example }) => example),
);
expect(advertisedOutput, contract.name).toEqual(
contract.outputSchema["~standard"].jsonSchema.output({ target }),
);
}
});
it("keeps other targets and library options distinct", () => {
const contract = toolContract("open_binary");
const input = toolInputSchemaWithMetadata(contract)["~standard"];
const latest = input.jsonSchema.input({ target });
const draft07 = input.jsonSchema.input({ target: "draft-07" });
const libraryOptions = { unrepresentable: "any" };
const configured = input.jsonSchema.input({ target, libraryOptions });
expect(latest.$schema).toBe("https://json-schema.org/draft/2020-12/schema");
expect(draft07.$schema).toBe("http://json-schema.org/draft-07/schema#");
expect(input.jsonSchema.input({ target: "draft-07" })).toBe(draft07);
expect(configured).not.toBe(latest);
expect(input.jsonSchema.input({ target, libraryOptions })).not.toBe(
configured,
);
expect(
toolInputSchemaWithMetadata(contract).safeParse(
contract.examples[0]?.input,
).success,
).toBe(true);
});
});

View File

@ -47,37 +47,72 @@ const PROPERTY_DESCRIPTIONS: Readonly<Record<string, string>> = {
unknown_id: "Exact residual-unknown identifier.",
};
type JsonSchemaProjection = z.ZodType["~standard"]["jsonSchema"]["input"];
/** Attach caller guidance to a canonical schema for the SDK wire projection. */
export const toolInputSchemaWithMetadata = <Contract extends ToolContract>(
contract: Contract,
): Contract["inputSchema"] => {
const schema = contract.inputSchema.meta(
z.globalRegistry.get(contract.inputSchema) ?? {},
): Contract["inputSchema"] =>
withAdvertisedJsonSchema(
contract.inputSchema,
"input",
(project) => (options) => ({
...describeProperties(project(options)),
examples: contract.examples.map(({ input }) => input),
}),
);
const standard = schema["~standard"];
const inputJsonSchema = standard.jsonSchema?.input;
if (inputJsonSchema === undefined)
throw new TypeError(
"Tool input schema does not expose Standard JSON Schema",
);
/** Advertise a canonical output schema without reconverting it per listing. */
export const toolOutputSchemaWithMetadata = <Contract extends ToolContract>(
contract: Contract,
): Contract["outputSchema"] =>
withAdvertisedJsonSchema(contract.outputSchema, "output");
const withAdvertisedJsonSchema = <Schema extends z.ZodType>(
canonical: Schema,
io: "input" | "output",
advertise: (project: JsonSchemaProjection) => JsonSchemaProjection = (
project,
) => project,
): Schema => {
// Zod's input projection drops root metadata when a descendant transforms.
// Preserve the parser and let the SDK own conversion of everything else.
const schema = canonical.meta(z.globalRegistry.get(canonical) ?? {});
const standard = schema["~standard"];
const project = standard.jsonSchema?.[io];
if (project === undefined)
throw new TypeError(
`Tool ${io} schema does not expose Standard JSON Schema`,
);
Object.defineProperty(schema, "~standard", {
value: {
...standard,
jsonSchema: {
...standard.jsonSchema,
input: (options: Parameters<typeof inputJsonSchema>[0]) => ({
...describeProperties(inputJsonSchema(options)),
examples: contract.examples.map(({ input }) => input),
}),
[io]: memoizeByTarget(advertise(project)),
},
},
});
return schema;
};
// The SDK reconverts every registered schema on each `tools/list`, although a
// registered contract cannot change. Like the SDK's own memoized output
// projection, the advertised value is shared and treated as read-only.
const memoizeByTarget = (
project: JsonSchemaProjection,
): JsonSchemaProjection => {
const byTarget = new Map<string, Record<string, unknown>>();
return (options) => {
if (options.libraryOptions !== undefined) return project(options);
const cached = byTarget.get(options.target);
if (cached !== undefined) return cached;
const projected = project(options);
byTarget.set(options.target, projected);
return projected;
};
};
const describeProperties = (
value: Readonly<Record<string, unknown>>,
): Record<string, unknown> =>

View File

@ -1,5 +1,8 @@
import type { ToolContract } from "../contracts/toolContracts.js";
import { toolInputSchemaWithMetadata } from "../contracts/toolSchemaMetadata.js";
import {
toolInputSchemaWithMetadata,
toolOutputSchemaWithMetadata,
} from "../contracts/toolSchemaMetadata.js";
/** Project the canonical Zod contracts directly into SDK registration. */
export const toolRegistrationOptions = <Contract extends ToolContract>(
@ -14,6 +17,6 @@ export const toolRegistrationOptions = <Contract extends ToolContract>(
title: contract.title,
description: contract.description,
inputSchema: toolInputSchemaWithMetadata(contract),
outputSchema: contract.outputSchema,
outputSchema: toolOutputSchemaWithMetadata(contract),
annotations: contract.annotations,
});