From cb4a89e546b7378814f186a3e80c4b003f66aa39 Mon Sep 17 00:00:00 2001 From: core-devops Date: Thu, 11 Jun 2026 12:36:21 -0700 Subject: [PATCH 1/2] =?UTF-8?q?feat(management):=20create=5Fapproval=20too?= =?UTF-8?q?l=20(mcp-server#61)=20=E2=80=94=20stop=20the=20concierge=20impr?= =?UTF-8?q?ovising=20with=20gated=20ops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The org concierge had no way to raise an approval in management mode (only list_pending_approvals), so when asked to demonstrate the approval flow it IMPROVISED by running destructive/gated operations — set_workspace_secret on its OWN workspace — which fires the secret-change auto-restart and terminated its own box mid-turn, twice on 2026-06-11 (core#2573; one occurrence cost a 14h org-root outage). Adds management-mode create_approval: POST /workspaces/:id/requests {kind:"approval", recipient_type:"user"} via mgmtCall — the same unified-requests shape the workspace-mode tool uses. Deliberately NO decide_approval in management mode: deciding is the HUMAN side of the gate and an agent must never hold it. Tests: roster test extended; behavior test asserts the exact POST body. 296 passing. Version bumped 1.5.0 -> 1.6.0 for the publish -> template-image -> repin chain. Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 2 +- src/__tests__/management.test.ts | 19 +++++++++++++++- src/tools/management/index.ts | 37 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a3ffa5c..164bbc7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@molecule-ai/mcp-server", - "version": "1.5.0", + "version": "1.6.0", "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 cc32ffb..03a17f9 100644 --- a/src/__tests__/management.test.ts +++ b/src/__tests__/management.test.ts @@ -52,6 +52,7 @@ import { handleResumeWorkspace, handleExportBundle, handleListOrgEvents, + handleCreateApproval as mgmtCreateApproval, } from "../tools/management/index.js"; import { handleRecreateWorkspace } from "../tools/management/cp_admin.js"; @@ -158,6 +159,22 @@ describe("workspace secret tools", () => { expect(JSON.parse(init.body as string)).toEqual({ key: "ANTHROPIC_API_KEY", value: "sk-x" }); }); + it("create_approval POSTs an approval-kind request addressed to the user (mcp-server#61)", async () => { + const f = mockFetch({ ok: true, id: "req-1" }); + global.fetch = f as unknown as typeof fetch; + await mgmtCreateApproval({ workspace_id: "w1", action: "Test approval", reason: "demo" }); + const { url, init } = lastCall(f); + expect(url).toBe(`${HOST}/workspaces/w1/requests`); + expect(init.method).toBe("POST"); + expect(JSON.parse(init.body as string)).toEqual({ + kind: "approval", + recipient_type: "user", + recipient_id: "", + title: "Test approval", + detail: "demo", + }); + }); + it("list_workspace_secrets GETs /workspaces/:id/secrets", async () => { const f = mockFetch([{ key: "FOO" }]); global.fetch = f as unknown as typeof fetch; @@ -589,7 +606,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", + "list_org_events", "list_pending_approvals", "create_approval", ]) { expect(names).toContain(expected); } diff --git a/src/tools/management/index.ts b/src/tools/management/index.ts index 0bfafc3..cac9916 100644 --- a/src/tools/management/index.ts +++ b/src/tools/management/index.ts @@ -380,6 +380,33 @@ export async function handleListPendingApprovals() { return toMcpResult(await mgmtGet("/approvals/pending")); } +// 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 +// 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 +// decide_approval here: deciding is the HUMAN side of the gate and an agent +// must never hold it. +const CreateApprovalMgmtSchema = z.object({ + workspace_id: z.string().describe("Workspace the approval is raised for/anchored to"), + action: z.string().describe("What needs approval (becomes the request title)"), + reason: z.string().optional().describe("Why it's needed (becomes the detail)"), +}); + +export async function handleCreateApproval(args: unknown) { + const p = validate(args, CreateApprovalMgmtSchema); + return toMcpResult( + await mgmtCall("POST", `/workspaces/${encodeURIComponent(p.workspace_id)}/requests`, { + kind: "approval", + recipient_type: "user", + recipient_id: "", + title: p.action, + detail: p.reason, + }), + ); +} + // --------------------------------------------------------------------------- // Registration // --------------------------------------------------------------------------- @@ -619,6 +646,16 @@ export function registerManagementTools(srv: McpServer) { {}, handleListPendingApprovals, ); + srv.tool( + "create_approval", + "Management: raise an approval request to the user for a workspace action. Use this (NEVER a destructive/gated operation) when you need a human decision or want to demonstrate the approval flow.", + { + workspace_id: z.string().describe("Workspace the approval is raised for/anchored to"), + action: z.string().describe("What needs approval (becomes the request title)"), + reason: z.string().optional().describe("Why it's needed (becomes the detail)"), + }, + handleCreateApproval, + ); // --- CP-tier tools (separate module — Org API Key cannot reach CP) --- registerCpAdminTools(srv); -- 2.52.0 From 95bab1407de303e355f02c759b98f2c28afa7b17 Mon Sep 17 00:00:00 2001 From: core-devops Date: Thu, 11 Jun 2026 12:37:35 -0700 Subject: [PATCH 2/2] =?UTF-8?q?feat(management):=20create=5Frequest=20(kin?= =?UTF-8?q?d=20task|approval)=20=E2=80=94=20unified=20form=20mirroring=20w?= =?UTF-8?q?orkspace-mode=20requests.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit create_approval stays as the approval-kind alias (#61 names it). kind=task covers the other half of the user's inbox: agent asks the user to DO something. Behavior test asserts the exact POST body. 297 passing. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/__tests__/management.test.ts | 18 +++++++++++++++- src/tools/management/index.ts | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/__tests__/management.test.ts b/src/__tests__/management.test.ts index 03a17f9..4a6d75e 100644 --- a/src/__tests__/management.test.ts +++ b/src/__tests__/management.test.ts @@ -53,6 +53,7 @@ import { handleExportBundle, handleListOrgEvents, handleCreateApproval as mgmtCreateApproval, + handleCreateRequest as mgmtCreateRequest, } from "../tools/management/index.js"; import { handleRecreateWorkspace } from "../tools/management/cp_admin.js"; @@ -175,6 +176,21 @@ 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; @@ -606,7 +622,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", + "list_org_events", "list_pending_approvals", "create_approval", "create_request", ]) { expect(names).toContain(expected); } diff --git a/src/tools/management/index.ts b/src/tools/management/index.ts index cac9916..561ec6c 100644 --- a/src/tools/management/index.ts +++ b/src/tools/management/index.ts @@ -407,6 +407,30 @@ 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 // --------------------------------------------------------------------------- @@ -656,6 +680,17 @@ 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); -- 2.52.0