test(mcp): check advertised schemas against a portable dialect in every profile (#10567)

* test(mcp): check advertised schemas against a portable dialect in every profile

* fix(mcp): validate advertised dialect by selected profile

* fix(mcp): refuse zero-padded repeat bounds and record host-specific path patterns
This commit is contained in:
morluto authored and GitHub committed 2026-11-14 15:20:31 -05:00
1 parent 85da0bd4ee
commit ef1b92bf1e
17 files changed
+986 -389

No files matched your search

+2
View File
@@ -265,6 +265,8 @@ const PATH_SCOPES = {
"src/contracts/sessionPathInputs.test.ts",
"tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts",
"tests/boundary/mcp/rawBinaryMcp.test.ts",
"src/contracts/advertisedDialect*.ts",
"tests/boundary/mcp/advertisedDialect.test.ts",
"tests/boundary/filesystem/safeOutputTree.test.ts",
"tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts",
"tests/boundary/providers/native/artifactProvider.extractionCompleteness.test.ts",
+5 -1
View File
@@ -79,7 +79,11 @@ export async function verifyInstalledCanaryPackage(
return withVerifierCleanup(async () => {
try {
await owner.open(client, transport);
const toolNames = await verifyCompleteToolCatalog(client);
const toolNames = await verifyCompleteToolCatalog(
client,
undefined,
environment,
);
return {
mcpTools: toolNames.length,
canonicalMcpTools: FULL_CATALOG_IDENTITY.counts.mcp_tools,
+1
View File
@@ -2,5 +2,6 @@ import type { Client } from "@modelcontextprotocol/client";
export function verifyCompleteToolCatalog(
client: Pick<Client, "listTools" | "callTool">,
options?: Parameters<Client["listTools"]>[1],
environment?: Readonly<NodeJS.ProcessEnv>,
): Promise<string[]>;
export function pathExists(path: string): Promise<boolean>;
+45 -6
View File
@@ -5,12 +5,26 @@ import { Ajv2020 } from "ajv/dist/2020.js";
* Verify canonical names, converted JSON Schemas, and session catalog metadata.
* Only this operation needs built contracts; path checks remain source-only.
*/
export const verifyCompleteToolCatalog = async (client, options) => {
const [{ requireMcpLifecycleResult, sortVerifierNames }, { TOOL_CONTRACTS }] =
await Promise.all([
import("./mcp-verifier-results.mjs"),
import("../../dist/contracts/toolContracts.js"),
]);
export const verifyCompleteToolCatalog = async (
client,
options,
environment = {},
) => {
const [
{ requireMcpLifecycleResult, sortVerifierNames },
{ TOOL_CONTRACTS },
dialect,
{ ADVERTISED_INPUT_DIALECT_BASELINE, ADVERTISED_OUTPUT_DIALECT_BASELINE },
{ mcpInputSchemaProfileSchema },
{ mcpOutputSchemaProfileSchema },
] = await Promise.all([
import("./mcp-verifier-results.mjs"),
import("../../dist/contracts/toolContracts.js"),
import("../../dist/contracts/advertisedDialect.js"),
import("../../dist/contracts/advertisedDialectBaseline.js"),
import("../../dist/config/mcpInputSchemaProfile.js"),
import("../../dist/config/mcpOutputSchemaProfile.js"),
]);
const listed = await client.listTools(undefined, options);
const canonical = sortVerifierNames(TOOL_CONTRACTS.map(({ name }) => name));
const observed = sortVerifierNames(listed.tools.map(({ name }) => name));
@@ -36,6 +50,31 @@ export const verifyCompleteToolCatalog = async (client, options) => {
);
}
}
// Compare the advertisement with the profiles actually passed to the server.
const inputProfile = mcpInputSchemaProfileSchema.parse(
environment.REA_MCP_INPUT_SCHEMA_PROFILE ?? "compact",
);
const outputProfile = mcpOutputSchemaProfileSchema.parse(
environment.REA_MCP_OUTPUT_SCHEMA_PROFILE ?? "full",
);
const regressions = [
...dialect.advertisedDialectRegressions(
dialect.countAdvertisedDialectViolations(
listed.tools.flatMap(dialect.advertisedInputDialectViolations),
),
ADVERTISED_INPUT_DIALECT_BASELINE[inputProfile],
),
...dialect.advertisedDialectRegressions(
dialect.countAdvertisedDialectViolations(
listed.tools.flatMap(dialect.advertisedOutputPatternViolations),
),
ADVERTISED_OUTPUT_DIALECT_BASELINE[outputProfile],
),
];
if (regressions.length > 0)
throw new Error(
`packaged MCP advertises constructs outside the portable schema dialect: ${regressions.join("; ")}`,
);
const sessionDefinition = listed.tools.find(
({ name }) => name === "binary_session",
);
+3 -3
View File
@@ -17,8 +17,8 @@ import * as prompts from "./verify-package-prompts.mjs";
import { verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
const verifyMcpToolsAndPrompts = async (client, mcpOptions) => {
await verifyCompleteToolCatalog(client, mcpOptions);
const verifyMcpToolsAndPrompts = async (client, mcpOptions, environment) => {
await verifyCompleteToolCatalog(client, mcpOptions, environment);
await prompts.verifyPromptCatalog(client, mcpOptions, prompts.names);
await prompts.verifyPromptCompletion(client, mcpOptions, []);
};
@@ -283,7 +283,7 @@ export async function verifyPackageMcp({
try {
await client.connect(transport);
const mcpOptions = { timeout: 15_000 };
await verifyMcpToolsAndPrompts(client, mcpOptions);
await verifyMcpToolsAndPrompts(client, mcpOptions, environment);
await verifyMcpTargetFree(client, mcpOptions);
await verifyMcpUnknownProvider(client, mcpOptions);
await verifyMcpBinaryLifecycle(client, mcpOptions);
+7 -3
View File
@@ -97,9 +97,13 @@ try {
advertised.filter((tool) => tool.name === "inspect_recorded_crash").length,
1,
);
const catalog = await verifyCompleteToolCatalog(basic.client, {
timeout: 90_000,
});
const catalog = await verifyCompleteToolCatalog(
basic.client,
{
timeout: 90_000,
},
environment,
);
for (const name of [
"inspect_binary_layout",
"inspect_recorded_crash",
+3 -1
View File
@@ -109,7 +109,9 @@ try {
let toolCount = 0;
await withVerifierCleanup(async () => {
await client.connect(server);
toolCount = (await verifyCompleteToolCatalog(client)).length;
toolCount = (
await verifyCompleteToolCatalog(client, undefined, environment)
).length;
}, [
async () => {
try {