Skilllibrary mcp-multi-tenant-design
Design MCP servers that serve multiple tenants or users with proper isolation, session management, and access control. Use when building shared MCP services, implementing per-user session state on Streamable HTTP, designing tenant-scoped tool access, or rate-limiting per-tenant usage.
git clone https://github.com/merceralex397-collab/skilllibrary
T=$(mktemp -d) && git clone --depth=1 https://github.com/merceralex397-collab/skilllibrary "$T" && mkdir -p ~/.claude/skills && cp -r "$T/07-mcp/mcp-multi-tenant-design" ~/.claude/skills/merceralex397-collab-skilllibrary-mcp-multi-tenant-design && rm -rf "$T"
07-mcp/mcp-multi-tenant-design/SKILL.mdPurpose
Design MCP servers that safely handle multiple tenants or users simultaneously. Covers session isolation, per-tenant auth, state partitioning, rate limiting, and the specific MCP session management mechanisms that enable multi-tenant operation.
When to use this skill
- Building a shared/hosted MCP server serving multiple organizations or users
- Implementing session isolation for Streamable HTTP MCP servers
- Designing per-tenant tool access or data scoping
- Adding rate limiting or usage quotas to an MCP server
- Scaling an MCP server from single-user to multi-user
Do not use this skill when
- Building a single-user local MCP server via stdio → use
mcp-development - Working on auth flow implementation → use
mcp-auth-transports - The server is project-local with one user
Architecture context
stdio servers are inherently single-tenant — one client process, one server process, direct IPC. Multi-tenancy only applies to Streamable HTTP servers.
Streamable HTTP servers can serve many clients. Each client gets its own session via
Mcp-Session-Id. The server must isolate state, auth, and resources between sessions.
Operating procedure
Phase 1 — Session isolation
MCP's session management for Streamable HTTP:
- Session creation: Server generates a session ID during
and returns it in theinitialize
response header.Mcp-Session-Id - Session binding: Client includes
on all subsequent requests.Mcp-Session-Id - Session termination: Client sends HTTP DELETE; server cleans up session state.
- Session expiry: Server returns 404 for expired sessions; client must re-initialize.
Implementation pattern:
const sessions = new Map<string, SessionState>(); function handleInitialize(req, res) { const sessionId = crypto.randomUUID(); sessions.set(sessionId, { tenantId: extractTenantFromToken(req), credentials: extractCredentials(req), createdAt: Date.now(), }); res.setHeader("Mcp-Session-Id", sessionId); // ... return initialize response } function handleToolCall(req, res) { const sessionId = req.headers["mcp-session-id"]; const session = sessions.get(sessionId); if (!session) { return res.status(404).json({ error: "Session expired" }); } // Use session.tenantId to scope all data access }
Phase 2 — Tenant-scoped data access
Every tool handler must scope data access to the current tenant:
- Database queries: Always include
or equivalentWHERE tenant_id = ? - API calls: Use the tenant's credentials, not shared credentials
- File access: Restrict to tenant-specific directories
- Resource URIs: Prefix with tenant context (e.g.,
)tenant://acme/resource
Anti-pattern: Shared mutable state across sessions. Each session's tool calls must be independent.
Phase 3 — Per-tenant tool surface
Not all tenants may see the same tools. Use dynamic
tools/list:
server.setRequestHandler("tools/list", async (request, context) => { const session = getSession(context); const permissions = await getPermissions(session.tenantId); const allTools = getRegisteredTools(); return { tools: allTools.filter(t => permissions.includes(t.name)) }; });
When a tenant's tool access changes, send
notifications/tools/list_changed to their active sessions.
Phase 4 — Rate limiting and quotas
Per-tenant rate limiting prevents one tenant from starving others:
- Track
count per session/tenant per time windowtools/call - Return MCP error with helpful message when limit exceeded:
{ "isError": true, "content": [{ "type": "text", "text": "Rate limit exceeded: 100 calls/minute. Retry after 32 seconds." }] } - Consider separate limits for read-only vs mutating tools
Phase 5 — Scaling
- Horizontal scaling: Use external session store (Redis, database) instead of in-memory Map
- Sticky sessions: If using in-memory state, route by
hashMcp-Session-Id - Connection limits: Cap concurrent SSE connections per tenant
- Graceful shutdown: Drain active sessions before stopping server
Decision rules
- Session ID must be cryptographically random (UUID v4 or crypto.randomUUID())
- Never trust client-provided tenant identity — derive it from the OAuth token
- Tool handlers must not share mutable state across sessions
- Use
for session affinity, not cookies or IP-based routingMcp-Session-Id - Set session TTL and clean up expired sessions proactively
Output requirements
- Session management implementation with per-tenant isolation
- Tenant-scoped data access in all tool handlers
- Rate limiting per tenant
- Scalability plan (session store, connection limits)
Related skills
— OAuth 2.1 flow that provides tenant identitymcp-auth-transports
— permission models for tool accessmcp-security-permissions
— general server development lifecyclemcp-development
Failure handling
- If session store fails, return 503 to clients rather than serving unscoped requests
- If tenant identity cannot be extracted from token, reject the request at initialize
- If a session's OAuth token expires, return 401 with guidance to re-authenticate