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:
parent
163ee09a74
commit
5ccb9241be
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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> =>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue