22
33from __future__ import annotations
44
5+ import base64
6+ import contextvars
57import inspect
8+ import json
69import re
710from collections .abc import AsyncIterator , Awaitable , Callable , Iterable , Sequence
811from contextlib import AbstractAsyncContextManager , asynccontextmanager
2831from mcp .server .elicitation import ElicitationResult , ElicitSchemaModelT , UrlElicitationResult , elicit_with_validation
2932from mcp .server .elicitation import elicit_url as _elicit_url
3033from mcp .server .lowlevel .helper_types import ReadResourceContents
34+ from mcp .server .lowlevel .request_handler import RequestHandler
3135from mcp .server .lowlevel .server import LifespanResultT , Server
3236from mcp .server .lowlevel .server import lifespan as default_lifespan
3337from mcp .server .mcpserver .exceptions import ResourceError
4246from mcp .server .streamable_http import EventStore
4347from mcp .server .streamable_http_manager import StreamableHTTPSessionManager
4448from mcp .server .transport_security import TransportSecuritySettings
45- from mcp .shared .context import LifespanContextT , RequestContext , RequestT
46- from mcp .types import Annotations , ContentBlock , GetPromptResult , Icon , ToolAnnotations
49+ from mcp .shared .context import LifespanContextT , RequestHandlerContext , RequestT
50+ from mcp .shared .exceptions import MCPError
51+ from mcp .types import (
52+ Annotations ,
53+ BlobResourceContents ,
54+ CallToolResult ,
55+ CompleteResult ,
56+ Completion ,
57+ ContentBlock ,
58+ GetPromptResult ,
59+ Icon ,
60+ ListPromptsResult ,
61+ ListResourcesResult ,
62+ ListResourceTemplatesResult ,
63+ ListToolsResult ,
64+ ReadResourceResult ,
65+ TextContent ,
66+ TextResourceContents ,
67+ ToolAnnotations ,
68+ )
4769from mcp .types import Prompt as MCPPrompt
4870from mcp .types import PromptArgument as MCPPromptArgument
4971from mcp .types import Resource as MCPResource
5476
5577_CallableT = TypeVar ("_CallableT" , bound = Callable [..., Any ])
5678
79+ _mcp_server_ctx : contextvars .ContextVar [RequestHandlerContext [ServerSession , Any , Any ]] = contextvars .ContextVar (
80+ "_mcp_server_ctx"
81+ )
82+
5783
5884class Settings (BaseSettings , Generic [LifespanResultT ]):
5985 """MCPServer settings.
@@ -266,16 +292,105 @@ def run(
266292
267293 def _setup_handlers (self ) -> None :
268294 """Set up core MCP protocol handlers."""
269- self ._lowlevel_server .list_tools ()(self .list_tools )
270- # Note: we disable the lowlevel server's input validation.
271- # MCPServer does ad hoc conversion of incoming data before validating -
272- # for now we preserve this for backwards compatibility.
273- self ._lowlevel_server .call_tool (validate_input = False )(self .call_tool )
274- self ._lowlevel_server .list_resources ()(self .list_resources )
275- self ._lowlevel_server .read_resource ()(self .read_resource )
276- self ._lowlevel_server .list_prompts ()(self .list_prompts )
277- self ._lowlevel_server .get_prompt ()(self .get_prompt )
278- self ._lowlevel_server .list_resource_templates ()(self .list_resource_templates )
295+
296+ async def handle_list_tools (ctx : Any , params : Any ) -> ListToolsResult :
297+ token = _mcp_server_ctx .set (ctx )
298+ try :
299+ return ListToolsResult (tools = await self .list_tools ())
300+ finally :
301+ _mcp_server_ctx .reset (token )
302+
303+ async def handle_call_tool (ctx : Any , params : Any ) -> CallToolResult :
304+ token = _mcp_server_ctx .set (ctx )
305+ try :
306+ try :
307+ result = await self .call_tool (params .name , params .arguments or {})
308+ except MCPError :
309+ raise
310+ except Exception as e :
311+ return CallToolResult (content = [TextContent (type = "text" , text = str (e ))], is_error = True )
312+ if isinstance (result , CallToolResult ):
313+ return result
314+ if isinstance (result , tuple ) and len (result ) == 2 :
315+ unstructured_content , structured_content = result
316+ return CallToolResult (
317+ content = list (unstructured_content ), # type: ignore[arg-type]
318+ structured_content = structured_content , # type: ignore[arg-type]
319+ )
320+ if isinstance (result , dict ):
321+ return CallToolResult (
322+ content = [TextContent (type = "text" , text = json .dumps (result , indent = 2 ))],
323+ structured_content = result ,
324+ )
325+ return CallToolResult (content = list (result ))
326+ finally :
327+ _mcp_server_ctx .reset (token )
328+
329+ async def handle_list_resources (ctx : Any , params : Any ) -> ListResourcesResult :
330+ token = _mcp_server_ctx .set (ctx )
331+ try :
332+ return ListResourcesResult (resources = await self .list_resources ())
333+ finally :
334+ _mcp_server_ctx .reset (token )
335+
336+ async def handle_read_resource (ctx : Any , params : Any ) -> ReadResourceResult :
337+ token = _mcp_server_ctx .set (ctx )
338+ try :
339+ results = await self .read_resource (params .uri )
340+ contents : list [TextResourceContents | BlobResourceContents ] = []
341+ for item in results :
342+ if isinstance (item .content , bytes ):
343+ contents .append (
344+ BlobResourceContents (
345+ uri = params .uri ,
346+ blob = base64 .b64encode (item .content ).decode (),
347+ mime_type = item .mime_type or "application/octet-stream" ,
348+ _meta = item .meta ,
349+ )
350+ )
351+ else :
352+ contents .append (
353+ TextResourceContents (
354+ uri = params .uri ,
355+ text = item .content ,
356+ mime_type = item .mime_type or "text/plain" ,
357+ _meta = item .meta ,
358+ )
359+ )
360+ return ReadResourceResult (contents = contents )
361+ finally :
362+ _mcp_server_ctx .reset (token )
363+
364+ async def handle_list_resource_templates (ctx : Any , params : Any ) -> ListResourceTemplatesResult :
365+ token = _mcp_server_ctx .set (ctx )
366+ try :
367+ return ListResourceTemplatesResult (resource_templates = await self .list_resource_templates ())
368+ finally :
369+ _mcp_server_ctx .reset (token )
370+
371+ async def handle_list_prompts (ctx : Any , params : Any ) -> ListPromptsResult :
372+ token = _mcp_server_ctx .set (ctx )
373+ try :
374+ return ListPromptsResult (prompts = await self .list_prompts ())
375+ finally :
376+ _mcp_server_ctx .reset (token )
377+
378+ async def handle_get_prompt (ctx : Any , params : Any ) -> GetPromptResult :
379+ token = _mcp_server_ctx .set (ctx )
380+ try :
381+ return await self .get_prompt (params .name , params .arguments )
382+ finally :
383+ _mcp_server_ctx .reset (token )
384+
385+ self ._lowlevel_server .add_handler (RequestHandler ("tools/list" , handler = handle_list_tools ))
386+ self ._lowlevel_server .add_handler (RequestHandler ("tools/call" , handler = handle_call_tool ))
387+ self ._lowlevel_server .add_handler (RequestHandler ("resources/list" , handler = handle_list_resources ))
388+ self ._lowlevel_server .add_handler (RequestHandler ("resources/read" , handler = handle_read_resource ))
389+ self ._lowlevel_server .add_handler (
390+ RequestHandler ("resources/templates/list" , handler = handle_list_resource_templates )
391+ )
392+ self ._lowlevel_server .add_handler (RequestHandler ("prompts/list" , handler = handle_list_prompts ))
393+ self ._lowlevel_server .add_handler (RequestHandler ("prompts/get" , handler = handle_get_prompt ))
279394
280395 async def list_tools (self ) -> list [MCPTool ]:
281396 """List all available tools."""
@@ -299,7 +414,7 @@ def get_context(self) -> Context[ServerSession, LifespanResultT, Request]:
299414 during a request; outside a request, most methods will error.
300415 """
301416 try :
302- request_context = self . _lowlevel_server . request_context
417+ request_context = _mcp_server_ctx . get ()
303418 except LookupError :
304419 request_context = None
305420 return Context (request_context = request_context , mcp_server = self )
@@ -487,7 +602,22 @@ async def handle_completion(ref, argument, context):
487602 return Completion(values=["option1", "option2"])
488603 return None
489604 """
490- return self ._lowlevel_server .completion ()
605+
606+ def decorator (func : _CallableT ) -> _CallableT :
607+ async def handler (ctx : Any , params : Any ) -> CompleteResult :
608+ token = _mcp_server_ctx .set (ctx )
609+ try :
610+ result = await func (params .ref , params .argument , params .context )
611+ return CompleteResult (
612+ completion = result if result is not None else Completion (values = [], total = None , has_more = None ),
613+ )
614+ finally :
615+ _mcp_server_ctx .reset (token )
616+
617+ self ._lowlevel_server .add_handler (RequestHandler ("completion/complete" , handler = handler ))
618+ return func
619+
620+ return decorator
491621
492622 def add_resource (self , resource : Resource ) -> None :
493623 """Add a resource to the server.
@@ -1006,13 +1136,13 @@ def my_tool(x: int, ctx: Context) -> str:
10061136 The context is optional - tools that don't need it can omit the parameter.
10071137 """
10081138
1009- _request_context : RequestContext [ServerSessionT , LifespanContextT , RequestT ] | None
1139+ _request_context : RequestHandlerContext [ServerSessionT , LifespanContextT , RequestT ] | None
10101140 _mcp_server : MCPServer | None
10111141
10121142 def __init__ (
10131143 self ,
10141144 * ,
1015- request_context : (RequestContext [ServerSessionT , LifespanContextT , RequestT ] | None ) = None ,
1145+ request_context : (RequestHandlerContext [ServerSessionT , LifespanContextT , RequestT ] | None ) = None ,
10161146 mcp_server : MCPServer | None = None ,
10171147 ** kwargs : Any ,
10181148 ):
@@ -1030,7 +1160,7 @@ def mcp_server(self) -> MCPServer:
10301160 @property
10311161 def request_context (
10321162 self ,
1033- ) -> RequestContext [ServerSessionT , LifespanContextT , RequestT ]:
1163+ ) -> RequestHandlerContext [ServerSessionT , LifespanContextT , RequestT ]:
10341164 """Access to the underlying request context."""
10351165 if self ._request_context is None : # pragma: no cover
10361166 raise ValueError ("Context is not available outside of a request" )
0 commit comments