From 4f07caa866b307f10d62b13b2341c4bf67cb37d4 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Thu, 11 Jun 2026 02:37:59 +0000 Subject: [PATCH] fix(mcp-tools): add confirm_name parameter to destructive workspace tools (#58) The platform gates destructive actions (delete_workspace, deprovision_workspace) behind a confirmation requirement that echoes the workspace's exact name via the X-Confirm-Name header. The MCP tools previously exposed no parameter to supply this confirmation, blocking agents from completing deletion even with human approval. Changes: - Add optional confirm_name parameter to delete_workspace and deprovision_workspace tool schemas - Plumb confirm_name through to X-Confirm-Name header in both apiCall (workspaces.ts) and mgmtCall (management/client.ts) - Add extraHeaders support to mgmtCall for parity with apiCall - Add tests verifying the header is sent when confirm_name is provided Fixes molecule-mcp-server#58 --- src/__tests__/index.test.ts | 12 ++++++++++++ src/__tests__/management.test.ts | 10 ++++++++++ src/tools/management/client.ts | 3 ++- src/tools/management/index.ts | 9 +++++++-- src/tools/workspaces.ts | 12 ++++++++---- 5 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index e9cb3d6..3813190 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -543,6 +543,18 @@ describe("handleDeleteWorkspace()", () => { expect.objectContaining({ method: "DELETE" }) ); }); + + test("sends X-Confirm-Name header when confirm_name is provided", async () => { + global.fetch = mockFetch({ deleted: true }); + await handleDeleteWorkspace({ workspace_id: "ws-del", confirm_name: "Test-PM" }); + expect(global.fetch).toHaveBeenCalledWith( + `${PLATFORM_URL}/workspaces/ws-del?confirm=true`, + expect.objectContaining({ + method: "DELETE", + headers: expect.objectContaining({ "X-Confirm-Name": "Test-PM" }), + }) + ); + }); }); describe("handleRestartWorkspace()", () => { diff --git a/src/__tests__/management.test.ts b/src/__tests__/management.test.ts index 30c00ab..cc32ffb 100644 --- a/src/__tests__/management.test.ts +++ b/src/__tests__/management.test.ts @@ -234,6 +234,16 @@ describe("workspace lifecycle tools", () => { expect(url).toBe(`${HOST}/workspaces/w1`); expect(init.method).toBe("DELETE"); }); + + it("deprovision_workspace sends X-Confirm-Name when confirm_name is provided", async () => { + const f = mockFetch({ ok: true }); + global.fetch = f as unknown as typeof fetch; + await handleDeprovisionWorkspace({ workspace_id: "w1", confirm_name: "Test-PM" }); + const { url, init } = lastCall(f); + expect(url).toBe(`${HOST}/workspaces/w1`); + expect(init.method).toBe("DELETE"); + expect(headersOf(init)["X-Confirm-Name"]).toBe("Test-PM"); + }); }); describe("budget + billing tools", () => { diff --git a/src/tools/management/client.ts b/src/tools/management/client.ts index 2365296..2622aec 100644 --- a/src/tools/management/client.ts +++ b/src/tools/management/client.ts @@ -99,6 +99,7 @@ export async function mgmtCall( method: string, path: string, body?: unknown, + extraHeaders?: Record, ): Promise { const headers = managementHeaders(); if (!isHeaders(headers)) return headers; @@ -106,7 +107,7 @@ export async function mgmtCall( const base = managementUrl(); const res = await fetch(`${base}${path}`, { method, - headers, + headers: { ...headers, ...(extraHeaders ?? {}) }, body: body !== undefined ? JSON.stringify(body) : undefined, }); if (!res.ok) { diff --git a/src/tools/management/index.ts b/src/tools/management/index.ts index 4cf5ba3..0bfafc3 100644 --- a/src/tools/management/index.ts +++ b/src/tools/management/index.ts @@ -44,6 +44,7 @@ const ProvisionWorkspaceSchema = z.object({ const DeprovisionWorkspaceSchema = z.object({ workspace_id: z.string().describe("Workspace UUID"), + confirm_name: z.string().optional().describe("Echo the workspace's exact name to confirm destructive action (maps to X-Confirm-Name header)"), }); const WorkspaceLifecycleSchema = z.object({ @@ -197,7 +198,8 @@ export async function handleProvisionWorkspace(args: unknown) { export async function handleDeprovisionWorkspace(args: unknown) { const p = validate(args, DeprovisionWorkspaceSchema); - return toMcpResult(await mgmtCall("DELETE", `/workspaces/${encodeURIComponent(p.workspace_id)}`)); + const headers = p.confirm_name ? { "X-Confirm-Name": p.confirm_name } : undefined; + return toMcpResult(await mgmtCall("DELETE", `/workspaces/${encodeURIComponent(p.workspace_id)}`, undefined, headers)); } export async function handleRestartWorkspace(args: unknown) { @@ -413,7 +415,10 @@ export function registerManagementTools(srv: McpServer) { srv.tool( "deprovision_workspace", "Management: delete/deprovision a workspace (cascades to children).", - { workspace_id: z.string().describe("Workspace UUID") }, + { + workspace_id: z.string().describe("Workspace UUID"), + confirm_name: z.string().optional().describe("Echo the workspace's exact name to confirm destructive action"), + }, handleDeprovisionWorkspace, ); srv.tool( diff --git a/src/tools/workspaces.ts b/src/tools/workspaces.ts index a4e3cde..8de8cd1 100644 --- a/src/tools/workspaces.ts +++ b/src/tools/workspaces.ts @@ -329,8 +329,9 @@ export async function handleGetWorkspace(params: { workspace_id: string }) { return toMcpResult(data); } -export async function handleDeleteWorkspace(params: { workspace_id: string }) { - const data = await apiCall("DELETE", `/workspaces/${params.workspace_id}?confirm=true`); +export async function handleDeleteWorkspace(params: { workspace_id: string; confirm_name?: string }) { + const headers = params.confirm_name ? { "X-Confirm-Name": params.confirm_name } : undefined; + const data = await apiCall("DELETE", `/workspaces/${params.workspace_id}?confirm=true`, undefined, headers); return toMcpResult(data); } @@ -434,8 +435,11 @@ export function registerWorkspaceTools(srv: McpServer) { srv.tool( "delete_workspace", - "Delete a workspace (cascades to children)", - { workspace_id: z.string().describe("Workspace ID") }, + "Delete a workspace (cascades to children).", + { + workspace_id: z.string().describe("Workspace ID"), + confirm_name: z.string().optional().describe("Echo the workspace's exact name to confirm destructive action"), + }, handleDeleteWorkspace ); -- 2.52.0