Design a multi-tenant remote MCP server that exposes each customer's Postgres database. Walk through transport, auth, isolation, and observability.
Streamable HTTP, OAuth 2.1 with tenant_id claim, per-tenant DB credentials and async contexts, token-bucket rate limits per (tenant, tool), OTEL spans tagged per tenant. MCP provides none of this.
Imagine a building where many companies share one management office. Each company has its own locked room with its own key, its own water meter, and its own usage cap. The front desk checks every visitor's badge before letting them in, writes their name on a log with the company they belong to, and only lets them visit their own room. If one company starts running loud equipment that slows the elevator for everyone, the building manager notices because each floor has its own speed meter. Some situations force a remodel: one company wants a custom lobby sign that confuses the directory, another company's government says their mail must stay in a different building, and sometimes a visitor's task takes so long the front desk forgets they came in. A multi-tenant MCP server is the same building with code in place of doors and meters.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
Concept explanation~2 min read
Everything you need to truly understand this topic: intuition, mechanics, step by step explanation, code, formulas, and worked example. Click to expand.
A multi-tenant remote MCP server that exposes each customer's Postgres database sits at the intersection of two high-risk properties: untrusted LLM tool calls touching a sensitive data store, and multiple tenants sharing infrastructure on a protocol that deliberately provides no isolation, sandboxing, or rate limiting.
The design requires six load-bearing layers. Transport is streamable HTTP because stdio cannot serve remote clients. Auth is OAuth 2.1 with PKCE plus a tenant_id claim on the access token, validated on every request. Isolation is per-tenant Postgres credentials, per-tenant connection pools, and per-tenant async execution contexts. Rate limiting is a token bucket keyed on (tenant_id, tool_name) with JSON-RPC error -32000 plus retry_after on rejection. Observability is two separate OpenTelemetry spans (JSON-RPC handler and Postgres call), both tagged with tenant_id and request_id. And a set of failure modes that will force redesign: dynamic per-tenant tool descriptions defeating client caching, data residency requiring regional splits, and long-running tools exceeding host timeouts.
This walkthrough covers each layer, names the anti-patterns each one prevents, and addresses two protocol-level gaps (rug-pull on tool definitions, prompt injection from query results) that the spec leaves entirely to the server implementer.
Transport and auth: streamable HTTP plus OAuth 2.1 with tenant binding
MCP supports two transport families: stdio and HTTP. Stdio is process-local, meaning the host spawns the server as a subprocess on the same machine. It cannot serve remote clients over a network. For a multi-tenant remote server, streamable HTTP is the only option. The 2025 spec's streamable HTTP profile is production-relevant in 2026 because it allows long tool calls to push incremental chunks back to the host instead of holding the connection silent until completion. This matters operationally: hosts have silent timeouts you cannot control, and a 90 second Postgres query without progress output can be killed mid-execution by the host.
Auth is where most multi-tenant MCP designs fail early. The correct baseline is OAuth 2.1 with PKCE for human users and bearer tokens for service to service. The access token must carry a tenant_id claim. Every JSON-RPC request validates four things: token signature, token expiry, scope match for the requested tool, and tenant binding between the token's claim and the request's tenant identifier (URL path or header).
Three anti-patterns produce real incidents. Bearer-only auth without a tenant claim means a leaked token authorizes access to whichever tenant the attacker can guess. Long-lived tokens without refresh stay valid for months after compromise. Tenant inferred from the URL path without token validation means any caller can claim any tenant by changing the path. The OAuth 2.1 plus PKCE plus tenant-claim baseline eliminates all three.
Per-tool OAuth scopes are the second essential discipline. Each tool should declare the scope it requires (tables:read, tables:write, schema:read), and the server should reject calls where the token lacks the needed scope. Without this, every token becomes a universal credential for everything the tenant can do.
Situations where this technique stops working.
2–4 min · Everything important, quickly.
Real products, models, and research that use this idea.
- Cloudflare Workers MCP runs remote servers at the edge with OAuth 2.1 auth and per-tenant isolation enforced by the Workers runtime, serving as the reference architecture for streamable HTTP deployment in 2026.
- Vercel's MCP integration ships OAuth 2.1 with PKCE as the default for human users and bearer tokens for server to server, matching the spec's resource-server design.
What an interviewer would ask next. Try answering before peeking at the approach.
QHow would you handle a tenant that needs custom tool descriptions when most MCP clients aggressively cache tools/list?
Three options with tradeoffs. (a) Static tool superset for every tenant, with per-tenant gating that returns a permission error at call time. Works with every host cache but loses per-tenant naming. (b) Per-tenant URL endpoint so each tenant's cache is correctly partitioned, at the cost of operational fan-out. (c) Push a tools/list_changed notification on the connection, effective only for subscribing clients. Most production deployments start with (a) and add (b) for high-value enterprise tenants.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Red flags & common mistakes
The phrases that signal junior thinking. Click to expand.
Sharing a single Postgres superuser across tenants and scoping with SET ROLE at runtime. One SQL injection in any tool reads every tenant's data, collapsing the isolation model entirely.
60 second bullets to scan on the way to the call.
Name streamable HTTP as the transport and explain why stdio cannot serve remote multi-tenant traffic.
Specify OAuth 2.1 with PKCE for human users, bearer for services, with a tenant_id claim validated on every request.
Primary sources. Browse if you want the original framing.
Same topic, related formats. Practice these next.