DEV Community

Rhumb
Rhumb

Posted on • Originally published at rhumb.dev

Multi-Tenant MCP Servers: One Server, Many Agents, Zero Credential Bleed

Multi-Tenant MCP Servers: One Server, Many Agents, Zero Credential Bleed

Dev.to tags: ai, mcp, programming, security
Canonical: https://rhumb.dev/blog/multi-tenant-mcp-server-design
Series: MCP Server Evaluation (companion to 034 + 036)


Introduction

The default answer to multi-tenant MCP is one server per tenant. It's simple. It avoids all the hard problems. It also creates sprawl that collapses under its own weight at any meaningful scale.

If you have 10 tenants, you have 10 server instances to deploy, monitor, rotate credentials for, and debug independently. At 100 tenants, this becomes an operational catastrophe. At 1,000, it's impossible.

The alternative is a single MCP server that safely isolates tenants from each other. This article covers how to build that — and specifically, what the failure modes are when you get it wrong.


Why Multi-Tenant MCP Is Harder Than Multi-Tenant HTTP

HTTP APIs have decades of multi-tenant patterns. Every cloud SaaS is multi-tenant. The patterns are well understood: authenticate per request, scope data access per auth token, don't share state between requests.

MCP has a different problem. The protocol surfaces tools, not endpoints. Tools have schemas. Schemas can be manipulated through prompt injection. And an MCP server that gets a crafted tool call from Agent A can sometimes be made to act on Agent B's context, resources, or credentials.

The blast radius of a multi-tenant failure in MCP isn't "wrong data returned." It's "Agent A used Agent B's API credentials to provision resources in B's account."

This is why the GitHub issue tracker for modelcontextprotocol/servers has an entire cluster around scope constraints, principal modeling, and tool-level authorization — it's a different security surface than HTTP.


The Three Isolation Layers You Need

Layer 1: Request-Level Credential Isolation

Every MCP tool call must be authenticated with credentials that belong to that call's principal, not to the server itself.

The wrong pattern:

Server credentials (long-lived, admin-scoped)
  → Shared across all tenant connections
  → Tenant A and Tenant B both use the same upstream API key
Enter fullscreen mode Exit fullscreen mode

The right pattern:

Per-connection credential lookup
  → Server receives auth context with each request
  → Resolves to per-tenant credentials at call time
  → Upstream API calls use tenant-scoped tokens
Enter fullscreen mode Exit fullscreen mode

Implementation: At connection time, validate a per-tenant bearer token. Store the resolved tenant identity in connection state. On every tool call, resolve credentials from that identity — not from server config.

This means the server never holds upstream credentials directly. It holds a credential resolver that maps tenant identity → scoped upstream tokens on demand.

Layer 2: Tool-Level Authorization

Not every tool should be available to every tenant. A multi-tenant MCP server where all tools are exposed to all connections collapses tenant isolation at the tool surface.

The pattern:

Tool manifest = f(tenant_id, tenant_permissions)
Enter fullscreen mode Exit fullscreen mode

When a new connection authenticates, resolve a tenant-specific tool manifest. Agent A's connection only sees tools A is allowed to call. Agent B's connection only sees B's tools.

This has two benefits:

  1. Explicit blast radius. A tenant can only call tools they're authorized for, regardless of what the server supports.
  2. Prompt injection resistance. A crafted prompt can't make an agent call tools that aren't in that agent's manifest.

Layer 3: Resource Scoping

Tool calls that touch external resources need to scope the resource identifiers to the tenant, not to the global namespace.

The failure mode: Agent A calls create_record(name="foo"), which creates a record with a globally unique ID. Agent B calls list_records() and can see A's record because the list has no tenant filter.

The fix: Every read and write operation applies a tenant scope at the data layer. This sounds obvious. It's routinely missed when you migrate from a single-tenant prototype (where there was no tenant concept) to a multi-tenant deployment.


The Four Failure Modes

1. Session State Bleed

MCP servers often maintain session state across calls. If that session state is keyed to the connection rather than the tenant identity, and if connections can be reused or pooled, you get state bleed.

The symptom: Agent A completes a workflow, leaves a session. Agent B reuses the connection and inherits A's state context.

The fix: Session state must be keyed to tenant identity, not connection. On connection close, explicitly invalidate session state. Never pool MCP connections across tenant boundaries.

2. Shared Upstream Rate Limits

If all tenants share upstream API keys, they share rate limits. Tenant A's agent running a high-volume batch at 2am consumes the rate limit budget that Tenant B's agent depends on for production workloads.

The symptom: Tenant B's agent starts getting 429s at 3am with no explanation. The server logs show total requests were under the limit — because A and B were tracked separately internally but shared the upstream key.

The fix: Per-tenant upstream credentials. If credential isolation is too complex for a given upstream provider, implement per-tenant request quotas enforced server-side before the upstream call.

3. Unscoped Tool Call Logs

Audit trails are required for most production deployments. If the log captures tool call inputs and outputs without tenant isolation, a log query or a log injection bug can expose Tenant A's data to Tenant B's operator.

The fix: Tag every log entry with tenant_id and enforce tenant-scoped log access at the query layer. Treat logs as a data surface with the same isolation requirements as the primary data store.

4. Prompt Injection Across Tenant Boundaries

The most dangerous multi-tenant MCP failure. An agent running in Tenant A's context processes a prompt that contains crafted tool instructions designed to extract Tenant B's data.

Example: A's agent processes a customer email (external, untrusted). The email contains instructions that reference another tenant's data. If the MCP server doesn't validate that the calling agent is authorized to supply tenant identifier overrides, A's agent executes a cross-tenant data access.

The fixes:

  1. Tool inputs must be validated against a schema that includes type + authorization constraints, not just type.
  2. Parameters that could override tenant context must be server-injected from the validated connection identity, never agent-supplied.
  3. Tool manifests must not surface parameters that agents can use to cross tenant boundaries.

How AN Score Data Shapes This

The APIs your MCP server calls on behalf of tenants have wildly different compatibility with per-tenant credential models.

High compatibility (AN Score 7.5+):

  • Stripe (8.1): Per-tenant API keys with explicit scope + read-only/write split. Rotation endpoints exist.
  • Anthropic (8.4): Per-org API keys with usage isolation. Separate keys per tenant is straightforward.
  • Twilio (8.0): Sub-account model for tenant isolation. Each tenant can have isolated credentials and rate limits.

Low compatibility (AN Score below 6.0):

  • HubSpot (4.6): Per-portal OAuth, but the OAuth flow requires human interaction. Programmatic per-tenant credentials require a private apps model that varies in scope support.
  • Salesforce (4.8): Per-org connectivity, but Connected App config is enterprise-gating. Tenant isolation requires separate org deployments or complex OAuth per-org flows.

The practical implication: if you're building a multi-tenant MCP server and your upstream APIs score below 6.0 on access readiness, expect 2-3x the per-tenant credential complexity you'd get with high-scoring providers. That's not a reason to avoid low-scoring providers — but it's a real implementation cost you should budget for.


Operational Checklist

Before you deploy a multi-tenant MCP server to production, verify:

  • [ ] Credential isolation: Each tenant connection uses tenant-scoped upstream credentials. The server holds zero global upstream keys.
  • [ ] Tool manifest isolation: Each connection receives a tenant-filtered tool list. Cross-tenant tools are not accessible.
  • [ ] Parameter injection: Tenant scope parameters are server-injected from auth context, never accepted from agent input.
  • [ ] Resource scoping: Every read and write applies tenant identity at the query layer. No shared resource namespaces.
  • [ ] Session invalidation: Session state is keyed to tenant identity. Connections are not pooled across tenants.
  • [ ] Rate limit isolation: Per-tenant upstream quotas are enforced before upstream calls. Shared rate limits are monitored per tenant.
  • [ ] Audit log isolation: All log entries include tenant identifier. Log access is scoped per tenant.
  • [ ] Prompt injection resistance: Tested with crafted inputs that attempt to supply tenant identifier overrides. Server rejects or ignores.

Summary

One-server-per-tenant is operationally unsustainable. Multi-tenant MCP is achievable, but requires isolation at four distinct layers: credentials, tools, resources, and session state.

The failure modes are not subtle. Credential bleed, shared rate limits, and cross-tenant data access are the acute risks. Prompt injection across tenant boundaries is the most severe — and the hardest to test for.

The upstream API dimension is real: high AN Score providers (Stripe, Anthropic, Twilio) have designed their credential and access models in ways that make per-tenant isolation tractable. Low-scoring providers push that complexity into your implementation layer.


Rhumb evaluates APIs on access readiness dimensions including scope support, rotation endpoints, and per-tenant isolation compatibility. Explore the AN Score methodology →

Related: A Production Readiness Checklist for Remote MCP Servers · Why Prompt Injection Hits Harder in MCP · The Complete Guide to API Selection for AI Agents

Top comments (0)