DurinDoor
Features

MCP control

JSON-RPC management server at POST /api/mcp/control, its tools, auth, and client setup.

The control server is a JSON-RPC 2.0 MCP endpoint for the running DurinDoor instance. An MCP client lists combos, toggles connections, reads quota, and patches non-secret settings without opening the dashboard. The MCP gateway is a different endpoint: it sits in front of upstream MCP servers and authenticates with a gateway key on /api/mcp-gateway. Control authenticates with a dashboard session, the CLI token, or a DurinDoor API key, the same principals as the Management API.

Client snippets are under Connect a client below. A curl-and-key walkthrough is on Automating DurinDoor.

Check transport support

POST /api/mcp/control is stateless HTTP. One JSON-RPC object per POST. The handler implements POST only.

MethodRoleHTTP
initializeHandshake. Result includes protocolVersion: "2024-11-05", capabilities.tools, serverInfo.name: "durindoor-control"200
notifications/initializedMCP notification (no id)202 empty body
tools/listRegistered control tools and their input schemas200
tools/callInvoke params.name with params.arguments200, including JSON-RPC errors

A tools/call result is MCP content: { content: [{ type: "text", text: "<pretty JSON>" }] }. Unknown JSON-RPC methods return { error: { code: -32601 } } with HTTP 200. Parse failures are -32700. Invalid request is -32600. tools/call without name is -32602. Tool throws use error.status as the JSON-RPC code (400, 404) while HTTP stays 200. Transport and auth failures from the gate are HTTP 401 or 403.

Authenticate the client

Remote clients can use this endpoint with any of these credentials:

  • dashboard JWT cookie auth_token
  • header x-9r-cli-token
  • DurinDoor API key (Authorization: Bearer, x-api-key, or ?key=)

When requireApiKey is off, a loopback caller on the same machine may omit credentials. That skip is for CLI and MCP clients, which send no Origin. A request that carries an Origin which is not this server is refused even on loopback: the wrapper stamps trusted-peer headers on every request, including a browser's, and a page on a hostile origin would otherwise drive these tools from the victim's machine. Remote callers always authenticate, even when requireApiKey and requireLogin are off. Unauthenticated requests receive { error: "Unauthorized" } with HTTP 401.

A gateway key does not unlock this endpoint.

Do not point a browser page at this URL from another origin. Curl, Claude Code, and Cursor send no Origin and are the intended clients.

Choose a control tool

Read-only tools do not write DurinDoor state. Mutating tools do.

ToolModeInputReturns
list_providersreadnone{ providers: [{ id, alias, name, category, authType }] } from built-in AI_PROVIDERS
list_connectionsreadnone{ connections } with secrets stripped and connectionProxyUrl dropped
toggle_connection_activemutateconnectionId (string), isActive (boolean){ connection } sanitized. 404 if missing
toggle_provider_activemutateproviderId (string), isActive (boolean){ connections } for every row of that provider. 400 unknown provider, 404 no rows
usage_statsreadperiod enum today, 24h, 7d, 30d, 60d, 90d, 180d, 365d, all{ stats }
token_saver_statsreadsame period{ stats }
model_listreadoptional kinds array of llm, image, tts, embedding, stt, imageToText, rerank, video. Default [llm]{ models } OpenAI list shape
list_combosreadnone{ combos }
get_comboreadid{ combo }. 404 if missing
create_combomutatename required; optional models, members, kind, capabilities, allowedConnectionIds{ combo }
update_combomutateid required; other fields optional, omitted keep stored values{ combo }
delete_combomutateiddelete result
quota_snapshotsreadprovider and/or connectionId; optional includeStale boolean. At least one filter required{ snapshots }
refresh_quotamutateconnectionId{ result, snapshots }. 404 missing connection, 400 if the provider has no quota endpoint
list_api_keysreadnone{ keys } management view plus usage totals. Raw secret never returned
get_settingsreadnone{ settings } with secrets withheld
update_settingsmutatesettings object{ settings } after the patch

Empty kinds or an unknown kind is 400. Invalid period is 400. Combo name rules match POST /api/combos.

Protect credentials and security settings

Connection tools return sanitized records. apiKey, accessToken, refreshToken, idToken, OAuth cookies, and clientSecret never leave the tool. connectionProxyUrl is dropped, not redacted, because it can embed credentials in shapes new URL does not parse.

API-key lists contain masked secrets and usage totals, never raw keys.

get_settings drops password, passwordSessionEpoch, oidcClientSecret, and mitmSudoEncrypted, and redacts user:password@ on outboundProxyUrl.

update_settings strips secret keys and security-critical settings (exposeComboOnly, requireLogin, requireApiKey, authMode, OIDC fields, tunnelDashboardAccess, enableObservability, outbound-proxy fields), and connection-scoped claudeAutoPing / codexAutoPing. An API-key caller cannot change security-critical settings. A patch whose keys are all stripped returns 400 No updatable settings provided rather than a silent no-op.

Connect a client

Each snippet sends the DurinDoor API key, not a gateway key.

claude mcp add --transport http durindoor-control http://localhost:20128/api/mcp/control \
  --header "Authorization: Bearer YOUR_DURINDOOR_API_KEY"

Verify read and write access

First call list_combos or get_settings and verify the returned state. Then, when you intend a change, ask the connected MCP client:

  • List my combos and their members.
  • Create a fallback combo named coding-default with anthropic/claude-sonnet-4-5 then openai/gpt-4.1.
  • Refresh quota for connection CONNECTION_ID.

Those map to list_combos, create_combo (name plus models), and refresh_quota (connectionId).

Limits and failure modes

HTTP 401 from the gate means the key, session, or CLI token was missing or rejected, or a foreign Origin hit loopback. HTTP 403 on a nearby path such as /api/mcp/control-evil is the local-only plugin branch, not this handler.

JSON-RPC 404 is an unknown tool name or a missing combo/connection. JSON-RPC 400 is bad input or a provider without a quota endpoint. toggle_provider_active 400 is an unknown providerId; 404 means that provider has no connections.

Control tools do not mint API keys, reveal secrets, shut the process down, or switch the database engine. Use a dashboard session for those. Gateway tools/call usage rows use provider: "mcp-gateway"; control calls are not that surface.

On this page

Edit on GitHub