Files
reverse-engineering-anything/tests/boundary/mcp/toolRegistrationOptions.test.ts
N0zoM1z0 2919def828 fix(skill): remove obsolete grants and audit tool effects
## Changes

Related to #545 and the permission-system removal in #555.

- Upgrade the shipped skill to v24 and remove obsolete filesystem grants, per-call approval flags, Electron file-root configuration, and DMG approval flags. Calls follow the user-requested investigation directly. Keep setup plan confirmation, actual OS requirements, and process/archive lifecycle boundaries.
- Correct the passive browser guide: endpoint/scope come from requests; ordinary local URL queries are retained, while credentials remain redacted.
- Audit canonical tool effects: declare possible DMG process/temp-directory effects, mark fresh extraction non-idempotent, and mark comparisons of supplied captures/PNGs as closed-world. Align artifact-provider capability effects with temporary mount-directory writes.
- Retain non-read-only hints for calls that add Evidence to the session; document that annotations are hints and that clients control their confirmation UI. Do not mislabel session mutations as read-only to suppress prompts.
- Regenerate package, skill, MCP, and product metadata from source.

## Verification

- `npm run test:focused -- <11 affected test files>`: **40 tests passed**, including skill transaction/backup/idempotence, SDK tools/list annotations, actual local capture/PNG comparisons, artifact extraction/containment/cancellation, DMG launcher/cleanup seam, setup confirmation, browser MCP and scenario MCP, CLI catalog parity.
- `npm run check`: typecheck, lint, format, knip, package metadata.
- `npm run docs:check`: generated catalogs, skill metadata, documentation facts, completion ledger, error schema.

No local real Hopper/Ghidra run: their adapters and analysis operations are unchanged. Native macOS DMG behavior is covered locally through the launcher seam; no local macOS/hdiutil claim is made.

Real browser verification passed using existing Headless Chrome143.0.7499.4: actual CLI/stdio MCP,9,331,407-byte PNG, passive capture/source maps, and owned/external scenario cleanup. An initial full Chrome143 run failed the existing screenshot-height assertion (1397 versus1536); the dedicated headless-shell run passed the complete verifier without code changes. The verifier run-lineage field remained unavailable; no lineage-coverage claim is made. Browser download into `.codex` was blocked by CDN403, so the successful run used an existing browser without changing host configuration.
2026-10-06 01:40:58 +08:00

153 lines
5.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { McpServer } from "@modelcontextprotocol/server";
import { TOOL_CONTRACTS } from "../../../src/contracts/toolContracts.js";
import { GENERATED_MCP_TOOL_CATALOG } from "../../../src/generatedMcpToolCatalog.js";
import { annotationsFromEffects } from "../../../src/contracts/toolEffects.js";
import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js";
describe("tool registration options", () => {
it("generates the checked-in catalog from the SDK wire projection", async () => {
const server = new McpServer({ name: "catalog-test", version: "0" });
for (const contract of TOOL_CONTRACTS)
server.registerTool(
contract.name,
toolRegistrationOptions(contract),
async () => ({
content: [{ type: "text" as const, text: "catalog-only" }],
isError: true as const,
}),
);
const client = new Client({ name: "catalog-test", version: "0" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const advertised = (await client.listTools()).tools.map((tool) => ({
name: tool.name,
title: tool.title,
description: tool.description,
inputSchema: tool.inputSchema,
outputSchema: tool.outputSchema,
annotations: tool.annotations,
}));
for (const contract of TOOL_CONTRACTS) {
const tool = advertised.find(({ name }) => name === contract.name);
expect(tool, contract.name).toBeDefined();
expect(describesObject(tool?.inputSchema), contract.name).toBe(true);
expect(describesObject(tool?.outputSchema), contract.name).toBe(true);
expect(tool?.title?.trim().length, contract.name).toBeGreaterThan(0);
expect(tool?.description?.trim().length, contract.name).toBeGreaterThan(
0,
);
expect(JSON.stringify(tool?.outputSchema), contract.name).not.toContain(
'"result":{}',
);
expect(tool?.annotations, contract.name).toEqual(
annotationsFromEffects(contract.effects),
);
expect(contract.examples.length, contract.name).toBeGreaterThan(0);
for (const example of contract.examples) {
expect(example.title.trim().length, contract.name).toBeGreaterThan(0);
expect(
contract.inputSchema.safeParse(example.input).success,
`${contract.name}: ${example.title}`,
).toBe(true);
}
expect(tool?.inputSchema.examples, contract.name).toEqual(
contract.examples.map(({ input }) => input),
);
expect(
missingPropertyDescriptions(tool?.inputSchema),
contract.name,
).toEqual([]);
}
expect(advertised).toEqual(
GENERATED_MCP_TOOL_CATALOG.map((tool) => ({
name: tool.name,
title: tool.title,
description: tool.description,
inputSchema: tool.inputSchema,
outputSchema: tool.outputSchema,
annotations: tool.annotations,
})),
);
for (const [name, expected] of [
["binary_session", { readOnlyHint: true, openWorldHint: false }],
["read_bytes", { readOnlyHint: false, destructiveHint: false }],
["set_comment", { readOnlyHint: false }],
["unset_bookmark", { destructiveHint: true }],
[
"capture_process_scenario",
{ readOnlyHint: false, openWorldHint: true },
],
["inspect_artifact", { readOnlyHint: false, openWorldHint: true }],
["extract_artifact", { readOnlyHint: false, idempotentHint: false }],
["compare_web_captures", { readOnlyHint: false, openWorldHint: false }],
[
"compare_web_screenshots",
{ readOnlyHint: false, openWorldHint: false },
],
] as const) {
expect(
advertised.find((tool) => tool.name === name)?.annotations,
name,
).toMatchObject(expected);
}
} finally {
await Promise.allSettled([client.close(), server.close()]);
}
});
});
const missingPropertyDescriptions = (
value: unknown,
path = "inputSchema",
): string[] => {
if (Array.isArray(value))
return value.flatMap((child, index) =>
missingPropertyDescriptions(child, `${path}[${index}]`),
);
if (!isObject(value)) return [];
const missing: string[] = [];
if (isObject(value.properties))
for (const [property, schema] of Object.entries(value.properties)) {
const propertyPath = `${path}.properties.${property}`;
if (!isObject(schema) || typeof schema.description !== "string")
missing.push(propertyPath);
missing.push(...missingPropertyDescriptions(schema, propertyPath));
}
for (const [key, child] of Object.entries(value))
if (
key !== "properties" &&
key !== "examples" &&
key !== "default" &&
key !== "const" &&
key !== "enum"
)
missing.push(...missingPropertyDescriptions(child, `${path}.${key}`));
return missing;
};
const isObject = (value: unknown): value is Readonly<Record<string, unknown>> =>
typeof value === "object" && value !== null && !Array.isArray(value);
const describesObject = (schema: unknown): boolean => {
if (!isObject(schema)) return false;
return (
schema.type === "object" ||
[schema.oneOf, schema.anyOf].some(
(variants) =>
Array.isArray(variants) &&
variants.length > 0 &&
variants.every(
(variant: unknown) => isObject(variant) && variant.type === "object",
),
)
);
};