mirror of
https://github.com/morluto/rea.git
synced 2026-11-06 06:46:10 +00:00
## 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.
153 lines
5.7 KiB
TypeScript
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",
|
|
),
|
|
)
|
|
);
|
|
};
|