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.
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.
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.
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>..
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.
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.
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:
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.
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:
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.
When your tool call reaches PostHog through MCPlex, which credential does PostHog receive?
Your friend has a valid MCPlex token for their account and calls /prasham/bundles/analytics/mcp with it. What happens?
mcpUserId !== owner.id → 403 Forbidden. Bundles are single-owner; a token only opens its own account's doors.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.