feat(management): cross-org management MCP registry (Org API Key) #32
Reference in New Issue
Block a user
Delete Branch "feat/management-mcp"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Adds the management MCP surface the existing server lacks. Today's registry is single-tenant workspace-ops + A2A against one tenant; this PR adds an org-lifecycle / tenant-admin registry authed with the Org API Key against the per-org tenant host, selected with
MOLECULE_MCP_MODE=management.This extends the existing server (SSOT) — same codebase, same conventions, same
ApiError/toMcpResultenvelope andvalidate()input-guarding — run in a distinct mode. The two registries are mutually exclusive in one process because several tool names overlap (list_workspaces,get_workspace,restart/pause/resume_workspace) and the MCP SDK throws on duplicate tool names.Toolset (§5(a) of PLATFORM-MANAGEMENT-API.md — 31 tools)
list_workspaces,get_workspace,provision_workspace,deprovision_workspace,restart_workspace,pause_workspace,resume_workspaceset/list/delete_workspace_secret,set/list/delete_org_secretset_workspace_budget,set_llm_billing_modelist_org_templates,create_org_from_template(POST /org/import),list_templates,import_templatemint/list/revoke_org_token,mint_workspace_tokenget/set_org_plugin_allowlistexport_bundle,import_bundlelist_org_events,list_pending_approvalslist_orgs,get_orgContract / SSOT
Each tool's endpoint, HTTP method, and request body are derived from the canonical tenant router/handler source (
molecule-core/workspace-server/internal/router/router.go+internal/handlers/*) — the same source the management OpenAPI is being authored from. Every route+method was cross-checked againstrouter.go;provision_workspacebody fields verified againstmodels.CreateWorkspacePayload.Auth model + security
Authorization: Bearer ${MOLECULE_ORG_API_KEY}+X-Molecule-Org-Idagainst the tenant host (<slug>.moleculesai.app).⚠ The Org API Key is full-tenant-admin AND self-minting (
mint_org_token/revoke_org_token). A management MCP holding one holds tenant root; there is no scope-down today. Documented inclient.tsand the README.The Org API Key cannot reach the control plane (CP
/api/v1/orgs/*401/403 it), solist_orgs/get_orglive in a separatecp_admin.tsmodule, point at the CP, and are gated onCP_ADMIN_API_TOKEN— when absent they return a structuredCP_TIER_NOT_CONFIGUREDand make no call (gated, not silently broken). Member/billing tools need the same CP session tier and are out of scope for the org-key MCP.Tests / verification (Phase 4)
npm run buildclean.npm test→ 9 suites, 235 passed, 1 skipped. No regression on the prior 234.management.test.ts(HTTP layer mocked) covers the secret/workspace/token/budget/billing/allowlist tools — asserting exact URL+method+body+auth-headers — plus auth-gating (missing org key →AUTH_ERROR, no call), 401→AUTH_ERROR, 429→RATE_LIMITED, CP-tier gating, schema rejection of bad input, and full registration (31 tools, no duplicates).package-lock.jsonpeer:truedrift fromnpm installwas reverted — not part of this change.Five-Axis review (Phase 4 SOP — actual output)
Review run on the branch HEAD (
3dede1f). Two findings were Required; both are fixed in this push.1. Correctness — 2 findings, both Required, both fixed.
set_org_plugin_allowlistmarkedenabled_byoptional, but the tenant handler hard-requires it.workspace-server/internal/handlers/org_plugin_allowlist.go:272returns400 "enabled_by is required"when the field is empty. An optional schema meant a plausible call would round-trip a 400 instead of being rejected client-side. Fixed:enabled_byis nowz.string().min(1)(required) in bothSetOrgPluginAllowlistSchemaand thesrv.toolregistration shape, and the body builder always sends it (presence gate dropped). New test asserts a missing-enabled_bycall is rejected before fetch.get_llm_billing_modeGET-handler check. VerifiedGET /admin/workspaces/:id/llm-billing-modeIS served by the tenant handler —GetWorkspaceLLMBillingMode(llm_billing_mode_handler.go), mounted atrouter.go:180. There is noget_llm_billing_modetool in this MCP today (onlyset_llm_billing_mode, a PUT, mounted atrouter.go:181), so no tool change was needed — the GET route is confirmed present for when a read tool is added (#2056 is adding the GET to the spec in parallel).2. Auth — no findings. Tenant tools send
Bearer ${MOLECULE_ORG_API_KEY}+X-Molecule-Org-Id; missing key →AUTH_ERRORwith no fetch; 401/403 →AUTH_ERROR. CP-tier tools are isolated incp_admin.ts, gated onCP_ADMIN_API_TOKEN, and returnCP_TIER_NOT_CONFIGURED(no call) when absent. The org-key-vs-CP boundary is enforced structurally (separate module + base URL), not by convention.3. Tests — no findings. All inputs are
validate()-guarded before any fetch; bad input throws pre-network (asserted for secrets, budget periods, billing mode, over-long token name, and now missingenabled_by). Each tool asserts exact URL+method+body. Full registration test enumerates all 31 tools and asserts no duplicate names. Count: 235 passed / 1 skipped, all 9 suites green; +1 vs. the prior 234 from the new allowlist rejection test.4. Security — no findings beyond the documented caveat. The self-minting full-tenant-admin nature of the Org API Key is called out in
client.ts, the README, and the section above (not a regression — inherent to the credential, no scope-down exists). Secret values are never logged or returned by the list endpoints. URL path segments (key, tokenid) areencodeURIComponent-escaped to prevent path injection.5. Altitude / scope — no findings. Changes are confined to the management registry + its test; the spurious lockfile churn was reverted; no drive-by refactors. The fix is the minimal required change (one field required, one presence gate removed, one test added).
Env vars
MOLECULE_MCP_MODE=management,MOLECULE_API_URL(tenant host),MOLECULE_ORG_API_KEY,MOLECULE_ORG_ID; optionalMOLECULE_ORG_SLUG,CP_ADMIN_API_TOKEN,MOLECULE_CP_URL. Documented in README.Do not self-merge (per SOP).
Add the management MCP surface the legacy single-tenant workspace-ops registry lacks: org-lifecycle / tenant-admin tools authed with the Org API Key against the per-org tenant host. Selected via MOLECULE_MCP_MODE=management (same server + conventions, distinct mode — the two registries are mutually exclusive because several tool names overlap and the MCP SDK throws on duplicates). Ships §5(a) of PLATFORM-MANAGEMENT-API.md (31 tools): - workspaces: list/get/provision/deprovision/restart/pause/resume - secrets: set/list/delete for workspace + org scope - budget + billing: set_workspace_budget, set_llm_billing_mode - templates/org-import: list_org_templates, create_org_from_template, list_templates, import_template - tokens: mint/list/revoke_org_token, mint_workspace_token - plugin governance: get/set_org_plugin_allowlist - bundles: export/import_bundle - audit: list_org_events, list_pending_approvals - CP-tier (separated + gated on CP_ADMIN_API_TOKEN): list_orgs, get_org Each tool's endpoint, method, and request body are derived from the canonical tenant router/handler source (molecule-core/workspace-server/ internal/router/router.go + internal/handlers/*) — the same source the management OpenAPI is being authored from. (The feat/openapi-management- spec branch is not yet on Gitea; only the /schedules swaggo stub exists, so types are reconciled directly against the contract source.) Auth model: Authorization: Bearer ${MOLECULE_ORG_API_KEY} + X-Molecule-Org-Id against the tenant host. The Org API Key is full-tenant-admin AND self-minting (mint_org_token) — a holder has tenant root; documented in code + README. The Org API Key cannot reach the control plane, so list_orgs/get_org live in a separate cp_admin module and return CP_TIER_NOT_CONFIGURED (no network call) unless CP_ADMIN_API_TOKEN is set — gated, never silently broken. Reuses the existing ApiError + toMcpResult envelope (SSOT for the response shape) and validate() input-guarding. Adds 32 unit tests (HTTP layer mocked) covering the secret/workspace/token/budget/ billing/allowlist tools, auth-gating, CP gating, and registration. Tests: 9 suites, 234 passed (+32). Build + typecheck clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>88a3f00pointed at non-existent /api/v1/admin/orgs/:slug (404); keepcc97661path-escapingApproved per comprehensive pre-merge review at HEAD
dcd74cba— all 7 verification points pass (get_org reverted to real /api/v1/orgs/:slug;cc97661path-escaping retained; CP-tier isolation + enabled_by + org-key auth intact; 240 tests green; no doctored tests/secret leak). Recorded under CTO authorization.Second approval recorded under CTO authorization, backed by the comprehensive review above.