Use compact MCP input advertisements by default while preserving explicit full, canonical validation, output contracts and complete Evidence. Require an explicit selected catalog for runtime identity and honor both profiles in CLI status. Real ripgrep/Zellij SDK tasks, independent ELF oracle, actual client trials, profile parity workflows and CI are recorded in experiments/analysis-efficiency/mcp-input-catalog.md. SDK token estimates improve; actual Codex token savings and general latency/memory gains are not established. Refs #5861 BREAKING CHANGE: MCP input advertisements default to compact. Set REA_MCP_INPUT_SCHEMA_PROFILE=full for complete advertised guidance and nested constraints.
120 lines
4.0 KiB
TypeScript
120 lines
4.0 KiB
TypeScript
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
import { z } from "zod";
|
|
import { expect, it } from "vitest";
|
|
|
|
import {
|
|
TOOL_CONTRACTS,
|
|
toolContract,
|
|
} from "../../../src/contracts/toolContracts.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";
|
|
|
|
const createServer = (omitOutputSchemas: boolean) =>
|
|
new EvidenceMcpServer(
|
|
{ name: "output-schema-fixture", version: "1" },
|
|
{ capabilities: {} },
|
|
undefined,
|
|
createToolResultDelivery(10 * 1024 * 1024),
|
|
undefined,
|
|
omitOutputSchemas,
|
|
);
|
|
|
|
it("omits only output schema advertisements and keeps canonical MCP output validation", async () => {
|
|
const server = createServer(true);
|
|
expect(server.catalogIdentity.digests.combined_sha256).not.toBe(
|
|
FULL_CATALOG_IDENTITY.digests.combined_sha256,
|
|
);
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
const client = new Client({ name: "schema-profile-test", version: "1" });
|
|
const contract = {
|
|
...toolContract("comment"),
|
|
name: "output_schema_fixture",
|
|
inputSchema: z.strictObject({}),
|
|
outputSchema: z.strictObject({ value: z.string() }),
|
|
} satisfies ToolContract;
|
|
server.registerTool(
|
|
contract.name,
|
|
toolRegistrationOptions(contract),
|
|
async () => ({
|
|
content: [{ type: "text" as const, text: "fixture" }],
|
|
structuredContent: { value: "complete" },
|
|
}),
|
|
);
|
|
const invalidContract = {
|
|
...contract,
|
|
name: "invalid_output_schema_fixture",
|
|
} satisfies ToolContract;
|
|
server.registerTool(
|
|
invalidContract.name,
|
|
toolRegistrationOptions(invalidContract),
|
|
async () => ({
|
|
content: [{ type: "text" as const, text: "invalid fixture" }],
|
|
structuredContent: { value: 42 },
|
|
}),
|
|
);
|
|
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
try {
|
|
const tools = (await client.listTools()).tools;
|
|
expect(tools).toHaveLength(2);
|
|
expect(tools.every((tool) => tool.outputSchema === undefined)).toBe(true);
|
|
const success = await client.callTool({
|
|
name: contract.name,
|
|
arguments: {},
|
|
});
|
|
expect(success.isError).not.toBe(true);
|
|
expect(success.structuredContent).toEqual({ value: "complete" });
|
|
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 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);
|
|
const [clientTransport, serverTransport] =
|
|
InMemoryTransport.createLinkedPair();
|
|
const client = new Client({ name: "catalog-size-test", version: "1" });
|
|
for (const contract of TOOL_CONTRACTS)
|
|
server.registerTool(
|
|
contract.name,
|
|
toolRegistrationOptions(contract),
|
|
async () => ({
|
|
content: [{ type: "text" as const, text: "unused" }],
|
|
}),
|
|
);
|
|
await server.connect(serverTransport);
|
|
await client.connect(clientTransport);
|
|
try {
|
|
const listed = await client.listTools();
|
|
return Buffer.byteLength(JSON.stringify(listed), "utf8");
|
|
} finally {
|
|
await client.close();
|
|
await server.close();
|
|
}
|
|
};
|
|
|
|
const fullBytes = await measure(false);
|
|
const compactBytes = await measure(true);
|
|
expect(fullBytes).toBeGreaterThan(1024 * 1024);
|
|
expect(compactBytes).toBeLessThan(1024 * 1024);
|
|
expect(compactBytes).toBeLessThan(fullBytes);
|
|
});
|