DurinDoor
Features

MCP gateway

One JSON-RPC endpoint in front of registered MCP servers, with gateway keys, grants, and OAuth.

The MCP gateway merges tools from one or more upstream MCP instances into a single JSON-RPC namespace. Each caller uses a dedicated gateway key granted to specific instances (and, on the backend, to specific tools). DurinDoor can attach an OAuth access token to upstream requests for an instance.

Open Dashboard → MCP Gateway → Instances (/dashboard/mcp-gateway) to register servers. Open Dashboard → MCP Gateway → Keys (/dashboard/mcp-gateway/keys) to mint keys. Client configuration is under Connect a client below.

Register and test an upstream server

New instance includes a preset selector for commonly used MCP servers. A preset only fills editor fields. It never saves, installs a package, starts a process, enables an instance, grants a gateway key, or opens OAuth. Review fields, add required local configuration, save, then use Test. Enable it and grant a gateway key only after a successful test.

PresetTransport and exact configurationPrerequisite / official source
Z.AI Web SearchHTTP https://api.z.ai/api/mcp/web_search_prime/mcpSaved Z.AI provider connection ID, whose stored key supplies authentication. Z.AI docs
GitHubstdio: docker run -i --rm -e GITHUB_PERSONAL_ACCESS_TOKEN ghcr.io/github/github-mcp-serverDocker plus least-privilege GITHUB_PERSONAL_ACCESS_TOKEN in Env JSON. GitHub MCP Server
Context7HTTP https://mcp.context7.com/mcpPublic access. Context7 API-key mode uses Authorization, which gateway intentionally blocks in instance headers. Context7 installation
Playwrightstdio: npx -y @playwright/mcp@latestNode.js 18+ and Playwright browser dependencies on gateway host. Playwright MCP
Filesystemstdio: npx -y @modelcontextprotocol/server-filesystem followed by approved absolute directoriesEvery path must be an explicit non-root Unix path. Saving without valid paths is blocked, including root aliases such as /tmp/... Filesystem server
Memorystdio: npx -y @modelcontextprotocol/server-memoryReview host-local persistence policy. Memory server
Sequential Thinkingstdio: npx -y @modelcontextprotocol/server-sequential-thinkingNo credentials. Sequential Thinking
Fetchstdio: uvx mcp-server-fetchInstall uv on gateway host. Fetch server
Brave Searchstdio: npx -y @brave/brave-search-mcp-serverSet BRAVE_API_KEY in Env JSON; saving without it is blocked. Brave Search MCP Server
FirecrawlHTTP https://mcp.firecrawl.dev/v2/mcpRate-limited keyless access only; the preset does not connect your account. Firecrawl keyless MCP
NotionHTTP https://mcp.notion.com/mcp, Requires OAuthSave, then click Login. A public HTTPS callback is required. Notion MCP
LinearHTTP https://mcp.linear.app/mcp, Requires OAuthSave, then click Login. A public HTTPS callback is required. Linear MCP

For Firecrawl account access, use Custom, set the URL to https://mcp.firecrawl.dev/v2/mcp-oauth, and turn on Requires OAuth before saving and logging in. See Firecrawl sign-in setup. This hosted MCP connection is separate from the local Firecrawl REST service used by web-fetch routing.

Selecting another preset fully replaces URL, command, args, env, headers, OAuth, and provider connection reference. It preserves only enabled state. Select Custom to enter or retain manual configuration; editing an existing instance stays custom and never rewrites it.

Secrets belong only in Env JSON for local stdio servers. Use no secret-like placeholder values: leave each required key absent until you can supply it. Instance headers cannot carry Authorization, cookies, proxy authorization, or mcp-* headers. This is deliberate: use supported OAuth, an upstream public/keyless mode, or an allowed local environment variable instead.

After testing, create or select a gateway key, grant only intended instances in Gateway keys, and point the client at the DurinDoor gateway URL. Presets do not grant clients access. Limit filesystem paths, use least-privilege provider credentials, and review which tools each key's grants permit.

Choose a client transport

Both share the same JSON-RPC handler and the same key check.

RouteMethodRole
/api/mcp-gatewayPOSTStreamable HTTP. One JSON-RPC request per POST. Notifications (no id) return 202.
/api/mcp-gateway/sseGETSSE handshake. Returns a message URL for this session.
/api/mcp-gateway/message?sessionId=POSTSSE companion. Missing sessionId is 400.

GET /api/mcp-gateway returns 405 with Allow: POST. Clients that send Accept: text/event-stream still get JSON on the POST path.

Stdio gateway bridge for cowork plugins only spawns registered bridge commands, never a command supplied by a client request. Separately, registered stdio instances (npx, python, docker, command kinds) run their operator-saved command and arguments on the DurinDoor host. Both are server-side configuration, not client-controlled execution.

Tool names are <instanceSlug>__<toolName>. tools/call splits on the first __. Disabled instances are omitted from tools/list and dispatch.

Create a gateway key and grant access

Gateway keys and dashboard API keys are separate credentials. A DurinDoor API key does not authenticate against the gateway, and a gateway key does not authenticate against /v1 or the management API.

Gateway keys are separate secrets; they are not dashboard session cookies.

Send the secret as Authorization: Bearer …, x-api-key, x-goog-api-key, or ?key=. A missing or inactive key returns JSON-RPC error gateway key missing or gateway key invalid with HTTP 401.

POST /api/mcp-gateway/keys and GET /api/mcp-gateway/keys/<id>/reveal are local-request only (403 otherwise). The list endpoint never returns the raw secret. Create returns it once. Reveal exists for a signed-in local dashboard session, same as DurinDoor API keys.

Grant the key to instances from the Keys page. Clients see tools from enabled, granted instances, filtered by any per-tool allow-list on the grant. Removing a grant revokes access right away. An empty grant list means the key authenticates but sees no tools.

The control endpoint POST /api/mcp/control does not accept a gateway key. It takes a dashboard session, the CLI token, or a DurinDoor API key. Full tool list and client setup: MCP control.

Manage instances through the API

GET/POST /api/mcp-gateway/instances lists and creates. GET/PUT/DELETE /api/mcp-gateway/instances/:id reads, updates, and removes one row. POST /api/mcp-gateway/instances/:id/test lists upstream tools as a probe.

An instance has a kind (http, sse, npx, python, docker, or command), a transport (HTTP, SSE, or stdio), an upstream URL or a command with args and env, optional fixed headers, and an optional OAuth flag. Enable it before you expect it in tools/list.

Sign in to an OAuth server

Turn on Requires OAuth for an upstream server that uses the authorization-code flow, then click Login on the instance. The flow has four stages:

  • Discovery. DurinDoor reads the server's WWW-Authenticate challenge, then the .well-known protected-resource and authorization-server metadata.
  • Registration. Dynamic Client Registration against the server's registration_endpoint, or a Client ID Metadata Document when the server supports it.
  • Login. DurinDoor opens the provider's consent page with PKCE and a resource indicator, then exchanges the code and stores the tokens on the instance.
  • Refresh. Tokens refresh on their own before expiry and after a 401. If the refresh token is rejected for good, the instance shows needs login. Click Login again to re-authorize.

OAuth needs a callback the upstream server can reach, so it works through a configured tunnel, Tailscale, or a public URL, not on a loopback-only host.

When oauth: true, the dashboard starts the flow at /api/mcp-gateway/oauth/:id/authorize. The callback is /api/mcp-gateway/oauth/:id/callback. Poll /api/mcp-gateway/oauth/:id/status. The upstream authorization server fetches /api/mcp-gateway/oauth/:id/client-metadata as the Client ID Metadata Document.

Auth for these four leaves differs by who calls them. authorize and status are operator actions on the management gate, so a dashboard session, the CLI token, or a DurinDoor API key all work; an API-key client can run a complete connect flow. client-metadata is public, because the upstream authorization server fetches it server-to-server. callback keeps the dashboard login policy: it is an upstream browser redirect carrying only code and state, so it has no DurinDoor credential to present, and its CSRF defense is the server-side state it validates.

The public origin for redirects is MCP_GATEWAY_OAUTH_PUBLIC_URL, then OAUTH_PUBLIC_BASE_URL, then an active tunnel, then forwarded proto/host. Loopback only works if that env override is set. A missing public HTTPS origin returns an error telling you to set MCP_GATEWAY_OAUTH_PUBLIC_URL or use the Cloudflare tunnel.

OAuth tokens stay on the server. Management APIs report needsReauth instead of returning tokens. Refresh uses a 60-second leeway. A 401 from an OAuth instance force-refreshes once and retries. Non-OAuth instances do not retry 401s. Concurrent refresh attempts share the same refresh.

Token endpoints and refresh redirects go through the outbound URL guard. Cross-origin redirects drop Authorization, Cookie, and Proxy-Authorization.

Choose gateway or control tools

POST /api/mcp/control is a separate JSON-RPC server for the running DurinDoor instance (combos, connections, settings, quota). Gateway callers stay on /api/mcp-gateway with a gateway key. Auth, every tool, client setup, and limits: MCP control. REST families and exclusions: Management API.

Check tool usage

Every tools/call, ok or error, writes a usage row with provider: "mcp-gateway" and model set to the namespaced tool name. Watch it on Dashboard → Usage.

Connect a client

Point any streamable-HTTP MCP client at the gateway. Replace the host with your DurinDoor base URL and the key with a gateway key from MCP Gateway → Keys.

{
  "mcpServers": {
    "durindoor": {
      "url": "http://localhost:20128/api/mcp-gateway",
      "headers": { "Authorization": "Bearer YOUR_GATEWAY_KEY" }
    }
  }
}

Verify the gateway

  1. Create an instance, enable it, run Test. You should see a tool count.
  2. Create a gateway key and grant that instance.
  3. From the same machine:
curl http://localhost:20128/api/mcp-gateway \
  -H "Authorization: Bearer YOUR_GATEWAY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'

Then tools/list with the same key. Call a tool as "name": "<slug>__<toolName>". A disabled instance or a key without that grant returns unknown tool.

Troubleshooting

  • 401 from the gateway. The gateway key is missing, invalid, or disabled. Send a gateway key, not a DurinDoor API key, as a Bearer token.
  • A tool is missing. Its instance is disabled or not granted to your key.
  • Instance shows needs login. The upstream OAuth token expired and could not refresh. Click Login again.
  • URL not allowed. The outbound URL guard rejected the upstream URL, for example an address prohibited by the outbound policy.
  • Stdio spawn failed. The command is not on the local plugin allowlist, or the process exited.

On this page

Edit on GitHub