From a53689c55d6e801d78596a36173fa2fee80be918 Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Sat, 10 Oct 2026 07:10:38 -0700 Subject: [PATCH] fix(mcp): stop advertising complete Evidence in input examples (#1560) Hosts forward every tool's input schema to the model on each request. Seventeen tools advertised a canonical example that embedded a complete producer Evidence record, up to 22 KB each, accounting for 116 KB of the 450 KB of advertised input schemas. Advertised examples now show an embedded Evidence record as its exact retained-evidence reference when the canonical input accepts one, and omit an example that can only carry inline Evidence. Canonical contract examples, their executable tests, and validation are unchanged. Co-authored-by: Claude Opus 5.5 --- docs/mcp-contracts.md | 11 ++- src/contracts/advertisedInputExamples.test.ts | 80 +++++++++++++++++++ src/contracts/advertisedInputExamples.ts | 75 +++++++++++++++++ src/contracts/toolSchemaMetadata.ts | 4 +- tests/boundary/mcp/toolSchemaValidity.test.ts | 4 +- 5 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 src/contracts/advertisedInputExamples.test.ts create mode 100644 src/contracts/advertisedInputExamples.ts diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md index 38161e1bd0..60713e9b22 100644 --- a/docs/mcp-contracts.md +++ b/docs/mcp-contracts.md @@ -26,7 +26,16 @@ health transition leaves that catalog unchanged and does not emit Advertised input and output schemas contain no reachable recursive references. Schemas share repeated definitions through schema-local references while preserving complete nested fields and validation rules. Input properties retain -their descriptions and literal examples. Root object unions are presented as +their descriptions and literal examples. + +Hosts forward input schemas to the model with every request, so advertised +examples never copy complete producer Evidence. An embedded Evidence record is +shown as its exact `{"kind":"retained-evidence","evidence_id":"ev_..."}` +reference where the tool accepts one. An example that can only carry inline +Evidence stays in the canonical contract and its executable tests, but is not +advertised. Every advertised example is a distinct valid canonical input. + +Root object unions are presented as one object without root `anyOf`, `oneOf`, or `allOf` for model API compatibility. The projection merges properties, retains shared required fields and group-specific field dependencies, and describes the accepted input groups. diff --git a/src/contracts/advertisedInputExamples.test.ts b/src/contracts/advertisedInputExamples.test.ts new file mode 100644 index 0000000000..3f7b12745d --- /dev/null +++ b/src/contracts/advertisedInputExamples.test.ts @@ -0,0 +1,80 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; + +import { advertisedInputExamples } from "./advertisedInputExamples.js"; +import { evidenceInputSchema } from "./evidenceInputContracts.js"; +import { JAVASCRIPT_FEATURE_TRACE_EXAMPLE } from "./javascript/javascriptApplicationWorkflowExamples.js"; +import { TOOL_CONTRACTS } from "./toolContracts.js"; +import { evidenceSchema } from "../domain/evidence.js"; +import type { JsonValue } from "../domain/jsonValue.js"; + +const evidence = JAVASCRIPT_FEATURE_TRACE_EXAMPLE.application; +const retained = { + kind: "retained-evidence", + evidence_id: evidence.evidence_id, +} as const; + +const embedsEvidence = (value: JsonValue): boolean => { + if (Array.isArray(value)) return value.some(embedsEvidence); + if (value === null || typeof value !== "object") return false; + return ( + evidenceSchema.safeParse(value).success || + Object.values(value).some(embedsEvidence) + ); +}; + +describe("advertised input examples", () => { + it("shows embedded Evidence as its exact retained reference", () => { + const inputSchema = z.strictObject({ + left: evidenceInputSchema, + right: z.array(evidenceInputSchema), + label: z.string(), + }); + expect( + advertisedInputExamples({ + inputSchema, + examples: [ + { + title: "Inline", + input: { left: evidence, right: [evidence], label: "a" }, + }, + { + title: "Retained", + input: { left: retained, right: [retained], label: "a" }, + }, + { title: "Other", input: { left: retained, right: [], label: "b" } }, + ], + }), + ).toEqual([ + { left: retained, right: [retained], label: "a" }, + { left: retained, right: [], label: "b" }, + ]); + }); + + it("leaves the advertisement when only inline Evidence is accepted", () => { + const inputSchema = z.strictObject({ + left: z.union([evidenceSchema, z.string()]), + }); + expect( + advertisedInputExamples({ + inputSchema, + examples: [ + { title: "Inline", input: { left: evidence } }, + { title: "Literal", input: { left: "value" } }, + ], + }), + ).toEqual([{ left: "value" }]); + }); + + it("advertises only distinct canonical inputs without complete Evidence", () => { + for (const contract of TOOL_CONTRACTS) { + const examples = advertisedInputExamples(contract); + const serialized = examples.map((example) => JSON.stringify(example)); + expect(new Set(serialized).size, contract.name).toBe(examples.length); + for (const example of examples) { + expect(contract.inputSchema.safeParse(example).success).toBe(true); + expect(embedsEvidence(example), contract.name).toBe(false); + } + } + }); +}); diff --git a/src/contracts/advertisedInputExamples.ts b/src/contracts/advertisedInputExamples.ts new file mode 100644 index 0000000000..e88832a69e --- /dev/null +++ b/src/contracts/advertisedInputExamples.ts @@ -0,0 +1,75 @@ +import type { ToolContract } from "./toolContractTypes.js"; +import { evidenceSchema } from "../domain/evidence.js"; +import type { JsonValue } from "../domain/jsonValue.js"; + +// Hosts forward every advertised input schema to the model on each turn. A +// canonical example that embeds complete producer Evidence repeats a prior +// result's bytes in that context while showing nothing about argument choice +// that the schema and descriptions do not already state; agents pass Evidence +// they received rather than composing it. + +interface RetainedProjection { + readonly value: JsonValue; + readonly embedsEvidence: boolean; +} + +/** The Evidence identifier of a complete Evidence record, or undefined. */ +const evidenceRecordId = (value: { + readonly [key: string]: JsonValue; +}): string | undefined => + typeof value.evidence_id === "string" && + Object.hasOwn(value, "normalized_result") && + evidenceSchema.safeParse(value).success + ? value.evidence_id + : undefined; + +/** Replace each embedded complete Evidence record with its exact retained reference. */ +const referenceEmbeddedEvidence = (value: JsonValue): RetainedProjection => { + if (Array.isArray(value)) { + const items = value.map(referenceEmbeddedEvidence); + return { + value: items.map((item) => item.value), + embedsEvidence: items.some((item) => item.embedsEvidence), + }; + } + if (value === null || typeof value !== "object") + return { value, embedsEvidence: false }; + const evidenceId = evidenceRecordId(value); + if (evidenceId !== undefined) + return { + value: { kind: "retained-evidence", evidence_id: evidenceId }, + embedsEvidence: true, + }; + const entries = Object.entries(value).map( + ([key, child]) => [key, referenceEmbeddedEvidence(child)] as const, + ); + return { + value: Object.fromEntries( + entries.map(([key, child]) => [key, child.value]), + ), + embedsEvidence: entries.some(([, child]) => child.embedsEvidence), + }; +}; + +/** + * Examples advertised in a tool's input schema. Embedded complete Evidence is + * shown as its exact same-session retained reference when the canonical input + * schema accepts that form; an example that can only carry inline Evidence + * stays in the canonical contract and its executable tests but leaves the + * advertisement. Every advertised example is a distinct valid canonical input. + */ +export const advertisedInputExamples = ( + contract: Pick, +): readonly JsonValue[] => { + const advertised = new Map(); + for (const { input } of contract.examples) { + const projection = referenceEmbeddedEvidence(input); + if ( + projection.embedsEvidence && + !contract.inputSchema.safeParse(projection.value).success + ) + continue; + advertised.set(JSON.stringify(projection.value), projection.value); + } + return [...advertised.values()]; +}; diff --git a/src/contracts/toolSchemaMetadata.ts b/src/contracts/toolSchemaMetadata.ts index 34089a929f..5ed63db5b4 100644 --- a/src/contracts/toolSchemaMetadata.ts +++ b/src/contracts/toolSchemaMetadata.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import type { ToolContract } from "./toolContractTypes.js"; +import { advertisedInputExamples } from "./advertisedInputExamples.js"; import { presentInputJsonSchema } from "./inputSchemaPresentation.js"; import { digestCanonicalValue } from "../domain/canonicalDigest.js"; @@ -63,9 +64,10 @@ export const toolInputSchemaWithMetadata = ( ...options, libraryOptions: { reused: "ref", ...options.libraryOptions }, }); + const examples = advertisedInputExamples(contract); return { ...presentInputJsonSchema(shared, fallbackPropertyDescription), - examples: contract.examples.map(({ input }) => input), + ...(examples.length === 0 ? {} : { examples }), }; }, ); diff --git a/tests/boundary/mcp/toolSchemaValidity.test.ts b/tests/boundary/mcp/toolSchemaValidity.test.ts index 993a5e00f5..1a27826d89 100644 --- a/tests/boundary/mcp/toolSchemaValidity.test.ts +++ b/tests/boundary/mcp/toolSchemaValidity.test.ts @@ -4,6 +4,7 @@ import { Ajv2020 } from "ajv/dist/2020.js"; import { describe, expect, it } from "vitest"; import { z } from "zod"; +import { advertisedInputExamples } from "../../../src/contracts/advertisedInputExamples.js"; import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js"; import { emptyArraySchema } from "../../../src/domain/emptyArraySchema.js"; import { processScenarioSchema } from "../../../src/domain/process/processScenario.js"; @@ -353,8 +354,9 @@ describe("MCP JSON Schema validity", () => { const tool = byName.get(contract.name); expect(tool?.title?.trim(), contract.name).toBeTruthy(); expect(tool?.description?.trim(), contract.name).toBeTruthy(); + const examples = advertisedInputExamples(contract); expect(tool?.inputSchema.examples, contract.name).toEqual( - contract.examples.map(({ input }) => input), + examples.length === 0 ? undefined : examples, ); for (const example of contract.examples) expect(