On this page
commander-mcp
Commander sits on both sides of MCP; the two halves are separate systems with separate trust boundaries, so read the half that matches the task.
1. Commander as MCP consumer and broker
The MCP client here is Claude Code inside a session; Commander's job is to hand it servers without ever handing it a long-lived credential.
Catalog. GET /api/mcp/servers returns org entitlements (synced from DataShield Auth) unioned with locally registered servers, each with toolCount and honest freshness (synced, expired, local) — no invented health dot. POST /api/mcp/sync re-syncs and degrades to {synced: 0, skipped: "org-disabled"} when the org tier is unconfigured, so local servers keep working. POST /api/mcp/local registers a local or bring-your-own server (grammar-checked slug, transport stdio/http/sse, command for stdio or endpoint otherwise, trust_tier defaulting to untrusted); its credential is a reference to an Auth vault connection, never an inline secret.
Bindings. GET/PUT /api/mcp/bindings/:personaId hold per-persona server_slug, tool_allow and require_approval with replace-set semantics; a binding may only narrow the entitled tool set, never widen it.
Credentials. lib/mcp-broker.js is a client of Auth's application binder. Endpoints are discovered (RFC 8414 / OIDC discovery), never hardcoded, and a contract-major mismatch fails loudly. The app authenticates with its client credential for a short-lived app token (RFC 6749 §4.4), which seeds a per-spawn, tool-scoped token exchange (RFC 8693) with scopes shaped mcp:<slug>:<tool> (or mcp:<slug>:* server-wide), the slug as audience and the endpoint as the RFC 8707 resource; lifetime is clamped to a session ceiling. Sender constraint is configurable — bearer (RFC 6750) by default, since a pre-minted token in a static config file cannot produce per-request proofs, with DPoP (RFC 9449) where the requester can sign them and mTLS (RFC 8705) as the third option. broker and gateway modes both mint; only the target URL differs (the server's own endpoint, or the Auth passthrough gateway from metadata). The client secret is environment-only, never a settings row.
Launch. POST /api/mcp/sessions/:id/launch-config resolves a persona's bindings into connections and returns `{mcpConfigPath, strictMcpConfig, allowedTools, blocksDangerousSkip, servers, warnings}. The per-session .mcp.json` is written into a 0700 runtime directory at mode 0600, enforced even if the file pre-existed. allowedTools compiles to mcp__<slug> for a whole server or mcp__<slug>__<tool> per tool, and a strict-config flag keeps the CLI to exactly that file. Approval policy is server-authoritative: an explicit require_approval wins, otherwise any destructive tool in scope or an untrusted server forces approval and blocks permission-skipping. An unresolvable server becomes a warnings[] entry, never a silent omission.
Teardown. Deleting or ending a session revokes every token minted for it (RFC 7009) and unlinks its .mcp.json, so no credential outlives the session; revocation is opportunistic and never blocks teardown.
2. Commander as an MCP server: the commander-mcp gateway
commander-mcp is a separate process (not part of the Commander server) that lets Claude on the web, ChatGPT and any other MCP client operate Commander. It speaks MCP 2025-06-18 over Streamable HTTP at <publicUrl>/mcp and relays each tool call to Commander's REST API over the loopback. **Commander itself never faces the internet**: the gateway is the only internet-facing surface, stateless and restartable without touching a PTY.
Authorization. Until DataShield Auth issues for it, the gateway is its own OAuth 2.1 authorization server: RFC 8414 metadata at /.well-known/oauth-authorization-server, RFC 9728 protected-resource metadata at /.well-known/oauth-protected-resource naming that server, RFC 7591 dynamic client registration (toggleable, rate-limited, client-count capped), authorization-code with PKCE S256 (plain refused), a login page verifying the Commander app password through the same argon2id path the SPA uses, single-use codes bound to client, PKCE and redirect, rotating refresh tokens, and RFC 7009 revocation. Access tokens are ES256 JWS minted with Node crypto and published at /oauth/jwks.json; signing keys live encrypted in the database. Verification on every call checks signature, issuer, aud equal to this gateway's resource id (RFC 8707 — a dev token cannot replay at prod), expiry and scopes; any failure is a generic 401 while the real reason is logged server-side only. An auth OAuth mode switches verification to Auth's discovered JWKS instead.
Sessions and dispatch. POST <publicUrl>/mcp carries JSON-RPC 2.0: initialize (protocol version negotiated) mints an Mcp-Session-Id that every later call must present; a session belonging to a different subject is refused 403, and an unknown or expired one asks for a fresh initialize. tools/list, tools/call, ping, resources/list, resources/read, prompts/list and prompts/get are served; DELETE <publicUrl>/mcp ends a session; a GET answers 405, since no server-push stream is offered in this phase. Resources are read-only views (a project's sessions, a session brief) and prompts are persona boot prompts, redacted.
Tool shape. One umbrella tool per entity or function, the operation as a command enum, per-command parameters documented in the tool description — 171 REST routes as 171 tools would be unusable. Each command declares scope, read-only-ness, destructiveness and whether it needs confirm: true; the MCP 2025-06-18 annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) describe the visible command set. A command outside the caller's scope is invisible in tools/list, not merely refused; one outside the gateway's enabled phases is refused with the phase named. Destructive commands need confirm: true after a human has agreed, and session-control commands are refused outright on a prod instance. _hello returns the gateway identity, the commands your token can use, those that exist but are phase- or scope-gated, and — given an intent — a suggested tool chain with a confidence score. Results are ANSI-stripped, secret-masked and size-capped; errors carry a message, never a stack.
Attribution and audit. The gateway reaches Commander with its own service session (x-auth-token, environment-only) plus x-acting-subject and x-acting-client naming the human and client it acts for — those headers attribute, they never authorize. Every tools/call, denials included, writes one commander.mcp_audit row: subject, client id, tool, command, an argument hash (never the arguments), outcome, latency, size. Migration 036 adds the OAuth client, code, token, signing-key and audit tables. The vault, integrations, service accounts, license and login seam are not exposed at all, and promotion is not a tool. Configuration follows one chain read once at boot — named default → environment → commander.settings mcp_gateway_*, secrets environment-only — and GET <publicUrl>/healthz reports instance label, OAuth mode, visible tool count and resource id.
Status and versioning
The gateway ships on the dev branch and is not on the prod branch in this documented version; the dev process listens on port 3220 by default (prod's on
- and needs a service token, migration 036 and a routed public URL before it
answers. Two consequences for an agent:
- Do not assume a gateway exists at an arbitrary Commander instance. The only
sound test is RFC 9728 discovery — fetch <publicUrl>/.well-known/oauth-protected-resource and, if it answers, the named authorization server's RFC 8414 metadata. No metadata, no gateway.
- Do not assume a tool list. The visible surface depends on the gateway's
enabled phases and your token's scopes, and it grows between versions: discover it at runtime with tools/list (and _hello for what is gated and why) rather than from any list written here, this page included.
Related skills
commander-personas (bindings inside a launch recipe), commander-vault (credentials you create, unlike minted MCP tokens), commander-security (the boundaries the gateway deliberately does not cross).
You've seen the proof
Ready for a number? Scope your deployment and we'll price it against your own economics.
Get your quote →