MCPlex internals · 8 min read

The Auth Relay

How one OAuth token to your MCPlex account becomes authenticated calls to every MCP server you subscribe to — without either side ever seeing the other's secrets.

01 The problem

You've registered a handful of MCP servers — PostHog, a memory server, whatever's next. Each one wants its own credential: an API key here, an OAuth dance there. Your MCP client (Claude, or any other) would need to hold all of them, and every new client you connect from — desktop, web, a CI job — needs them all again.

MCPlex flips this: the client authenticates once, to MCPlex, with standard MCP OAuth 2.1. The upstream credentials live server-side, under your account, and MCPlex replays them on your behalf.

The analogy

MCPlex is a hotel concierge. You show your room key once at the desk. The concierge holds the keys to every service door in the building — and never hands them to you. You ask for things; doors open; the keyring stays on the concierge's belt.

02 The big idea: two auth worlds, one relay

There are two completely separate authentication systems in play, and the whole design is that they only meet as data — a userId — never as shared secrets:

Inbound: MCP OAuth 2.1. Your client gets a Bearer token scoped to your MCPlex account (24 h TTL, PKCEProof Key for Code Exchange — the client proves it's the same app that started the flow, so intercepted authorization codes are useless., dynamic client registrationRFC 7591: the MCP client registers itself at /oauth/register automatically — no manual "create an app" step.).
Outbound: per-server credentials — an AES-GCM-encrypted API key in KV, or an upstream OAuth token living inside your personal Durable ObjectA single-instance, stateful Cloudflare Workers object with its own private SQLite storage. MCPlex creates exactly one per user, named user-<your id>..

// the relay, end to end

Bearer (OAuth 2.1) DO RPC (no bearer) API key upstream OAuth YOUR MCP CLIENT MCPLEX WORKER gate · owner check YOUR AGENT DO user-<userId> UPSTREAM · POSTHOG sees only its own key UPSTREAM · MEMORY sees only its own token
The invariant

Your inbound Bearer token never crosses to an upstream server, and upstream credentials never reach your client. Each hop swaps the credential for the next one. That's the relay.

03 Watch one tool call travel

Step through a real request — POST /prasham/bundles/analytics/mcp — and watch which credential is live at each hop. The diagram above lights up as you go.

Press Next to begin.

04 The front door: try to get in

The mcpAuthMiddleware guards every bundle endpoint. It unwraps your Bearer token, recovers the grant props{ userId, userSlug } — baked into the grant at consent time, stored encrypted. The access token itself wraps the decryption key, so props are only readable by presenting the token., and the proxy then checks you actually own the bundle in the URL. Fire each request and read the response:

// POST /prasham/bundles/analytics/mcp

← pick a request above
Where tokens live

The OAuth library stores everything in KV under prefixes — client:, grant:, token: — in the same physical namespace as MCPlex's own apikey: entries. Two bindings, one namespace, zero collisions. Revoke any client app from the /tokens page and its token 401s on next use.

05 The vault: what MCPlex holds for you

Outbound credentials take one of two shapes, and neither ever touches D1 in plaintext:

API keys are AES-GCM encrypted with a server-side key and stored in KV as apikey:<serverId>. At call time they're decrypted and sent upstream as Authorization: Bearer ….
Upstream OAuth is delegated to the agents SDK: you approve the upstream server in a popup once, and the resulting access + refresh tokens live in your Durable Object's private SQLite — surviving hibernation, isolated per user, refreshed automatically.

This demo runs real AES-GCM in your browser (same algorithm, same 12-byte IV prefix format as src/lib/encryption.ts). Type a fake key and encrypt it:

// the API-key path, live

← what lands in KV appears here
Gotcha

Encrypt the same key twice — the blob changes every time. That's the random per-write IV (purple prefix) doing its job: identical secrets never produce identical ciphertext, so KV contents leak nothing about key reuse.

06 Check yourself

// q1 · what does the upstream server see?

When your tool call reaches PostHog through MCPlex, which credential does PostHog receive?

Right — the relay swaps credentials at the Durable Object hop. The inbound Bearer token is validated and dropped; only the upstream's own credential travels the last leg. PostHog never learns MCPlex exists as an auth party.

// q2 · the borrowed token

Your friend has a valid MCPlex token for their account and calls /prasham/bundles/analytics/mcp with it. What happens?

The token unwraps fine (it's real), so no 401. But the proxy resolves the bundle owner from the URL slug and compares: mcpUserId !== owner.id403 Forbidden. Bundles are single-owner; a token only opens its own account's doors.

07 Recap

The one sentence

Your client holds one OAuth 2.1 token that proves who you are to MCPlex; everything after the owner check runs on credentials MCPlex keeps for you — encrypted keys in KV, delegated OAuth tokens in your own Durable Object — and the two sides never exchange secrets, only your userId.

One token in. Many credentials out. Nothing shared across the seam — that's the auth relay.