Skip to content

Commit b6add0b

Browse files
committed
refactor: update MCPServer to use RequestHandler pattern instead of decorators
Replace decorator-based handler registration with explicit RequestHandler objects registered via add_handler(). Each handler manages its own contextvar for request context propagation. Key changes: - Add _mcp_server_ctx contextvar (replaces lowlevel server's request_ctx) - Rewrite _setup_handlers() with RequestHandler objects for all 7 methods - Rewrite get_context() to read from _mcp_server_ctx - Rewrite completion() decorator to create RequestHandler inline - Update Context class to use RequestHandlerContext (was RequestContext) - Handle CallToolResult pass-through and CombinationContent tuples - Re-raise MCPError in call_tool handler (UrlElicitationRequiredError) - Apply default MIME types for resources (text/plain, application/octet-stream) - Match old json.dumps indent=2 for structured tool output
1 parent 4c8fe51 commit b6add0b

File tree

1 file changed

+147
-17
lines changed

1 file changed

+147
-17
lines changed

src/mcp/server/mcpserver/server.py

Lines changed: 147 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
from __future__ import annotations
44

5+
import base64
6+
import contextvars
57
import inspect
8+
import json
69
import re
710
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence
811
from contextlib import AbstractAsyncContextManager, asynccontextmanager
@@ -28,6 +31,7 @@
2831
from mcp.server.elicitation import ElicitationResult, ElicitSchemaModelT, UrlElicitationResult, elicit_with_validation
2932
from mcp.server.elicitation import elicit_url as _elicit_url
3033
from mcp.server.lowlevel.helper_types import ReadResourceContents
34+
from mcp.server.lowlevel.request_handler import RequestHandler
3135
from mcp.server.lowlevel.server import LifespanResultT, Server
3236
from mcp.server.lowlevel.server import lifespan as default_lifespan
3337
from mcp.server.mcpserver.exceptions import ResourceError
@@ -42,8 +46,26 @@
4246
from mcp.server.streamable_http import EventStore
4347
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
4448
from 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+
)
4769
from mcp.types import Prompt as MCPPrompt
4870
from mcp.types import PromptArgument as MCPPromptArgument
4971
from mcp.types import Resource as MCPResource
@@ -54,6 +76,10 @@
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

5884
class 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

Comments
 (0)