refactor(mcp): derive every advertised tool through one owner (#11268)

* refactor(mcp): derive every advertised tool through one owner

The server's registration and tools/list rewrite, catalog identity and the
Copilot verifier each assembled the advertised description, input and
output schema themselves. src/contracts/advertisedTool.ts now owns that
composition; served environment catalogs (MCP listing and card) go through
the same owner's dialect projection, keeping argument descriptions.

* test(mcp): pin each remaining environment dialect violation to its query union
This commit is contained in:
morluto authored and GitHub committed 2026-11-15 23:54:01 +08:00
1 parent d1675822f6
commit 500c1f46b1
15 files changed
+351 -198

No files matched your search

+10 -8
View File
@@ -9,8 +9,8 @@ import {
toolContract,
} from "../dist/contracts/toolContracts.js";
import { advertisedInputJsonSchema } from "../dist/contracts/toolSchemaMetadata.js";
import { presentAdvertisedInputSchema } from "../dist/contracts/compactInputSchema.js";
import { COMPACT_INPUT_SCHEMA_PRESENTATION } from "../dist/config/mcpInputSchemaProfile.js";
import { advertiseTool } from "../dist/contracts/advertisedTool.js";
import { advertisedToolPresentation } from "../dist/config/mcpOutputSchemaProfile.js";
import { isPathWithinRoot } from "../dist/domain/localPath.js";
import { createVerifierCommands } from "./lib/verify-commands.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
@@ -166,12 +166,14 @@ await withVerifierCleanup(async () => {
);
assert.deepEqual(
tool.parameters,
presentAdvertisedInputSchema(
advertisedInputJsonSchema(
toolContract(tool.name.slice(prefix.length)),
),
COMPACT_INPUT_SCHEMA_PRESENTATION,
),
advertiseTool(
{
inputSchema: advertisedInputJsonSchema(
toolContract(tool.name.slice(prefix.length)),
),
},
advertisedToolPresentation("off"),
).inputSchema,
`${tool.name}: Copilot must forward the compact input schema`,
);
schemasChecked++;
@@ -6,6 +6,7 @@ import type {
RecordedExchange,
} from "./environmentSource.js";
import {
advertisedEnvironmentTool,
environmentCard,
environmentToolArguments,
environmentToolDefinitions as prepareDefinitions,
@@ -594,11 +595,9 @@ describe("environmentCard", () => {
"description",
"inputSchema",
]);
expect(entry).toEqual({
name: definitions[index]?.name,
description: definitions[index]?.description,
inputSchema: definitions[index]?.inputSchema,
});
const definition = definitions[index];
if (definition === undefined) throw new Error(`Missing tool ${index}`);
expect(entry).toEqual(advertisedEnvironmentTool(definition));
}
expect(environmentCard("renamed", definitions).name).toBe("renamed");
});
@@ -1,3 +1,4 @@
import { advertiseToolInput } from "../../contracts/advertisedTool.js";
import { jsonPointerTokens } from "../../domain/jsonPointer.js";
import {
ownMember,
@@ -65,6 +66,35 @@ export interface EnvironmentToolDefinition {
readonly tool: EnvironmentManifestTool;
}
/** What MCP clients and the card receive for one served tool. */
export interface AdvertisedEnvironmentTool {
readonly name: string;
readonly description: string;
readonly inputSchema: Record<string, unknown>;
}
/**
* The served tool in the portable advertised dialect. A served catalog bounds
* its own size, so argument descriptions stay; calls are still checked
* against the recorded `inputSchema`.
*/
export const advertisedEnvironmentTool = ({
name,
description,
inputSchema,
}: EnvironmentToolDefinition): AdvertisedEnvironmentTool => {
const advertised = advertiseToolInput(
description,
inputSchema as unknown as Record<string, unknown>,
"annotated",
);
return {
name,
description: advertised.description ?? description,
inputSchema: advertised.inputSchema,
};
};
const environmentToolAnnotations = ({
method,
path_template: path,
@@ -560,10 +590,7 @@ export interface EnvironmentCard {
readonly capabilities: {
/** Null for a replay environment; the intake declaration when served from a state. */
readonly extensions: null | readonly [typeof STATE_INTAKE_EXTENSION];
readonly tools: readonly Pick<
EnvironmentToolDefinition,
"name" | "description" | "inputSchema"
>[];
readonly tools: readonly AdvertisedEnvironmentTool[];
readonly operations: readonly [] | typeof STATE_OPERATIONS;
};
readonly children_environments: null;
@@ -582,11 +609,7 @@ export const environmentCard = (
additionalInterfaces: [{ url: "/mcp", transport: "mcp" }],
capabilities: {
extensions: dataPlane === "state" ? [STATE_INTAKE_EXTENSION] : null,
tools: tools.map(({ name: toolName, description, inputSchema }) => ({
name: toolName,
description,
inputSchema,
})),
tools: tools.map(advertisedEnvironmentTool),
operations: dataPlane === "state" ? STATE_OPERATIONS : [],
},
children_environments: null,
+48 -51
View File
@@ -14,14 +14,13 @@ import {
advertisedInputJsonSchema,
advertisedOutputJsonSchema,
} from "./contracts/toolSchemaMetadata.js";
import type { CompactInputSchemaPresentation } from "./contracts/compactInputSchema.js";
import {
describeAdvertisedTool,
presentAdvertisedInput,
type CompactInputSchemaPresentation,
} from "./contracts/compactInputSchema.js";
import { presentAdvertisedOutputSchema } from "./contracts/compactOutputSchema.js";
advertiseTool,
type AdvertisedToolPresentation,
} from "./contracts/advertisedTool.js";
import {
COMPACT_OUTPUT_SCHEMA_PRESENTATION,
advertisedToolPresentation,
resolveMcpOutputSchemaProfile,
type McpOutputSchemaProfile,
} from "./config/mcpOutputSchemaProfile.js";
@@ -47,42 +46,31 @@ const createToolCatalog = (contracts: readonly ToolContract[]) =>
output_schema: advertisedOutputJsonSchema(contract),
}));
/** The compact input schema and the tool description carrying its guidance. */
const presentedInput = (
description: string,
schema: Record<string, unknown>,
presentation: CompactInputSchemaPresentation,
) => {
const input = presentAdvertisedInput(schema, presentation);
return {
description: describeAdvertisedTool(description, input.guidance),
input_schema: input.schema,
};
};
/** Advertised input for every tool; output stays canonical unless bounded. */
/**
* The identity's view of each tool: what the selected presentation
* advertises. An omitted output advertisement still digests the canonical
* reply contract the server validates.
*/
const presentToolCatalog = (
catalog: ReturnType<typeof createToolCatalog>,
presentation: CompactInputSchemaPresentation,
presentation: AdvertisedToolPresentation,
) =>
catalog.map((tool) => ({
...tool,
...presentedInput(tool.description, tool.input_schema, presentation),
}));
const presentOutputSchemas = (
catalog: ReturnType<typeof presentToolCatalog>,
outputSchemaProfile: McpOutputSchemaProfile,
) =>
outputSchemaProfile === "compact"
? catalog.map((tool) => ({
...tool,
output_schema: presentAdvertisedOutputSchema(
tool.output_schema,
COMPACT_OUTPUT_SCHEMA_PRESENTATION,
),
}))
: catalog;
catalog.map((tool) => {
const advertised = advertiseTool(
{
description: tool.description,
inputSchema: tool.input_schema,
outputSchema: tool.output_schema,
},
presentation,
);
return {
...tool,
description: advertised.description ?? tool.description,
input_schema: advertised.inputSchema ?? tool.input_schema,
output_schema: advertised.outputSchema ?? tool.output_schema,
};
});
const createPromptCatalog = (contracts: readonly PromptContract[]) =>
contracts
@@ -111,13 +99,17 @@ const digest = (value: unknown): string => {
};
const digestCatalogs = (
catalog: ReturnType<typeof presentToolCatalog>,
catalog: ReturnType<typeof createToolCatalog>,
prompts: ReturnType<typeof createPromptCatalog>,
compactPresentation: CompactInputSchemaPresentation,
outputSchemaProfile: McpOutputSchemaProfile,
) => {
const tools = {
output_schema_profile: outputSchemaProfile,
catalog: presentOutputSchemas(catalog, outputSchemaProfile),
catalog: presentToolCatalog(
catalog,
advertisedToolPresentation(outputSchemaProfile, compactPresentation),
),
};
return {
tools_sha256: digest(tools),
@@ -144,11 +136,9 @@ export const createCatalogDigests = (
),
) =>
digestCatalogs(
presentToolCatalog(
createToolCatalog(sortByName(contracts)),
compactPresentation,
),
createToolCatalog(sortByName(contracts)),
createPromptCatalog(promptContracts),
compactPresentation,
outputSchemaProfile,
);
@@ -162,14 +152,21 @@ export const createCatalogDigestProfiles = (
contracts: readonly ToolContract[] = TOOL_CONTRACTS,
promptContracts: readonly PromptContract[] = PROMPT_CONTRACTS,
): CatalogDigestProfiles => {
const catalog = presentToolCatalog(
createToolCatalog(sortByName(contracts)),
COMPACT_INPUT_SCHEMA_PRESENTATION,
);
const catalog = createToolCatalog(sortByName(contracts));
const prompts = createPromptCatalog(promptContracts);
return {
off: digestCatalogs(catalog, prompts, "off"),
compact: digestCatalogs(catalog, prompts, "compact"),
off: digestCatalogs(
catalog,
prompts,
COMPACT_INPUT_SCHEMA_PRESENTATION,
"off",
),
compact: digestCatalogs(
catalog,
prompts,
COMPACT_INPUT_SCHEMA_PRESENTATION,
"compact",
),
};
};
+15
View File
@@ -1,7 +1,10 @@
import { z } from "zod";
import type { AdvertisedToolPresentation } from "../contracts/advertisedTool.js";
import type { CompactInputSchemaPresentation } from "../contracts/compactInputSchema.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { err, ok, type Result } from "../domain/result.js";
import { COMPACT_INPUT_SCHEMA_PRESENTATION } from "./mcpInputSchemaProfile.js";
/**
* Per-schema budget for compact output advertisements. Output contracts are
@@ -42,6 +45,18 @@ export const COMPACT_OUTPUT_SCHEMA_PRESENTATION: {
readonly budgetBytes: number;
} = Object.freeze({ budgetBytes: COMPACT_OUTPUT_SCHEMA_BUDGET_BYTES });
/** How REA's MCP catalog advertises its tools for a selected output profile. */
export const advertisedToolPresentation = (
outputSchemaProfile: McpOutputSchemaProfile,
input: CompactInputSchemaPresentation = COMPACT_INPUT_SCHEMA_PRESENTATION,
): AdvertisedToolPresentation => ({
input: { budgetBytes: input.budgetBytes },
output:
outputSchemaProfile === "compact"
? COMPACT_OUTPUT_SCHEMA_PRESENTATION
: undefined,
});
/** Parse the output schema advertisement selection at server composition. */
export const parseMcpOutputSchemaProfile = (
value: string | undefined,
+136
View File
@@ -0,0 +1,136 @@
import { projectToAdvertisedDialect } from "./advertisedDialectProjection.js";
import {
presentAdvertisedInput,
type CompactInputSchemaPresentation,
} from "./compactInputSchema.js";
import {
presentAdvertisedOutputSchema,
type CompactOutputSchemaPresentation,
} from "./compactOutputSchema.js";
// The one owner of what a client receives for a tool: its description, input
// schema and output schema. The server's registration and `tools/list`
// listing, catalog identities, served environment catalogs and client
// verifiers all derive the advertised tool here from the canonical JSON
// Schemas; canonical server-side validation is unchanged.
/** How the canonical schemas of one catalog are advertised. */
export interface AdvertisedToolPresentation {
/**
* The per-tool input budget of the compact presentation, or `annotated`:
* project into the portable dialect and keep nested annotations, for a
* catalog that bounds its own size.
*/
readonly input: CompactInputSchemaPresentation | "annotated";
/** Output schemas are bounded by this presentation, or omitted without one. */
readonly output: CompactOutputSchemaPresentation | undefined;
}
/** The client-facing fields of one tool, canonical or advertised. */
export interface AdvertisedToolSchemas {
readonly description?: string | undefined;
readonly inputSchema?: Record<string, unknown> | undefined;
readonly outputSchema?: Record<string, unknown> | undefined;
}
/** The tool description followed by the input guidance its schema omits. */
const describe = (
description: string | undefined,
guidance: readonly string[],
): string | undefined => {
if (guidance.length === 0) return description;
const input = guidance.join(" ");
return description === undefined || description === ""
? input
: `${description}\n\n${input}`;
};
// The SDK and the contract metadata memoize each canonical schema object, so
// repeated listings and both output profiles reuse one projection.
const annotated = new WeakMap<
object,
ReturnType<typeof projectToAdvertisedDialect>
>();
const presentInput = (
schema: Record<string, unknown>,
presentation: AdvertisedToolPresentation["input"],
): {
readonly schema: Record<string, unknown>;
readonly guidance: readonly string[];
} => {
if (presentation !== "annotated")
return presentAdvertisedInput(schema, presentation);
let projected = annotated.get(schema);
if (projected === undefined)
annotated.set(schema, (projected = projectToAdvertisedDialect(schema)));
return projected;
};
/**
* The advertised description and input schema. Reading the description
* projects the schema; a tool without an input schema keeps its description.
*/
export const advertiseToolInput = (
description: string | undefined,
inputSchema: Record<string, unknown>,
presentation: AdvertisedToolPresentation["input"],
): {
readonly description: string | undefined;
readonly inputSchema: Record<string, unknown>;
} => {
const input = presentInput(inputSchema, presentation);
return {
description: describe(description, input.guidance),
inputSchema: input.schema,
};
};
const compactedOutput = new WeakMap<
object,
Map<number, Record<string, unknown>>
>();
/** The advertised output schema, or undefined when the presentation omits it. */
export const advertiseToolOutput = (
outputSchema: Record<string, unknown>,
presentation: AdvertisedToolPresentation["output"],
): Record<string, unknown> | undefined => {
if (presentation === undefined) return undefined;
let byBudget = compactedOutput.get(outputSchema);
if (byBudget === undefined)
compactedOutput.set(outputSchema, (byBudget = new Map()));
let advertised = byBudget.get(presentation.budgetBytes);
if (advertised === undefined)
byBudget.set(
presentation.budgetBytes,
(advertised = presentAdvertisedOutputSchema(outputSchema, presentation)),
);
return advertised;
};
/** Everything a client receives for one tool, from its canonical schemas. */
export const advertiseTool = (
canonical: AdvertisedToolSchemas,
presentation: AdvertisedToolPresentation,
): AdvertisedToolSchemas => {
const input =
canonical.inputSchema === undefined
? { description: canonical.description }
: advertiseToolInput(
canonical.description,
canonical.inputSchema,
presentation.input,
);
const outputSchema =
canonical.outputSchema === undefined
? undefined
: advertiseToolOutput(canonical.outputSchema, presentation.output);
return {
...(input.description === undefined
? {}
: { description: input.description }),
...("inputSchema" in input ? { inputSchema: input.inputSchema } : {}),
...(outputSchema === undefined ? {} : { outputSchema }),
};
};
+1 -28
View File
@@ -109,7 +109,7 @@ export const compactAdvertisedInputSchema = (
// read one projection of the same memoized canonical schema object.
const compacted = new WeakMap<object, Map<number, CompactedInputSchema>>();
/** The advertised input for the selected presentation, with its guidance. */
/** The compact advertised input with its guidance; `advertisedTool.ts` composes the tool. */
export const presentAdvertisedInput = (
schema: Record<string, unknown>,
compactPresentation: CompactInputSchemaPresentation,
@@ -125,30 +125,3 @@ export const presentAdvertisedInput = (
);
return presented;
};
/** Return the JSON schema clients receive for the selected input presentation. */
export const presentAdvertisedInputSchema = (
schema: Record<string, unknown>,
compactPresentation: CompactInputSchemaPresentation,
): Record<string, unknown> =>
presentAdvertisedInput(schema, compactPresentation).schema;
/** The tool description followed by the input guidance its schema omits. */
export function describeAdvertisedTool(
description: string,
guidance: readonly string[],
): string;
export function describeAdvertisedTool(
description: string | undefined,
guidance: readonly string[],
): string | undefined;
export function describeAdvertisedTool(
description: string | undefined,
guidance: readonly string[],
): string | undefined {
if (guidance.length === 0) return description;
const input = guidance.join(" ");
return description === undefined || description === ""
? input
: `${description}\n\n${input}`;
}
+2 -3
View File
@@ -226,13 +226,12 @@ export class DeferredEvidenceMcpServer extends EvidenceMcpServerCore {
*/
protected override describeTool(
name: string,
description: string | undefined,
guidance: () => readonly string[],
advertised: () => string | undefined,
): { description?: string } {
const recorded = this.tools.get(name)?.registered.description;
if (this.materializing && recorded !== undefined)
return { description: recorded };
return super.describeTool(name, description, guidance);
return super.describeTool(name, advertised);
}
protected override bindTool(
+33 -51
View File
@@ -29,14 +29,14 @@ import {
import { analysisErrorProjectionSchema } from "../contracts/errorSchemas.js";
import { transportClosedByOwner } from "./transportOwnerClose.js";
import type { CompactInputSchemaPresentation } from "../contracts/compactInputSchema.js";
import {
describeAdvertisedTool,
presentAdvertisedInput,
type CompactInputSchemaPresentation,
} from "../contracts/compactInputSchema.js";
import { presentAdvertisedOutputSchema } from "../contracts/compactOutputSchema.js";
advertiseToolInput,
advertiseToolOutput,
type AdvertisedToolPresentation,
} from "../contracts/advertisedTool.js";
import {
COMPACT_OUTPUT_SCHEMA_PRESENTATION,
advertisedToolPresentation,
resolveMcpOutputSchemaProfile,
type McpOutputSchemaProfile,
} from "../config/mcpOutputSchemaProfile.js";
@@ -235,15 +235,12 @@ const replaceToolResultPayload = (
/**
* Rewrite only client-facing output schemas; SDK registrations stay canonical.
* `compact` bounds each advertised schema, while `off` omits it entirely.
* The SDK memoises each tool's converted schema, so the caller passes a cache
* keyed by that object; repeated listings reuse the same projection.
* The presentation bounds each advertised schema or omits it entirely.
*/
const presentOutputSchemasForToolsList = (
message: JSONRPCMessage,
requestId: RequestId,
profile: McpOutputSchemaProfile,
compactedSchemas: WeakMap<Record<string, unknown>, Record<string, unknown>>,
presentation: AdvertisedToolPresentation["output"],
): JSONRPCMessage => {
if (
!isJSONRPCResultResponse(message) ||
@@ -257,21 +254,12 @@ const presentOutputSchemasForToolsList = (
result: {
...message.result,
tools: message.result.tools.map((tool: unknown) => {
if (!isRecord(tool)) return tool;
if (profile === "off") {
const { outputSchema: _outputSchema, ...advertisedTool } = tool;
return advertisedTool;
}
if (!isRecord(tool.outputSchema)) return tool;
let outputSchema = compactedSchemas.get(tool.outputSchema);
if (outputSchema === undefined) {
outputSchema = presentAdvertisedOutputSchema(
tool.outputSchema,
COMPACT_OUTPUT_SCHEMA_PRESENTATION,
);
compactedSchemas.set(tool.outputSchema, outputSchema);
}
return { ...tool, outputSchema };
if (!isRecord(tool) || !isRecord(tool.outputSchema)) return tool;
const { outputSchema: canonical, ...advertisedTool } = tool;
const outputSchema = advertiseToolOutput(canonical, presentation);
return outputSchema === undefined
? advertisedTool
: { ...advertisedTool, outputSchema };
}),
},
} as JSONRPCMessage;
@@ -394,7 +382,7 @@ export class EvidenceMcpServerCore extends RequestTrackingMcpServer {
): RegisteredPrompt {
return super.registerPrompt(name, config, callback);
}
private readonly compactPresentation: CompactInputSchemaPresentation;
private readonly presentation: AdvertisedToolPresentation;
private readonly maximumInputElements: number;
private readonly evidenceReferenceAlternatives = new Set<string>();
@@ -408,7 +396,7 @@ export class EvidenceMcpServerCore extends RequestTrackingMcpServer {
private recordEvidence: EvidenceWriter["recordEvidence"] | undefined,
public delivery: ToolResultDelivery,
compactPresentation: CompactInputSchemaPresentation = COMPACT_INPUT_SCHEMA_PRESENTATION,
private readonly outputSchemaProfile: McpOutputSchemaProfile = resolveMcpOutputSchemaProfile(
outputSchemaProfile: McpOutputSchemaProfile = resolveMcpOutputSchemaProfile(
undefined,
),
identity?: ReturnType<typeof createCatalogIdentity>,
@@ -420,9 +408,10 @@ export class EvidenceMcpServerCore extends RequestTrackingMcpServer {
options.maxToolInputElements ?? DEFAULT_MCP_MAX_INPUT_ELEMENTS;
super(info, { ...options, maxToolInputElements: maximumInputElements });
this.maximumInputElements = maximumInputElements;
this.compactPresentation = {
budgetBytes: compactPresentation.budgetBytes,
};
this.presentation = advertisedToolPresentation(
outputSchemaProfile,
compactPresentation,
);
this.identity = identity;
}
@@ -521,35 +510,35 @@ export class EvidenceMcpServerCore extends RequestTrackingMcpServer {
config: { readonly description?: string; readonly inputSchema?: Schema },
): { description?: string; inputSchema?: Schema } {
const { description, inputSchema } = config;
const presentation = this.compactPresentation;
const { input } = this.presentation;
if (inputSchema === undefined || !hasAdvertisedInputJsonSchema(inputSchema))
return config;
const present = (projected: Record<string, unknown>) =>
presentAdvertisedInput(projected, presentation);
return {
...this.describeTool(name, description, () => {
...this.describeTool(name, () => {
const projected = (inputSchema as StandardSchemaWithJSON)[
"~standard"
].jsonSchema.input({ target: "draft-2020-12" });
return isRecord(projected) ? present(projected).guidance : [];
return isRecord(projected)
? advertiseToolInput(description, projected, input).description
: description;
}),
inputSchema: transformAdvertisedInputJsonSchema(
inputSchema,
(projected) => present(projected).schema,
(projected) =>
advertiseToolInput(description, projected, input).inputSchema,
),
};
}
/**
* The listed description: guidance the compact input schema omits follows
* the registered description. Reading the guidance projects the schema.
* the registered description. Reading it projects the schema.
*/
protected describeTool(
_name: string,
description: string | undefined,
guidance: () => readonly string[],
advertised: () => string | undefined,
): { description?: string } {
const described = describeAdvertisedTool(description, guidance());
const described = advertised();
return described === undefined ? {} : { description: described };
}
@@ -591,7 +580,7 @@ export class EvidenceMcpServerCore extends RequestTrackingMcpServer {
() => this.recordEvidence,
() => this.delivery,
() => this.withAdmittedAnalysis,
this.outputSchemaProfile,
this.presentation.output,
this.maximumInputElements,
this.evidenceReferenceAlternatives,
),
@@ -618,12 +607,6 @@ class ErrorEvidenceTransport implements Transport {
return this.currentDelivery();
}
/** Bounded projections already computed for this server's budget. */
private readonly compactedOutputSchemas = new WeakMap<
Record<string, unknown>,
Record<string, unknown>
>();
constructor(
private readonly transport: Transport,
private readonly currentEvidenceWriter: () =>
@@ -631,7 +614,7 @@ class ErrorEvidenceTransport implements Transport {
| undefined,
private readonly currentDelivery: () => ToolResultDelivery,
private readonly currentAdmission: () => WithAdmittedAnalysis | undefined,
private readonly outputSchemaProfile: McpOutputSchemaProfile,
private readonly outputPresentation: AdvertisedToolPresentation["output"],
private readonly maximumInputElements: number,
private readonly evidenceReferenceAlternatives: ReadonlySet<string>,
) {
@@ -788,8 +771,7 @@ class ErrorEvidenceTransport implements Transport {
? presentOutputSchemasForToolsList(
recovered,
id,
this.outputSchemaProfile,
this.compactedOutputSchemas,
this.outputPresentation,
)
: recovered;
await this.publish(advertised, tool, options, request);
@@ -9,6 +9,7 @@ import { afterEach, describe, expect, it } from "vitest";
import { environmentServeSource } from "../../../tests/fixtures/environmentServe.js";
import {
advertisedEnvironmentTool,
environmentCard,
environmentToolDefinitions as prepareDefinitions,
REPLAY_CATALOG_INSTRUCTIONS,
@@ -147,11 +148,7 @@ describe("environment routes", () => {
JSON.stringify(
echoCardShape(
"grafana-example",
definitions.map(({ name, description, inputSchema }) => ({
name,
description,
inputSchema,
})),
definitions.map(advertisedEnvironmentTool),
),
),
);
+11 -3
View File
@@ -22,7 +22,9 @@ import {
} from "../../application/environment/environmentToolAnswer.js";
import { EnvironmentStateStore } from "../../application/environment/environmentStateStore.js";
import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import { transformAdvertisedInputJsonSchema } from "../../contracts/toolSchemaMetadata.js";
import {
advertisedEnvironmentTool,
environmentCard,
REPLAY_CATALOG_INSTRUCTIONS,
type EnvironmentCard,
@@ -114,12 +116,17 @@ export const createEnvironmentMcpServer = (
instructions: `${UNTRUSTED_CONTENT_INSTRUCTIONS} ${REPLAY_CATALOG_INSTRUCTIONS}`,
},
);
for (const definition of tools)
for (const definition of tools) {
const advertised = advertisedEnvironmentTool(definition);
server.registerTool(
definition.name,
{
description: definition.description,
inputSchema: fromJsonSchema<ReplayArguments>(definition.inputSchema),
description: advertised.description,
// Calls are validated against the recorded schema; clients list the advertised one.
inputSchema: transformAdvertisedInputJsonSchema(
fromJsonSchema<ReplayArguments>(definition.inputSchema),
() => advertised.inputSchema,
),
annotations: definition.annotations,
},
(args): CallToolResult => {
@@ -141,6 +148,7 @@ export const createEnvironmentMcpServer = (
};
},
);
}
return server;
};
@@ -9,11 +9,8 @@ import {
} from "../../../src/contracts/toolContracts.js";
import { PROMPT_CONTRACTS } from "../../../src/contracts/promptContracts.js";
import { advertisedInputJsonSchema } from "../../../src/contracts/toolSchemaMetadata.js";
import {
describeAdvertisedTool,
presentAdvertisedInput,
presentAdvertisedInputSchema,
} from "../../../src/contracts/compactInputSchema.js";
import { advertiseTool } from "../../../src/contracts/advertisedTool.js";
import { advertisedToolPresentation } from "../../../src/config/mcpOutputSchemaProfile.js";
import { createCatalogDigests } from "../../../src/catalogIdentity.js";
import {
COMPACT_INPUT_SCHEMA_BUDGET_BYTES,
@@ -90,10 +87,10 @@ describe("MCP compact input schema profile", () => {
);
for (const contract of TOOL_CONTRACTS)
expect(compactTools.get(contract.name)?.inputSchema).toEqual(
presentAdvertisedInputSchema(
advertisedInputJsonSchema(contract),
compactPresentation,
),
advertiseTool(
{ inputSchema: advertisedInputJsonSchema(contract) },
advertisedToolPresentation("off", compactPresentation),
).inputSchema,
);
const compactIdentity = createCatalogDigests();
@@ -124,12 +121,13 @@ it("extends tool descriptions only with input guidance", async () => {
const contract = TOOL_CONTRACTS.find(({ name }) => name === tool.name);
if (contract === undefined) throw new Error(`Missing ${tool.name}`);
expect(tool.description, tool.name).toBe(
describeAdvertisedTool(
toolRegistrationOptions(contract).description,
presentAdvertisedInput(advertisedInputJsonSchema(contract), {
budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES,
}).guidance,
),
advertiseTool(
{
description: toolRegistrationOptions(contract).description,
inputSchema: advertisedInputJsonSchema(contract),
},
advertisedToolPresentation("off"),
).description,
);
expect(tool.inputSchema.description, tool.name).toBeUndefined();
}
@@ -1,5 +1,5 @@
import { presentAdvertisedInputSchema } from "../../../src/contracts/compactInputSchema.js";
import { COMPACT_INPUT_SCHEMA_PRESENTATION } from "../../../src/config/mcpInputSchemaProfile.js";
import { advertiseTool } from "../../../src/contracts/advertisedTool.js";
import { advertisedToolPresentation } from "../../../src/config/mcpOutputSchemaProfile.js";
import { randomUUID } from "node:crypto";
import { fileURLToPath } from "node:url";
import { isCallToolResult } from "@modelcontextprotocol/server";
@@ -99,10 +99,10 @@ it("preserves advertised schemas and handler meaning when presentation order cha
);
if (canonical === undefined) throw new Error(`Missing ${advertised.name}`);
expect(advertised.inputSchema).toEqual(
presentAdvertisedInputSchema(
canonical.inputSchema,
COMPACT_INPUT_SCHEMA_PRESENTATION,
),
advertiseTool(
{ inputSchema: canonical.inputSchema },
advertisedToolPresentation("off"),
).inputSchema,
);
// The default output profile omits output schemas.
expect(advertised.outputSchema).toBeUndefined();
+29 -3
View File
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
import { timedCaptureEvidence } from "../../fixtures/analysisView.js";
import { INITIAL_TRASH_ROWS } from "../../fixtures/environmentStateLaterReads.js";
import { jsonObjectSchema } from "../../../src/domain/jsonValue.js";
import { advertisedInputDialectViolations } from "../../../src/contracts/advertisedDialect.js";
import {
Client,
@@ -23,9 +24,9 @@ import {
} from "../../support/process/processFixture.js";
const CARD_PATH = "/.well-known/agent-env.json";
/** SHA-256 of the 14,519-byte card served for the fixture named "renamed", its descriptions naming each tool's recorded responses; pure replay must serve these bytes. */
/** SHA-256 of the card served for the fixture named "renamed", its descriptions naming each tool's recorded responses; pure replay must serve these bytes. */
const REPLAY_CARD_SHA256 =
"995c280f0798f77e76f206c068eb6c2c1f8c4210a03ac1c01fba8cc07b5d7f09";
"825703b02f48d614b3c4690445b0b6b00333d975406dc7e8dd335836bceb7d59";
/** Serve the fixture pair as pure replay through the built CLI. */
const serveReplayOnly = async (
@@ -175,7 +176,9 @@ processTest(
tools.find(({ name }) => name === tool.name)?.inputSchema.properties
?.tag,
).toMatchObject({
oneOf: [
// The recorded `oneOf` is advertised as the portable `anyOf`; the
// branches are disjoint, so both accept the same values.
anyOf: [
{ type: "string" },
{ type: "number" },
{ type: "boolean" },
@@ -212,6 +215,29 @@ describe("rea environment-serve --replay-only", () => {
expect(tools.map(({ name }) => name).sort()).toEqual(
source.manifest.tools.map(({ name }) => name).sort(),
);
// A served catalog reaches models through the same clients as REA's.
// The dialect has no portable form for a query parameter's
// string, number, boolean or string-list union yet, so only that
// union's untyped node remains outside it.
for (const violation of tools.flatMap(
advertisedInputDialectViolations,
)) {
expect(violation).toMatchObject({ rule: "type", detail: "untyped" });
const property = /^\$\.properties\.([^.]+)$/u.exec(violation.path);
const name = property?.[1] ?? "";
const tool = tools.find(
({ name: listed }) => listed === violation.tool,
);
expect(tool?.inputSchema.properties?.[name], violation.path).toEqual({
description: expect.stringMatching(/^Query parameter /u),
anyOf: [
{ type: "string" },
{ type: "number" },
{ type: "boolean" },
{ type: "array", items: { type: "string" } },
],
});
}
const users = tools.find(({ name }) => name === "get_users_by_id");
expect(users?.description).toContain(
'; returns a JSON object with fields "id", "uid", "login", "email"; recorded statuses 200;',
+9 -11
View File
@@ -15,11 +15,8 @@ import type {
} from "../../../src/application/AnalysisProvider.js";
import { observed as ok } from "../../fixtures/analysisExecution.js";
import { createServer } from "../../../src/server/createServer.js";
import { COMPACT_INPUT_SCHEMA_BUDGET_BYTES } from "../../../src/config/mcpInputSchemaProfile.js";
import {
describeAdvertisedTool,
presentAdvertisedInput,
} from "../../../src/contracts/compactInputSchema.js";
import { advertisedToolPresentation } from "../../../src/config/mcpOutputSchemaProfile.js";
import { advertiseTool } from "../../../src/contracts/advertisedTool.js";
import { advertisedInputJsonSchema } from "../../../src/contracts/toolSchemaMetadata.js";
import {
TOOL_CONTRACTS,
@@ -177,12 +174,13 @@ it("advertises the complete currently available inventory with a session", async
// to the end of the contract description.
if (contract === undefined) continue;
expect(tool.description, tool.name).toBe(
describeAdvertisedTool(
contract.description,
presentAdvertisedInput(advertisedInputJsonSchema(contract), {
budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES,
}).guidance,
),
advertiseTool(
{
description: contract.description,
inputSchema: advertisedInputJsonSchema(contract),
},
advertisedToolPresentation("off"),
).description,
);
expect(tool.annotations, tool.name).toEqual(contract?.annotations);
expect(tool.inputSchema, tool.name).toBeDefined();