mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 11:13:21 +00:00
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:
1 parent
85da0bd4ee
commit
ef1b92bf1e
17 files changed
+986
-389
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>;
|
||||
@@ -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",
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user