Skip to content

Commit 2941534

Browse files
authored
Merge pull request #9 from andylim-duo/feature/multi-tenant-mcpserver-handlers
feat(server): thread tenant_id through MCPServer handlers to managers
2 parents 7f83ad4 + 0e0a73d commit 2941534

3 files changed

Lines changed: 349 additions & 28 deletions

File tree

src/mcp/server/mcpserver/context.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,13 @@ def __init__(
6969
self._request_context = request_context
7070
self._mcp_server = mcp_server
7171

72+
@property
73+
def tenant_id(self) -> str | None:
74+
"""Get the tenant_id for this request, if available."""
75+
if self._request_context is not None:
76+
return self._request_context.tenant_id
77+
return None
78+
7279
@property
7380
def mcp_server(self) -> MCPServer:
7481
"""Access to the MCPServer instance."""
@@ -114,7 +121,7 @@ async def read_resource(self, uri: str | AnyUrl) -> Iterable[ReadResourceContent
114121
The resource content as either text or bytes
115122
"""
116123
assert self._mcp_server is not None, "Context is not available outside of a request"
117-
return await self._mcp_server.read_resource(uri, self)
124+
return await self._mcp_server.read_resource(uri, self, tenant_id=self.tenant_id)
118125

119126
async def elicit(
120127
self,

src/mcp/server/mcpserver/server.py

Lines changed: 50 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -293,14 +293,14 @@ def run(
293293
async def _handle_list_tools(
294294
self, ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None
295295
) -> ListToolsResult:
296-
return ListToolsResult(tools=await self.list_tools())
296+
return ListToolsResult(tools=await self.list_tools(tenant_id=ctx.tenant_id))
297297

298298
async def _handle_call_tool(
299299
self, ctx: ServerRequestContext[LifespanResultT], params: CallToolRequestParams
300300
) -> CallToolResult:
301301
context = Context(request_context=ctx, mcp_server=self)
302302
try:
303-
result = await self.call_tool(params.name, params.arguments or {}, context)
303+
result = await self.call_tool(params.name, params.arguments or {}, context, tenant_id=ctx.tenant_id)
304304
except MCPError:
305305
raise
306306
except Exception as e:
@@ -326,13 +326,13 @@ async def _handle_call_tool(
326326
async def _handle_list_resources(
327327
self, ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None
328328
) -> ListResourcesResult:
329-
return ListResourcesResult(resources=await self.list_resources())
329+
return ListResourcesResult(resources=await self.list_resources(tenant_id=ctx.tenant_id))
330330

331331
async def _handle_read_resource(
332332
self, ctx: ServerRequestContext[LifespanResultT], params: ReadResourceRequestParams
333333
) -> ReadResourceResult:
334334
context = Context(request_context=ctx, mcp_server=self)
335-
results = await self.read_resource(params.uri, context)
335+
results = await self.read_resource(params.uri, context, tenant_id=ctx.tenant_id)
336336
contents: list[TextResourceContents | BlobResourceContents] = []
337337
for item in results:
338338
if isinstance(item.content, bytes):
@@ -358,22 +358,24 @@ async def _handle_read_resource(
358358
async def _handle_list_resource_templates(
359359
self, ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None
360360
) -> ListResourceTemplatesResult:
361-
return ListResourceTemplatesResult(resource_templates=await self.list_resource_templates())
361+
return ListResourceTemplatesResult(
362+
resource_templates=await self.list_resource_templates(tenant_id=ctx.tenant_id)
363+
)
362364

363365
async def _handle_list_prompts(
364366
self, ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None
365367
) -> ListPromptsResult:
366-
return ListPromptsResult(prompts=await self.list_prompts())
368+
return ListPromptsResult(prompts=await self.list_prompts(tenant_id=ctx.tenant_id))
367369

368370
async def _handle_get_prompt(
369371
self, ctx: ServerRequestContext[LifespanResultT], params: GetPromptRequestParams
370372
) -> GetPromptResult:
371373
context = Context(request_context=ctx, mcp_server=self)
372-
return await self.get_prompt(params.name, params.arguments, context)
374+
return await self.get_prompt(params.name, params.arguments, context, tenant_id=ctx.tenant_id)
373375

374-
async def list_tools(self) -> list[MCPTool]:
376+
async def list_tools(self, *, tenant_id: str | None = None) -> list[MCPTool]:
375377
"""List all available tools."""
376-
tools = self._tool_manager.list_tools()
378+
tools = self._tool_manager.list_tools(tenant_id=tenant_id)
377379
return [
378380
MCPTool(
379381
name=info.name,
@@ -389,17 +391,22 @@ async def list_tools(self) -> list[MCPTool]:
389391
]
390392

391393
async def call_tool(
392-
self, name: str, arguments: dict[str, Any], context: Context[LifespanResultT, Any] | None = None
394+
self,
395+
name: str,
396+
arguments: dict[str, Any],
397+
context: Context[LifespanResultT, Any] | None = None,
398+
*,
399+
tenant_id: str | None = None,
393400
) -> Sequence[ContentBlock] | dict[str, Any]:
394401
"""Call a tool by name with arguments."""
395402
if context is None:
396403
context = Context(mcp_server=self)
397-
return await self._tool_manager.call_tool(name, arguments, context, convert_result=True)
404+
return await self._tool_manager.call_tool(name, arguments, context, convert_result=True, tenant_id=tenant_id)
398405

399-
async def list_resources(self) -> list[MCPResource]:
406+
async def list_resources(self, *, tenant_id: str | None = None) -> list[MCPResource]:
400407
"""List all available resources."""
401408

402-
resources = self._resource_manager.list_resources()
409+
resources = self._resource_manager.list_resources(tenant_id=tenant_id)
403410
return [
404411
MCPResource(
405412
uri=resource.uri,
@@ -414,8 +421,8 @@ async def list_resources(self) -> list[MCPResource]:
414421
for resource in resources
415422
]
416423

417-
async def list_resource_templates(self) -> list[MCPResourceTemplate]:
418-
templates = self._resource_manager.list_templates()
424+
async def list_resource_templates(self, *, tenant_id: str | None = None) -> list[MCPResourceTemplate]:
425+
templates = self._resource_manager.list_templates(tenant_id=tenant_id)
419426
return [
420427
MCPResourceTemplate(
421428
uri_template=template.uri_template,
@@ -431,13 +438,17 @@ async def list_resource_templates(self) -> list[MCPResourceTemplate]:
431438
]
432439

433440
async def read_resource(
434-
self, uri: AnyUrl | str, context: Context[LifespanResultT, Any] | None = None
441+
self,
442+
uri: AnyUrl | str,
443+
context: Context[LifespanResultT, Any] | None = None,
444+
*,
445+
tenant_id: str | None = None,
435446
) -> Iterable[ReadResourceContents]:
436447
"""Read a resource by URI."""
437448
if context is None:
438449
context = Context(mcp_server=self)
439450
try:
440-
resource = await self._resource_manager.get_resource(uri, context)
451+
resource = await self._resource_manager.get_resource(uri, context, tenant_id=tenant_id)
441452
except ValueError:
442453
raise ResourceError(f"Unknown resource: {uri}")
443454

@@ -459,6 +470,8 @@ def add_tool(
459470
icons: list[Icon] | None = None,
460471
meta: dict[str, Any] | None = None,
461472
structured_output: bool | None = None,
473+
*,
474+
tenant_id: str | None = None,
462475
) -> None:
463476
"""Add a tool to the server.
464477
@@ -477,6 +490,7 @@ def add_tool(
477490
- If None, auto-detects based on the function's return type annotation
478491
- If True, creates a structured tool (return type annotation permitting)
479492
- If False, unconditionally creates an unstructured tool
493+
tenant_id: Optional tenant scope for the tool
480494
"""
481495
self._tool_manager.add_tool(
482496
fn,
@@ -487,18 +501,20 @@ def add_tool(
487501
icons=icons,
488502
meta=meta,
489503
structured_output=structured_output,
504+
tenant_id=tenant_id,
490505
)
491506

492-
def remove_tool(self, name: str) -> None:
507+
def remove_tool(self, name: str, *, tenant_id: str | None = None) -> None:
493508
"""Remove a tool from the server by name.
494509
495510
Args:
496511
name: The name of the tool to remove
512+
tenant_id: Optional tenant scope for the tool
497513
498514
Raises:
499515
ToolError: If the tool does not exist
500516
"""
501-
self._tool_manager.remove_tool(name)
517+
self._tool_manager.remove_tool(name, tenant_id=tenant_id)
502518

503519
def tool(
504520
self,
@@ -607,13 +623,14 @@ async def handler(
607623

608624
return decorator
609625

610-
def add_resource(self, resource: Resource) -> None:
626+
def add_resource(self, resource: Resource, *, tenant_id: str | None = None) -> None:
611627
"""Add a resource to the server.
612628
613629
Args:
614630
resource: A Resource instance to add
631+
tenant_id: Optional tenant scope for the resource
615632
"""
616-
self._resource_manager.add_resource(resource)
633+
self._resource_manager.add_resource(resource, tenant_id=tenant_id)
617634

618635
def resource(
619636
self,
@@ -727,13 +744,14 @@ def decorator(fn: _CallableT) -> _CallableT:
727744

728745
return decorator
729746

730-
def add_prompt(self, prompt: Prompt) -> None:
747+
def add_prompt(self, prompt: Prompt, *, tenant_id: str | None = None) -> None:
731748
"""Add a prompt to the server.
732749
733750
Args:
734751
prompt: A Prompt instance to add
752+
tenant_id: Optional tenant scope for the prompt
735753
"""
736-
self._prompt_manager.add_prompt(prompt)
754+
self._prompt_manager.add_prompt(prompt, tenant_id=tenant_id)
737755

738756
def prompt(
739757
self,
@@ -1060,9 +1078,9 @@ def streamable_http_app(
10601078
debug=self.settings.debug,
10611079
)
10621080

1063-
async def list_prompts(self) -> list[MCPPrompt]:
1081+
async def list_prompts(self, *, tenant_id: str | None = None) -> list[MCPPrompt]:
10641082
"""List all available prompts."""
1065-
prompts = self._prompt_manager.list_prompts()
1083+
prompts = self._prompt_manager.list_prompts(tenant_id=tenant_id)
10661084
return [
10671085
MCPPrompt(
10681086
name=prompt.name,
@@ -1082,13 +1100,18 @@ async def list_prompts(self) -> list[MCPPrompt]:
10821100
]
10831101

10841102
async def get_prompt(
1085-
self, name: str, arguments: dict[str, Any] | None = None, context: Context[LifespanResultT, Any] | None = None
1103+
self,
1104+
name: str,
1105+
arguments: dict[str, Any] | None = None,
1106+
context: Context[LifespanResultT, Any] | None = None,
1107+
*,
1108+
tenant_id: str | None = None,
10861109
) -> GetPromptResult:
10871110
"""Get a prompt by name with arguments."""
10881111
if context is None:
10891112
context = Context(mcp_server=self)
10901113
try:
1091-
prompt = self._prompt_manager.get_prompt(name)
1114+
prompt = self._prompt_manager.get_prompt(name, tenant_id=tenant_id)
10921115
if not prompt:
10931116
raise ValueError(f"Unknown prompt: {name}")
10941117

0 commit comments

Comments
 (0)