diff --git a/package.json b/package.json index 164bbc7..fea4702 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@molecule-ai/mcp-server", - "version": "1.6.0", + "version": "1.6.1", "description": "MCP server for Molecule AI Agent Team \u2014 manage workspaces, agents, and skills from any AI coding tool", "type": "module", "exports": { diff --git a/src/__tests__/management.test.ts b/src/__tests__/management.test.ts index 4a6d75e..d204cdd 100644 --- a/src/__tests__/management.test.ts +++ b/src/__tests__/management.test.ts @@ -10,6 +10,14 @@ jest.mock("@modelcontextprotocol/sdk/server/mcp.js", () => ({ McpServer: class { registeredToolNames: string[] = []; tool(name: string) { + // Mirror the real SDK: duplicate tool names throw at registration. + // Without this the composed-server test cannot catch cross-registry + // collisions (the management create_request duplicate killed the + // management server at startup on 2026-06-11; only the image smoke + // gate caught it). + if (this.registeredToolNames.includes(name)) { + throw new Error(`Tool ${name} is already registered`); + } this.registeredToolNames.push(name); } connect() { @@ -53,7 +61,6 @@ import { handleExportBundle, handleListOrgEvents, handleCreateApproval as mgmtCreateApproval, - handleCreateRequest as mgmtCreateRequest, } from "../tools/management/index.js"; import { handleRecreateWorkspace } from "../tools/management/cp_admin.js"; @@ -176,21 +183,6 @@ describe("workspace secret tools", () => { }); }); - it("create_request kind=task POSTs a task-kind request to the user", async () => { - const f = mockFetch({ ok: true, id: "req-2" }); - global.fetch = f as unknown as typeof fetch; - await mgmtCreateRequest({ workspace_id: "w1", kind: "task", title: "Review the report", detail: "by EOD" }); - const { url, init } = lastCall(f); - expect(url).toBe(`${HOST}/workspaces/w1/requests`); - expect(JSON.parse(init.body as string)).toEqual({ - kind: "task", - recipient_type: "user", - recipient_id: "", - title: "Review the report", - detail: "by EOD", - }); - }); - it("list_workspace_secrets GETs /workspaces/:id/secrets", async () => { const f = mockFetch([{ key: "FOO" }]); global.fetch = f as unknown as typeof fetch; @@ -622,7 +614,7 @@ describe("registration + mode", () => { "mint_org_token", "list_org_tokens", "revoke_org_token", "mint_workspace_token", "get_org_plugin_allowlist", "set_org_plugin_allowlist", "export_bundle", "import_bundle", - "list_org_events", "list_pending_approvals", "create_approval", "create_request", + "list_org_events", "list_pending_approvals", "create_approval", ]) { expect(names).toContain(expected); } @@ -632,11 +624,23 @@ describe("registration + mode", () => { it("createServer in management mode registers only the management surface", () => { process.env.MOLECULE_MCP_MODE = "management"; + // The mock McpServer throws on duplicate names (like the real SDK), so + // simply composing the full management-mode server here is the + // regression gate against cross-registry tool-name collisions. const srv = createServer() as unknown as { registeredToolNames: string[] }; expect(srv.registeredToolNames).toContain("provision_workspace"); + // The unified request tools come from requests.ts (BOTH modes) — the + // management registry must NOT duplicate them. + expect(srv.registeredToolNames).toContain("create_request"); + expect(srv.registeredToolNames).toContain("create_approval"); // Legacy-only tools (chat_with_agent) must NOT be present in mgmt mode. expect(srv.registeredToolNames).not.toContain("chat_with_agent"); }); + + it("createServer in workspace mode composes without tool-name collisions", () => { + process.env.MOLECULE_MCP_MODE = ""; + expect(() => createServer()).not.toThrow(); + }); }); describe("path segment escaping", () => { diff --git a/src/tools/management/index.ts b/src/tools/management/index.ts index 561ec6c..799bd2f 100644 --- a/src/tools/management/index.ts +++ b/src/tools/management/index.ts @@ -382,7 +382,11 @@ export async function handleListPendingApprovals() { // create_approval (mcp-server#61) — raise an approval-kind request addressed // to the USER via the unified requests system (same shape the workspace-mode -// tool uses; see ../approvals.ts handleCreateApproval). Without this tool the +// tool uses; see ../approvals.ts handleCreateApproval). The GENERAL form is +// create_request from ../requests.ts, registered in BOTH modes by +// createServer — do NOT add a management duplicate of it: the MCP SDK throws +// on duplicate tool names and the whole management server dies at startup +// (caught by the platform-agent image smoke gate, 2026-06-11). Without this // org concierge IMPROVISED approval demos by running gated/destructive ops // (set_workspace_secret on itself → secret-change auto-restart → its own box // terminated mid-turn, twice on 2026-06-11 — core#2573). Deliberately NO @@ -407,30 +411,6 @@ export async function handleCreateApproval(args: unknown) { ); } -// create_request — the unified form (mirrors the workspace-mode tool in -// ../requests.ts): kind='task' asks the user to DO something; kind='approval' -// asks the user to APPROVE something. create_approval above is the -// approval-kind convenience alias (issue #61 names it explicitly). -const CreateRequestMgmtSchema = z.object({ - workspace_id: z.string().describe("Workspace the request is raised for/anchored to"), - kind: z.enum(["task", "approval"]).describe("task = please do X; approval = please approve X"), - title: z.string().describe("Short title shown in the user's inbox"), - detail: z.string().optional().describe("Longer context / why"), -}); - -export async function handleCreateRequest(args: unknown) { - const p = validate(args, CreateRequestMgmtSchema); - return toMcpResult( - await mgmtCall("POST", `/workspaces/${encodeURIComponent(p.workspace_id)}/requests`, { - kind: p.kind, - recipient_type: "user", - recipient_id: "", - title: p.title, - detail: p.detail, - }), - ); -} - // --------------------------------------------------------------------------- // Registration // --------------------------------------------------------------------------- @@ -680,17 +660,6 @@ export function registerManagementTools(srv: McpServer) { }, handleCreateApproval, ); - srv.tool( - "create_request", - "Management: raise a request to the user — kind='task' asks them to DO something; kind='approval' asks them to APPROVE something. The safe way to put work or decisions in the user's inbox.", - { - workspace_id: z.string().describe("Workspace the request is raised for/anchored to"), - kind: z.enum(["task", "approval"]).describe("task = please do X; approval = please approve X"), - title: z.string().describe("Short title shown in the user's inbox"), - detail: z.string().optional().describe("Longer context / why"), - }, - handleCreateRequest, - ); // --- CP-tier tools (separate module — Org API Key cannot reach CP) --- registerCpAdminTools(srv);