Skip to content

Commit 03b98e1

Browse files
committed
refactor: add multi-tenancy TODO comments across MCP codebase
- Added comprehensive TODO comments identifying areas requiring tenant isolation (auth tokens, sessions, managers, request contexts) - Documented shared state concerns in tool, resource, and prompt managers that need tenant scoping - Noted missing tenant_id fields in core models (AccessToken, AuthorizationCode, RefreshToken, RequestContext, ServerSession, ClientSession) - Identified need for test coverage of multi-tenant scenarios an
1 parent 9eae96a commit 03b98e1

File tree

12 files changed

+67
-0
lines changed

12 files changed

+67
-0
lines changed

src/mcp/client/session.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,9 @@ class ClientSession(
108108
types.ServerNotification,
109109
]
110110
):
111+
# TODO: Multi-tenancy - ClientSession does not track or send tenant_id. Need to add tenant_id
112+
# field and pass it to the server during initialization and with each request (either in request
113+
# metadata or as a header). This allows the server to process requests in the correct tenant context.
111114
def __init__(
112115
self,
113116
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
@@ -137,6 +140,9 @@ def __init__(
137140
self._server_capabilities: types.ServerCapabilities | None = None
138141

139142
async def initialize(self) -> types.InitializeResult:
143+
# TODO: Multi-tenancy - Client initialization does not send tenant_id to server.
144+
# Need to add tenant_id parameter to initialize() and include it in InitializeRequestParams
145+
# or as part of clientInfo metadata. Server must know which tenant context to use.
140146
sampling = types.SamplingCapability() if self._sampling_callback is not _default_sampling_callback else None
141147
elicitation = (
142148
types.ElicitationCapability() if self._elicitation_callback is not _default_elicitation_callback else None

src/mcp/server/auth/middleware/auth_context.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ def get_access_token() -> AccessToken | None:
1717
Returns:
1818
The access token if an authenticated user is available, None otherwise.
1919
"""
20+
# TODO: Multi-tenancy - Need to add get_tenant_id() helper function that extracts
21+
# tenant_id from the AccessToken in the current auth context. All data access operations
22+
# should use this tenant_id to scope queries and prevent cross-tenant data access.
2023
auth_user = auth_context_var.get()
2124
return auth_user.access_token if auth_user else None
2225

src/mcp/server/auth/provider.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ class AuthorizationParams(BaseModel):
1717

1818

1919
class AuthorizationCode(BaseModel):
20+
# TODO: Multi-tenancy - AuthorizationCode lacks tenant_id field. Need to add tenant_id
21+
# to track which tenant this authorization code belongs to, ensuring codes cannot be
22+
# exchanged across tenant boundaries.
2023
code: str
2124
scopes: list[str]
2225
expires_at: float
@@ -28,13 +31,19 @@ class AuthorizationCode(BaseModel):
2831

2932

3033
class RefreshToken(BaseModel):
34+
# TODO: Multi-tenancy - RefreshToken lacks tenant_id field. Need to add tenant_id
35+
# to ensure refresh tokens are scoped to specific tenants and cannot be used to
36+
# obtain access tokens for other tenants.
3137
token: str
3238
client_id: str
3339
scopes: list[str]
3440
expires_at: int | None = None
3541

3642

3743
class AccessToken(BaseModel):
44+
# TODO: Multi-tenancy - AccessToken lacks tenant_id field. This is critical - need to add
45+
# tenant_id to ensure access tokens are scoped to specific tenants. All resource/tool/prompt
46+
# access should be filtered by the tenant_id from the access token to prevent cross-tenant access.
3847
token: str
3948
client_id: str
4049
scopes: list[str]

src/mcp/server/fastmcp/prompts/manager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ class PromptManager:
1919
"""Manages FastMCP prompts."""
2020

2121
def __init__(self, warn_on_duplicate_prompts: bool = True):
22+
# TODO: Multi-tenancy - Prompts are stored in a shared dictionary without tenant scoping.
23+
# Need to either: (1) add tenant_id parameter to all methods and scope storage by tenant
24+
# (e.g., dict[tuple[tenant_id, prompt_name], Prompt]), or (2) create separate PromptManager
25+
# instances per tenant. Prompts registered by one tenant should not be accessible to others.
2226
self._prompts: dict[str, Prompt] = {}
2327
self.warn_on_duplicate_prompts = warn_on_duplicate_prompts
2428

src/mcp/server/fastmcp/resources/resource_manager.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ class ResourceManager:
2424
"""Manages FastMCP resources."""
2525

2626
def __init__(self, warn_on_duplicate_resources: bool = True):
27+
# TODO: Multi-tenancy - Resources and templates are stored in shared dictionaries
28+
# without tenant scoping. Need to either: (1) add tenant_id parameter to all methods
29+
# and scope storage by tenant (e.g., dict[tuple[tenant_id, uri], Resource]), or
30+
# (2) create separate ResourceManager instances per tenant. Resources registered by
31+
# one tenant should not be accessible to other tenants.
2732
self._resources: dict[str, Resource] = {}
2833
self._templates: dict[str, ResourceTemplate] = {}
2934
self.warn_on_duplicate_resources = warn_on_duplicate_resources

src/mcp/server/fastmcp/server.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ class Settings(BaseSettings, Generic[LifespanResultT]):
7878
For example, FASTMCP_DEBUG=true will set debug=True.
7979
"""
8080

81+
# TODO: Multi-tenancy - Settings are loaded from environment variables without tenant scoping.
82+
# For multi-tenant deployments, need to support tenant-specific configuration overrides,
83+
# potentially loading from tenant-scoped config sources (e.g., database, per-tenant env files).
8184
model_config = SettingsConfigDict(
8285
env_prefix="FASTMCP_",
8386
env_file=".env",
@@ -198,6 +201,10 @@ def __init__( # noqa: PLR0913
198201
# We need to create a Lifespan type that is a generic on the server type, like Starlette does.
199202
lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan), # type: ignore
200203
)
204+
# TODO: Multi-tenancy - These managers maintain shared state across all tenants.
205+
# Need to either: (1) make managers tenant-aware by accepting tenant_id in all operations,
206+
# or (2) create separate manager instances per tenant with tenant-scoped storage.
207+
# Tools, resources, and prompts registered should be scoped to tenant context.
201208
self._tool_manager = ToolManager(tools=tools, warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools)
202209
self._resource_manager = ResourceManager(warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources)
203210
self._prompt_manager = PromptManager(warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts)
@@ -210,15 +217,23 @@ def __init__( # noqa: PLR0913
210217
elif auth_server_provider or token_verifier:
211218
raise ValueError("Cannot specify auth_server_provider or token_verifier without auth settings")
212219

220+
# TODO: Multi-tenancy - Auth providers and token verifiers are shared across all tenants.
221+
# Need to support tenant-specific auth configurations, or at minimum ensure tokens
222+
# include tenant_id claims that are validated. AccessToken should include tenant_id field.
213223
self._auth_server_provider = auth_server_provider
214224
self._token_verifier = token_verifier
215225

216226
# Create token verifier from provider if needed (backwards compatibility)
217227
if auth_server_provider and not token_verifier:
218228
self._token_verifier = ProviderTokenVerifier(auth_server_provider)
229+
# TODO: Multi-tenancy - Event store is shared across tenants. Events should be
230+
# scoped by tenant_id to prevent cross-tenant data leakage in resumable sessions.
219231
self._event_store = event_store
220232
self._custom_starlette_routes: list[Route] = []
221233
self.dependencies = self.settings.dependencies
234+
# TODO: Multi-tenancy - Session manager tracks sessions globally without tenant scoping.
235+
# Sessions should be partitioned by tenant_id to isolate tenant data and prevent
236+
# cross-tenant session access. Consider tenant_id as part of session key.
222237
self._session_manager: StreamableHTTPSessionManager | None = None
223238

224239
# Set up MCP protocol handlers

src/mcp/server/fastmcp/tools/tool_manager.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ def __init__(
2525
*,
2626
tools: list[Tool] | None = None,
2727
):
28+
# TODO: Multi-tenancy - Tools are stored in a shared dictionary without tenant scoping.
29+
# Need to either: (1) add tenant_id parameter to all methods and scope storage by tenant
30+
# (e.g., dict[tuple[tenant_id, tool_name], Tool]), or (2) create separate ToolManager
31+
# instances per tenant. Tools registered by one tenant should not be accessible to others.
2832
self._tools: dict[str, Tool] = {}
2933
if tools is not None:
3034
for tool in tools:

src/mcp/server/lowlevel/server.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,15 @@ def __init__(
149149
self.website_url = website_url
150150
self.icons = icons
151151
self.lifespan = lifespan
152+
# TODO: Multi-tenancy - Request handlers are shared across all tenants. Handlers should
153+
# receive tenant context and scope their operations accordingly. Consider tenant_id as part
154+
# of the request context to enable tenant-aware data access.
152155
self.request_handlers: dict[type, Callable[..., Awaitable[types.ServerResult]]] = {
153156
types.PingRequest: _ping_handler,
154157
}
155158
self.notification_handlers: dict[type, Callable[..., Awaitable[None]]] = {}
159+
# TODO: Multi-tenancy - Tool cache is shared across all tenants. Need to scope cache by
160+
# tenant_id or maintain separate caches per tenant to prevent cross-tenant data leakage.
156161
self._tool_cache: dict[str, types.Tool] = {}
157162
logger.debug("Initializing server %r", name)
158163

src/mcp/server/session.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ class ServerSession(
7777
types.ClientNotification,
7878
]
7979
):
80+
# TODO: Multi-tenancy - ServerSession does not track tenant_id. Need to add tenant_id field
81+
# to identify which tenant this session belongs to. This is critical for isolating tenant data
82+
# and ensuring requests are processed in the correct tenant context.
8083
_initialized: InitializationState = InitializationState.NotInitialized
8184
_client_params: types.InitializeRequestParams | None = None
8285

src/mcp/server/streamable_http_manager.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ def __init__(
6969

7070
# Session tracking (only used if not stateless)
7171
self._session_creation_lock = anyio.Lock()
72+
# TODO: Multi-tenancy - Server instances are tracked globally by session_id without tenant scoping.
73+
# Need to ensure sessions are isolated per tenant. Consider using composite key (tenant_id, session_id)
74+
# or partitioning _server_instances by tenant_id to prevent cross-tenant session access.
7275
self._server_instances: dict[str, StreamableHTTPServerTransport] = {}
7376

7477
# The task group will be set during lifespan

0 commit comments

Comments
 (0)