fix(management): remove duplicate create_request (management-mode server died at startup) — v1.6.1 #63

Merged
core-devops merged 1 commits from fix/mgmt-create-request-collision into main 2026-06-11 20:28:58 +00:00
3 changed files with 27 additions and 54 deletions
+1 -1
View File
@@ -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": {
+21 -17
View File
@@ -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", () => {
+5 -36
View File
@@ -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);