Skip to content

Commit 85d27fb

Browse files
committed
fix(package): verify the complete MCP catalog
1 parent 20dba84 commit 85d27fb

5 files changed

Lines changed: 41 additions & 27 deletions

File tree

scripts/lib/verify-package-core.mjs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ export const runWithStatus = async (command, args, env) => {
2828
/** Parse JSON from a command output string. */
2929
export const json = (text) => JSON.parse(text);
3030

31-
/** Verify that the advertised MCP catalog matches the session's available tools. */
32-
export const verifyAvailableToolCatalog = async (client, options) => {
31+
/** Verify that the complete advertised MCP catalog matches session metadata. */
32+
export const verifyCompleteToolCatalog = async (client, options) => {
3333
const listed = await client.listTools(undefined, options);
3434
const status = await client.callTool(
3535
{ name: "binary_session", arguments: { detail: "full" } },
@@ -38,14 +38,11 @@ export const verifyAvailableToolCatalog = async (client, options) => {
3838
const availability = status.structuredContent?.result?.tool_availability;
3939
if (!Array.isArray(availability))
4040
throw new Error("packaged MCP omitted tool availability");
41-
const expected = availability
42-
.filter(({ available }) => available === true)
43-
.map(({ name }) => name)
44-
.sort();
41+
const expected = availability.map(({ name }) => name).sort();
4542
const observed = listed.tools.map(({ name }) => name).sort();
4643
if (JSON.stringify(observed) !== JSON.stringify(expected))
4744
throw new Error(
48-
"packaged MCP tool inventory diverged from session availability",
45+
"packaged MCP tool inventory diverged from session metadata",
4946
);
5047
return observed;
5148
};

scripts/verify-package-mcp.mjs

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,12 @@ import { Client } from "@modelcontextprotocol/client";
77
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
88
import { MCP_STARTUP_POLICY } from "../dist/mcpStartupPolicy.js";
99
import * as prompts from "./verify-package-prompts.mjs";
10-
import {
11-
json,
12-
verifyAvailableToolCatalog,
13-
} from "./lib/verify-package-core.mjs";
10+
import { json, verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
1411

1512
const execute = promisify(execFile);
1613

1714
const verifyMcpToolsAndPrompts = async (client, mcpOptions) => {
18-
await verifyAvailableToolCatalog(client, mcpOptions);
15+
await verifyCompleteToolCatalog(client, mcpOptions);
1916
await prompts.verifyPromptCatalog(client, mcpOptions, prompts.names);
2017
await prompts.verifyPromptCompletion(client, mcpOptions, false);
2118
};
@@ -43,11 +40,21 @@ const verifyMcpReplay = async (client, mcpOptions, investigationReplay) => {
4340
};
4441

4542
const verifyMcpTargetFree = async (client, mcpOptions) => {
46-
const names = (await client.listTools(undefined, mcpOptions)).tools.map(
47-
({ name }) => name,
43+
const status = await client.callTool(
44+
{ name: "binary_session", arguments: { detail: "full" } },
45+
mcpOptions,
4846
);
49-
if (names.includes("current_document"))
50-
throw new Error("packaged target-free MCP advertised a target-bound tool");
47+
const currentDocument =
48+
status.structuredContent?.result?.tool_availability?.find(
49+
({ name }) => name === "current_document",
50+
);
51+
if (
52+
currentDocument?.available !== false ||
53+
currentDocument.reason !== "target_required"
54+
)
55+
throw new Error(
56+
"packaged target-free MCP omitted target-bound availability metadata",
57+
);
5158
};
5259

5360
const verifyMcpUnknownProvider = async (client, mcpOptions) => {
@@ -96,11 +103,12 @@ const verifyMcpOpenAndBind = async (client, mcpOptions) => {
96103
};
97104

98105
const verifyMcpLinuxToolAvailability = async (client, mcpOptions) => {
99-
const names = (await client.listTools(undefined, mcpOptions)).tools.map(
100-
({ name }) => name,
106+
const current = await client.callTool(
107+
{ name: "current_document", arguments: {} },
108+
mcpOptions,
101109
);
102-
if (names.includes("current_document"))
103-
throw new Error("packaged Linux MCP advertised unavailable Hopper tools");
110+
if (current.isError !== true)
111+
throw new Error("packaged Linux MCP executed an unavailable Hopper tool");
104112
};
105113

106114
const verifyMcpNonLinuxCurrentDocument = async (

scripts/verify-windows-package.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { promisify } from "node:util";
99
import { Client } from "@modelcontextprotocol/client";
1010
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
1111

12-
import { verifyAvailableToolCatalog } from "./lib/verify-package-core.mjs";
12+
import { verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
1313
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
1414

1515
const exec = promisify(execFile);
@@ -84,7 +84,7 @@ try {
8484
let toolCount = 0;
8585
try {
8686
await client.connect(server);
87-
toolCount = (await verifyAvailableToolCatalog(client)).length;
87+
toolCount = (await verifyCompleteToolCatalog(client)).length;
8888
} finally {
8989
await client.close();
9090
}

tests/boundary/mcp/mainReload.test.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
type RuntimeDependencies = NonNullable<Parameters<typeof run>[0]>;
2020

2121
const directories: string[] = [];
22+
const RELOAD_ASSERTION_TIMEOUT_MS = 5_000;
2223

2324
afterEach(async () => {
2425
await Promise.all(
@@ -64,7 +65,9 @@ describe("runtime permission reload", () => {
6465

6566
releaseValidRead.resolve();
6667
await expect
67-
.poll(() => canRead(runtime.authority, fixture.newRoot))
68+
.poll(() => canRead(runtime.authority, fixture.newRoot), {
69+
timeout: RELOAD_ASSERTION_TIMEOUT_MS,
70+
})
6871
.toBe(true);
6972

7073
expect(runtime.options.processPolicy?.().status).toBe("enabled");
@@ -109,7 +112,9 @@ describe("runtime permission reload", () => {
109112
releaseFirstRead.resolve();
110113
await secondReadStarted.promise;
111114
await expect
112-
.poll(() => canRead(runtime.authority, fixture.latestRoot))
115+
.poll(() => canRead(runtime.authority, fixture.latestRoot), {
116+
timeout: RELOAD_ASSERTION_TIMEOUT_MS,
117+
})
113118
.toBe(true);
114119

115120
expect(runtime.options.processPolicy?.().status).toBe("disabled");
@@ -134,9 +139,13 @@ describe("runtime permission reload", () => {
134139
configure(fixture.env, fixture.newRoot, true);
135140
runtime.reload();
136141
runtime.reload();
137-
await expect.poll(() => reads).toBe(2);
138142
await expect
139-
.poll(() => canRead(runtime.authority, fixture.newRoot))
143+
.poll(() => reads, { timeout: RELOAD_ASSERTION_TIMEOUT_MS })
144+
.toBe(2);
145+
await expect
146+
.poll(() => canRead(runtime.authority, fixture.newRoot), {
147+
timeout: RELOAD_ASSERTION_TIMEOUT_MS,
148+
})
140149
.toBe(true);
141150

142151
expect(reads).toBe(2);

tests/composition/analysis-sessions/identity.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ describe("package identity", () => {
4848
"node scripts/generate-package-metadata.mjs --check",
4949
);
5050
expect(packageJson.scripts.prebuild).toBe(
51-
"npm run deps:check && npm run metadata:check",
51+
"npm run deps:check && npm run metadata:check && node scripts/clean-build-output.mjs",
5252
);
5353
expect(packageJson.scripts["precheck:fast"]).toBe("npm run deps:check");
5454
expect(packageJson.scripts["check:fast"]).toBe("turbo run typecheck lint");

0 commit comments

Comments
 (0)