DEV Community

Jeremy Sebastian
Jeremy Sebastian

Posted on

One MCP server, five auth shapes: per-seat door credentials when a client can't send a header

Here's a problem I didn't expect to be the blocker.

I had three model seats — ChatGPT, Gemini, Copilot — already working against one MCP server (my Canonical Vault, which does no transport credential check; governance lives server-side in branch rules). Those surfaces connect URL-only.

My newer MCP server, the Commons, accepted exactly one presentation: Authorization: Bearer <token>.

Result: three credentialed seats, zero connections. Not a permissions problem. A presentation problem.

OAuth was the obvious textbook answer — and the wrong first move. The seats didn't need a new identity system. They needed a door shaped like the one they already walk through.

The pattern: door credentials

Ship a per-seat secret that can be presented in whatever shape the client can actually send, all resolving through one identity function:

Presentation Shape
Bearer (original) Authorization: Bearer <secret>
URL path POST /mcp/s/<secret>
URL query POST /mcp?k=<secret>
API key header X-Api-Key: <secret>
Basic password Authorization: Basic <user>:<secret>

Rules that make this safe enough to ship:

  1. Hash at rest, always. Doors are stored as SHA-256 hashes only (migration 003_door_credentials). The plaintext is shown once, at mint time, in a connection sheet the operator hands to that seat's surface.
  2. One identity function. Every shape — root token or door secret, any presentation — resolves through the same authenticate() path. No shape gets a weaker check. A bogus secret returns unauthenticated in every shape (verified live, per seat).
  3. Revocable independently. Admin gained issue-door / revoke-door. Revoking a seat's door does not touch its root token, and vice versa. I verified this on a throwaway door: revoked door → revoked, root token unaffected.
  4. High entropy, per seat. Each seat gets its own secret, so a leak is one seat's problem, with one seat's revocation — not a global rotation.
  5. Auth: None on the client side. In the seat's own surface you register the door URL as the server URL and set auth to None — the credential is in the URL, exactly like the URL-only servers these seats already use.

What shipped

Commons v0.5.0, in production:

  • Migration 003 applied to production Postgres
  • Auth resolves Bearer, Basic, X-Api-Key, /mcp/s/<secret>, and ?k= through the one identity function
  • Doors minted and verified live for chatgpt, gemini, and copilot: whoami through the door itself + every alternate shape, per seat
  • Test suite 46/46, typecheck clean, health endpoint reports 0.5.0

Remaining step is deliberately manual: add each door URL in that seat's own surface. Credentials cross into another vendor's UI only by the operator's hand.

When to use this — and when not to

Use door credentials when:

  • Your clients are heterogeneous agents/hosts, some of which genuinely cannot set headers
  • Each client is a distinct, named seat (not anonymous public traffic)
  • You can hash at rest, rate-limit, and revoke per seat

Don't use them when:

  • A real OAuth flow is available to all your clients — prefer it; it's the front door, this is the side door
  • Secrets in URLs would land in logs you don't control without redaction — path segments do end up in access logs; treat those logs as credential-adjacent
  • You need user-delegated, scoped consent rather than seat identity

I demoted OAuth and a REST shim to optional hardening, in that order. The unblocking move was meeting the clients at the presentation they already had — without weakening the one rule that matters: identity is a credential, never a claim, and the server derives it the same way no matter which door you came through.


Part of the "Building Governed Agents" series. Previous: "The Commons: Claims, Never Commands." Code context: the Commons is an append-only MCP pool where models exchange claims under governance — source currently in a private repo, protocol described in the series. Paper: doi.org/10.5281/zenodo.23242126 · Writing: liminaljermo.substack.com

Hit the same wall with an MCP client that can't send headers? I want to hear which shape you shipped.

Top comments (0)