feat(mcp): support compact output schema advertisements (#7218)

Add opt-in bounded output-schema advertisements while preserving the full default, canonical server validation and complete structured content. Verify real stdio discovery under a 1 MiB receive cap, all 5,367 libcrypto export coordinates, complete Evidence parity, whole-task serialization estimates and resource bounds.

Fixes #2512. Integrates #7054 by Akshad Jogi with original authorship retained.

Co-authored-by: Akshad Jogi <akshadjogi@gmail.com>
This commit is contained in:
gensokyo-devandAkshad Jogi authored and GitHub committed 2026-10-26 17:59:51 +08:00
1 parent e596a48251
commit 9085222ca1
22 files changed
+1831 -771

No files matched your search

+60 -50
View File
@@ -63,6 +63,64 @@ const assertCliCatalogIdentity = async (
}
};
const catalogProfiles = [
{
name: "default compact input",
environment: {
REA_MCP_INPUT_SCHEMA_PROFILE: undefined,
REA_MCP_OUTPUT_SCHEMA_PROFILE: "full",
},
identity: createCatalogIdentity(
compactPresentationForMcpInputSchemaProfile(undefined),
),
},
{
name: "full",
environment: {
REA_MCP_INPUT_SCHEMA_PROFILE: "full",
REA_MCP_OUTPUT_SCHEMA_PROFILE: "full",
},
identity: FULL_CATALOG_IDENTITY,
},
{
name: "compact",
environment: {
REA_MCP_INPUT_SCHEMA_PROFILE: "compact",
REA_MCP_OUTPUT_SCHEMA_PROFILE: "full",
},
identity: createCatalogIdentity(
compactPresentationForMcpInputSchemaProfile("compact"),
),
},
{
name: "output schemas off",
environment: {
REA_MCP_INPUT_SCHEMA_PROFILE: "full",
REA_MCP_OUTPUT_SCHEMA_PROFILE: "off",
},
identity: createCatalogIdentity(undefined, "off"),
},
{
name: "output schemas compact",
environment: {
REA_MCP_INPUT_SCHEMA_PROFILE: "full",
REA_MCP_OUTPUT_SCHEMA_PROFILE: "compact",
},
identity: createCatalogIdentity(undefined, "compact"),
},
{
name: "compact input and output schemas off",
environment: {
REA_MCP_INPUT_SCHEMA_PROFILE: "compact",
REA_MCP_OUTPUT_SCHEMA_PROFILE: "off",
},
identity: createCatalogIdentity(
compactPresentationForMcpInputSchemaProfile("compact"),
"off",
),
},
];
describe("production MCP doctor", () => {
it("reuses immutable compiled profile digests and preserves custom-budget identity", async () => {
// Exercise the generated module with Node, outside Vitest's source loader.
@@ -79,7 +137,7 @@ describe("production MCP doctor", () => {
assert.ok(Object.isFrozen(profiles));
for (const input of ["full", "compact"]) {
assert.ok(Object.isFrozen(profiles[input]));
for (const output of ["full", "off"]) {
for (const output of ["full", "compact", "off"]) {
const identity = createCatalogIdentity(input === "compact" ? { budgetBytes } : undefined, output);
assert.equal(identity.digests, profiles[input][output]);
assert.ok(Object.isFrozen(identity.digests));
@@ -124,55 +182,7 @@ describe("production MCP doctor", () => {
});
});
it.each([
{
name: "default compact input",
environment: {
REA_MCP_INPUT_SCHEMA_PROFILE: undefined,
REA_MCP_OUTPUT_SCHEMA_PROFILE: "full",
},
identity: createCatalogIdentity(
compactPresentationForMcpInputSchemaProfile(undefined),
),
},
{
name: "full",
environment: {
REA_MCP_INPUT_SCHEMA_PROFILE: "full",
REA_MCP_OUTPUT_SCHEMA_PROFILE: "full",
},
identity: FULL_CATALOG_IDENTITY,
},
{
name: "compact",
environment: {
REA_MCP_INPUT_SCHEMA_PROFILE: "compact",
REA_MCP_OUTPUT_SCHEMA_PROFILE: "full",
},
identity: createCatalogIdentity(
compactPresentationForMcpInputSchemaProfile("compact"),
),
},
{
name: "output schemas off",
environment: {
REA_MCP_INPUT_SCHEMA_PROFILE: "full",
REA_MCP_OUTPUT_SCHEMA_PROFILE: "off",
},
identity: createCatalogIdentity(undefined, "off"),
},
{
name: "compact input and output schemas off",
environment: {
REA_MCP_INPUT_SCHEMA_PROFILE: "compact",
REA_MCP_OUTPUT_SCHEMA_PROFILE: "off",
},
identity: createCatalogIdentity(
compactPresentationForMcpInputSchemaProfile("compact"),
"off",
),
},
])(
it.each(catalogProfiles)(
"verifies the $name catalog through production stdio and both CLI inventories",
async ({ environment, identity }) => {
const result = await runProductionMcpDoctor({
+239 -9
View File
@@ -1,4 +1,5 @@
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { Ajv2020 } from "ajv/dist/2020.js";
import { z } from "zod";
import { expect, it } from "vitest";
@@ -6,24 +7,34 @@ import {
TOOL_CONTRACTS,
toolContract,
} from "../../../src/contracts/toolContracts.js";
import { COMPACT_OUTPUT_SCHEMA_BUDGET_BYTES } from "../../../src/config/mcpOutputSchemaProfile.js";
import { COMPACT_INPUT_SCHEMA_BUDGET_BYTES } from "../../../src/config/mcpInputSchemaProfile.js";
import type { CompactInputSchemaPresentation } from "../../../src/contracts/compactInputSchema.js";
import { EvidenceMcpServer } from "../../../src/server/EvidenceMcpServer.js";
import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js";
import { createToolResultDelivery } from "../../../src/server/toolResult.js";
import type { ToolContract } from "../../../src/contracts/toolContractTypes.js";
import { FULL_CATALOG_IDENTITY } from "../../../src/catalogIdentity.js";
import type { McpOutputSchemaProfile } from "../../../src/config/mcpOutputSchemaProfile.js";
import { advertisedSchemaErrors } from "../../fixtures/mcpAdvertisedSchemaValidity.js";
const createServer = (omitOutputSchemas: boolean) =>
const createServer = (
outputSchemaProfile: McpOutputSchemaProfile,
compactInputPresentation:
| CompactInputSchemaPresentation
| undefined = undefined,
) =>
new EvidenceMcpServer(
{ name: "output-schema-fixture", version: "1" },
{ capabilities: {} },
undefined,
createToolResultDelivery(10 * 1024 * 1024),
undefined,
omitOutputSchemas,
compactInputPresentation,
outputSchemaProfile,
);
it("omits only output schema advertisements and keeps canonical MCP output validation", async () => {
const server = createServer(true);
const server = createServer("off");
expect(server.catalogIdentity.digests.combined_sha256).not.toBe(
FULL_CATALOG_IDENTITY.digests.combined_sha256,
);
@@ -86,9 +97,97 @@ it("omits only output schema advertisements and keeps canonical MCP output valid
}
});
it("keeps a full catalog below a measured one-megabyte frame when output schemas are off", async () => {
const measure = async (omitOutputSchemas: boolean): Promise<number> => {
const server = createServer(omitOutputSchemas);
it("bounds output schema advertisements and keeps canonical MCP output validation", async () => {
const server = createServer("compact");
expect(server.catalogIdentity.digests.combined_sha256).not.toBe(
FULL_CATALOG_IDENTITY.digests.combined_sha256,
);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const client = new Client({ name: "compact-output-test", version: "1" });
const contract = {
...toolContract("comment"),
name: "compact_output_schema_fixture",
inputSchema: z.strictObject({}),
outputSchema: z.strictObject({
value: z.string().describe("Nested guidance leaves."),
nested: z
.object({ count: z.number().int() })
.describe("Nested guidance leaves."),
}),
} satisfies ToolContract;
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async () => ({
content: [{ type: "text" as const, text: "fixture" }],
structuredContent: { value: "complete", nested: { count: 1 } },
}),
);
const invalidContract = {
...contract,
name: "invalid_compact_output_schema_fixture",
} satisfies ToolContract;
server.registerTool(
invalidContract.name,
toolRegistrationOptions(invalidContract),
async () => ({
content: [{ type: "text" as const, text: "invalid fixture" }],
structuredContent: { value: 42, nested: { count: 1 } },
}),
);
await server.connect(serverTransport);
await client.connect(clientTransport);
try {
const tools = (await client.listTools()).tools;
expect(tools).toHaveLength(2);
const advertised = tools.find((tool) => tool.name === contract.name)!
.outputSchema as Record<string, unknown>;
expect(advertised.type).toBe("object");
expect(advertised.$id).toBeUndefined();
expect((advertised.properties as Record<string, unknown>).value).toEqual({
type: "string",
});
expect(
(advertised.properties as Record<string, unknown>).nested,
).toMatchObject({ type: "object" });
const success = await client.callTool({
name: contract.name,
arguments: {},
});
expect(success.isError).not.toBe(true);
expect(success.structuredContent).toEqual({
value: "complete",
nested: { count: 1 },
});
const invalid = await client.callTool({
name: invalidContract.name,
arguments: {},
});
expect(invalid.isError).toBe(true);
expect(invalid.content).toEqual([
expect.objectContaining({
type: "text",
text: expect.stringContaining("Output validation error"),
}),
]);
} finally {
await client.close();
await server.close();
}
});
it("keeps the default compact-input catalog below a measured one-megabyte frame", async () => {
const compactInput: CompactInputSchemaPresentation = {
budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES,
};
const measure = async (
profile: McpOutputSchemaProfile,
inputPresentation: CompactInputSchemaPresentation | undefined,
): Promise<number> => {
const server = createServer(profile, inputPresentation);
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const client = new Client({ name: "catalog-size-test", version: "1" });
@@ -104,6 +203,26 @@ it("keeps a full catalog below a measured one-megabyte frame when output schemas
await client.connect(clientTransport);
try {
const listed = await client.listTools();
if (profile === "compact") {
// Validate the advertised documents as the client receives them, not
// just their bytes: a projection that broke a reference or left an
// invalid schema would otherwise still pass the budget assertion.
expect(
advertisedSchemaErrors(
listed.tools.map((tool) => ({
name: tool.name,
inputSchema: tool.inputSchema,
outputSchema: tool.outputSchema,
})),
),
).toEqual([]);
for (const tool of listed.tools) {
expect(tool.outputSchema).toBeDefined();
expect(
Buffer.byteLength(JSON.stringify(tool.outputSchema), "utf8"),
).toBeLessThanOrEqual(COMPACT_OUTPUT_SCHEMA_BUDGET_BYTES);
}
}
return Buffer.byteLength(JSON.stringify(listed), "utf8");
} finally {
await client.close();
@@ -111,9 +230,120 @@ it("keeps a full catalog below a measured one-megabyte frame when output schemas
}
};
const fullBytes = await measure(false);
const compactBytes = await measure(true);
const fullBytes = await measure("full", undefined);
const compactBytes = await measure("compact", compactInput);
const offBytes = await measure("off", compactInput);
const fullInputOffBytes = await measure("off", undefined);
expect(fullBytes).toBeGreaterThan(1024 * 1024);
expect(compactBytes).toBeLessThan(1024 * 1024);
expect(offBytes).toBeLessThan(1024 * 1024);
expect(fullInputOffBytes).toBeLessThan(1024 * 1024);
expect(compactBytes).toBeLessThan(fullBytes);
expect(compactBytes).toBeGreaterThan(offBytes);
});
it("rejects canonical-invalid output that the reduced advertisement accepts", async () => {
// Enough nested detail that the compact projection cannot inline the
// canonical schema within the budget, so the advertised form is the shallow
// root shape rather than the complete validation structure.
const nestedDetail = Object.fromEntries(
Array.from({ length: 150 }, (_, index) => [
`nested_${String(index).padStart(3, "0")}`,
z.string().optional(),
]),
);
const outputSchema = z.strictObject({
value: z.string(),
payload: z.object({
inner: z.string(),
count: z.number().int(),
...nestedDetail,
}),
});
const server = createServer("compact");
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
const client = new Client({ name: "reduced-output-test", version: "1" });
const contract = {
...toolContract("comment"),
name: "reduced_output_schema_fixture",
inputSchema: z.strictObject({}),
outputSchema,
} satisfies ToolContract;
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async () => ({
content: [{ type: "text" as const, text: "fixture" }],
structuredContent: {
value: "complete",
payload: { inner: "text", count: 1 },
},
}),
);
// `payload.inner` is a string canonically. The shallow advertisement keeps
// only `payload: {type: "object"}`, so it accepts the malformed value.
const malformed = {
value: "complete",
payload: { inner: 42, count: 1 },
};
const invalidContract = {
...contract,
name: "invalid_reduced_output_schema_fixture",
} satisfies ToolContract;
server.registerTool(
invalidContract.name,
toolRegistrationOptions(invalidContract),
async () => ({
content: [{ type: "text" as const, text: "invalid fixture" }],
structuredContent: malformed,
}),
);
await server.connect(serverTransport);
await client.connect(clientTransport);
try {
const tools = (await client.listTools()).tools;
const advertised = tools.find((tool) => tool.name === contract.name)!
.outputSchema as Record<string, unknown>;
expect(advertised.description).toContain("reduced form");
expect((advertised.properties as Record<string, unknown>).payload).toEqual({
type: "object",
});
expect(
Buffer.byteLength(JSON.stringify(advertised), "utf8"),
).toBeLessThanOrEqual(COMPACT_OUTPUT_SCHEMA_BUDGET_BYTES);
// The regression this guards: the advertisement accepts a value the
// canonical contract rejects, so validating results against the
// advertisement instead of the contract would deliver malformed output.
const validateAdvertised = new Ajv2020({
strict: false,
validateFormats: false,
}).compile(advertised);
expect(validateAdvertised(malformed)).toBe(true);
const success = await client.callTool({
name: contract.name,
arguments: {},
});
expect(success.isError).not.toBe(true);
expect(success.structuredContent).toEqual({
value: "complete",
payload: { inner: "text", count: 1 },
});
const invalid = await client.callTool({
name: invalidContract.name,
arguments: {},
});
expect(invalid.isError).toBe(true);
expect(invalid.content).toEqual([
expect.objectContaining({
type: "text",
text: expect.stringContaining("Output validation error"),
}),
]);
} finally {
await client.close();
await server.close();
}
});
+7 -47
View File
@@ -13,6 +13,7 @@ import {
arrayItemSchemaError,
unsupportedInputLiteralSchemaError,
} from "../../fixtures/mcpClientSchemaConstraints.js";
import { advertisedSchemaErrors } from "../../fixtures/mcpAdvertisedSchemaValidity.js";
import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js";
import { servedCatalog } from "../../../src/application/environment/environmentState.js";
import { servedFrom } from "../../fixtures/environmentStateServing.js";
@@ -31,47 +32,6 @@ interface ToolSchemas {
readonly annotations?: Record<string, unknown> | undefined;
}
function schemaErrors(tools: readonly ToolSchemas[]): string[] {
// Reuse Ajv's meta-schema validators, then clear each root's reference
// namespace, including nested IDs, before validating the next schema.
const ajv = new Ajv2020({
strict: false,
validateFormats: false,
addUsedSchema: false,
});
return tools.flatMap((tool) =>
["inputSchema", "outputSchema"].flatMap((kind) => {
const schema =
kind === "inputSchema" ? tool.inputSchema : tool.outputSchema;
if (schema === undefined) return [];
try {
if (!ajv.validateSchema(schema))
return [`${tool.name}.${kind}: ${ajv.errorsText(ajv.errors)}`];
ajv.compile(schema);
const clientError = arrayItemSchemaError(
schema,
`${tool.name}.${kind}`,
);
if (clientError !== undefined) return [clientError];
if (kind === "inputSchema") {
const literalError = unsupportedInputLiteralSchemaError(
schema,
`${tool.name}.${kind}`,
);
if (literalError !== undefined) return [literalError];
}
return [];
} catch (cause: unknown) {
return [
`${tool.name}.${kind}: ${cause instanceof Error ? cause.message : String(cause)}`,
];
} finally {
ajv.removeSchema();
}
}),
);
}
function expectStrictInputSchemaParity(
tools: readonly ToolSchemas[],
ajv: Ajv2020,
@@ -484,7 +444,7 @@ describe("served environment catalogs", () => {
);
expect(tools.some((tool) => tool.name === "delete_books")).toBe(true);
expect(
schemaErrors(
advertisedSchemaErrors(
tools.map((tool) => ({
name: tool.name,
inputSchema: tool.inputSchema as unknown as Record<string, unknown>,
@@ -534,7 +494,7 @@ describe("MCP JSON Schema validity", () => {
expect(tools.map(({ name }) => name).sort()).toEqual(
TOOL_CONTRACTS.map(({ name }) => name).sort(),
);
expect(schemaErrors(tools)).toEqual([]);
expect(advertisedSchemaErrors(tools)).toEqual([]);
expect(localInputReferences(tools)).toEqual([]);
expect(recursiveReferences(tools)).toEqual([]);
expectKnownAuthorityHints(tools);
@@ -879,7 +839,7 @@ describe("MCP process input JSON Schema", () => {
});
it("ships nonrecursive input and output schemas in the generated catalog", () => {
expect(schemaErrors(GENERATED_MCP_TOOL_CATALOG)).toEqual([]);
expect(advertisedSchemaErrors(GENERATED_MCP_TOOL_CATALOG)).toEqual([]);
expect(recursiveReferences(GENERATED_MCP_TOOL_CATALOG)).toEqual([]);
});
@@ -888,7 +848,7 @@ it("ships input schemas without local references in the generated catalog", () =
});
it("rejects unresolved output-schema references even for uncalled tools", () => {
const errors = schemaErrors([
const errors = advertisedSchemaErrors([
{
name: "dangling",
inputSchema: { type: "object" },
@@ -901,7 +861,7 @@ it("rejects unresolved output-schema references even for uncalled tools", () =>
it("validates each tool independently when output schema IDs repeat", () => {
const outputSchema = { $id: "urn:rea:test:shared-output", type: "string" };
const errors = schemaErrors([
const errors = advertisedSchemaErrors([
{ name: "first", inputSchema: { type: "object" }, outputSchema },
{
name: "second",
@@ -974,7 +934,7 @@ it.each([false, true])(
"rejects references to another tool's nested IDs (earlier compilation fails: %s)",
(invalidPrevious) => {
const nestedId = "urn:rea:test:nested-output";
const errors = schemaErrors([
const errors = advertisedSchemaErrors([
{
name: "first",
inputSchema: { type: "object" },
@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import {
COMPACT_OUTPUT_SCHEMA_BUDGET_BYTES,
compactPresentationForMcpOutputSchemaProfile,
parseMcpOutputSchemaProfile,
} from "../../../src/config/mcpOutputSchemaProfile.js";
describe("MCP output schema profile configuration", () => {
it("accepts the full, compact, and off presentations", () => {
expect(parseMcpOutputSchemaProfile(undefined)).toEqual({
ok: true,
value: undefined,
});
for (const value of ["full", "compact", "off"] as const)
expect(parseMcpOutputSchemaProfile(value)).toEqual({
ok: true,
value,
});
});
it.each(["", "FULL", "compact ", "off ", "minimal", "0"])(
"rejects unknown profiles: %j",
(value) => {
const parsed = parseMcpOutputSchemaProfile(value);
if (parsed.ok) throw new Error("Expected invalid profile");
expect(parsed.error.message).toContain("REA_MCP_OUTPUT_SCHEMA_PROFILE");
},
);
it("selects a bounded presentation only for compact", () => {
expect(compactPresentationForMcpOutputSchemaProfile("compact")).toEqual({
budgetBytes: COMPACT_OUTPUT_SCHEMA_BUDGET_BYTES,
});
expect(compactPresentationForMcpOutputSchemaProfile(undefined)).toBe(
undefined,
);
expect(compactPresentationForMcpOutputSchemaProfile("full")).toBe(
undefined,
);
expect(compactPresentationForMcpOutputSchemaProfile("off")).toBe(undefined);
});
});
+67
View File
@@ -0,0 +1,67 @@
import { Ajv2020 } from "ajv/dist/2020.js";
import {
arrayItemSchemaError,
unsupportedInputLiteralSchemaError,
} from "./mcpClientSchemaConstraints.js";
/** Advertised schema pair as a client receives it from `tools/list`. */
export interface AdvertisedToolSchemas {
readonly name: string;
readonly inputSchema: Record<string, unknown>;
readonly outputSchema?: Record<string, unknown> | undefined;
}
/**
* Validate advertised schemas as clients receive them: Ajv's declared-dialect
* meta-schema and reference resolution, then the constraints a real client
* enforces. Compiling catches a compact projection that drops a referenced
* definition or leaves a local `$ref` without its target, which a byte-budget
* assertion alone would not.
*/
export function advertisedSchemaErrors(
tools: readonly AdvertisedToolSchemas[],
): string[] {
// Reuse Ajv's meta-schema validators, then clear each root's reference
// namespace, including nested IDs, before validating the next schema.
const ajv = new Ajv2020({
strict: false,
validateFormats: false,
addUsedSchema: false,
// This check compiles schemas to detect invalid references but never runs
// their validators. Generated-code optimization adds substantial work here
// without checking another contract; actual SDK validators keep defaults.
code: { optimize: false },
});
return tools.flatMap((tool) =>
["inputSchema", "outputSchema"].flatMap((kind) => {
const schema =
kind === "inputSchema" ? tool.inputSchema : tool.outputSchema;
if (schema === undefined) return [];
try {
if (!ajv.validateSchema(schema))
return [`${tool.name}.${kind}: ${ajv.errorsText(ajv.errors)}`];
ajv.compile(schema);
const clientError = arrayItemSchemaError(
schema,
`${tool.name}.${kind}`,
);
if (clientError !== undefined) return [clientError];
if (kind === "inputSchema") {
const literalError = unsupportedInputLiteralSchemaError(
schema,
`${tool.name}.${kind}`,
);
if (literalError !== undefined) return [literalError];
}
return [];
} catch (cause: unknown) {
return [
`${tool.name}.${kind}: ${cause instanceof Error ? cause.message : String(cause)}`,
];
} finally {
ajv.removeSchema();
}
}),
);
}