feat(management): create_approval tool (mcp-server#61) — stop the concierge improvising with gated ops #62
+1
-1
@@ -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": {
|
||||
|
||||
@@ -52,6 +52,8 @@ import {
|
||||
handleResumeWorkspace,
|
||||
handleExportBundle,
|
||||
handleListOrgEvents,
|
||||
handleCreateApproval as mgmtCreateApproval,
|
||||
handleCreateRequest as mgmtCreateRequest,
|
||||
} from "../tools/management/index.js";
|
||||
import { handleRecreateWorkspace } from "../tools/management/cp_admin.js";
|
||||
|
||||
@@ -158,6 +160,37 @@ 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("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;
|
||||
@@ -589,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",
|
||||
"list_org_events", "list_pending_approvals", "create_approval", "create_request",
|
||||
]) {
|
||||
expect(names).toContain(expected);
|
||||
}
|
||||
|
||||
@@ -380,6 +380,57 @@ 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,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// 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
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -619,6 +670,27 @@ 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,
|
||||
);
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user