# Low-Level Server Refactoring Plan ## Overview This document outlines the plan to refactor the low-level `Server` class (`src/mcp/server/lowlevel/server.py`) from a **decorator-based approach** to a **constructor-based callback approach**. ### Current Approach (Decorator-Based) ```python server = Server("my-server") @server.list_tools() async def handle_list_tools() -> list[types.Tool]: return [...] @server.call_tool() async def handle_call_tool(name: str, arguments: dict) -> dict: return {"result": "..."} ``` ### Proposed Approach (Constructor-Based) All handlers receive **context as the first parameter** and **params as the second parameter**, and return a properly typed result object. ```python async def handle_list_tools( ctx: RequestContext[ServerSession, Any, Any], params: types.PaginatedRequestParams | None, ) -> types.ListToolsResult: return types.ListToolsResult(tools=[...]) async def handle_call_tool( ctx: RequestContext[ServerSession, Any, Any], params: types.CallToolRequestParams, ) -> types.CallToolResult: return types.CallToolResult(content=[...]) server = Server( name="my-server", on_list_tools=handle_list_tools, on_call_tool=handle_call_tool, ) ``` --- ## Phase 1: Update Constructor Signature **File:** `src/mcp/server/lowlevel/server.py` Add new handler parameters with **inline types** (no type aliases). Each handler follows the pattern: - First parameter: `RequestContext[ServerSession, LifespanResultT, RequestT]` - Second parameter: The specific `*Params` type for that request - Return type: The specific `*Result` type for that request ### New Constructor Parameters ```python from mcp.shared.context import RequestContext from mcp.server.session import ServerSession class Server(Generic[LifespanResultT, RequestT]): def __init__( self, name: str, version: str | None = None, title: str | None = None, description: str | None = None, instructions: str | None = None, website_url: str | None = None, icons: list[types.Icon] | None = None, lifespan: Callable[ [Server[LifespanResultT, RequestT]], AbstractAsyncContextManager[LifespanResultT], ] = lifespan, *, on_list_prompts: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.PaginatedRequestParams | None], Awaitable[types.ListPromptsResult], ] | None = None, on_get_prompt: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.GetPromptRequestParams], Awaitable[types.GetPromptResult], ] | None = None, # Resources on_list_resources: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.PaginatedRequestParams | None], Awaitable[types.ListResourcesResult], ] | None = None, on_list_resource_templates: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.PaginatedRequestParams | None], Awaitable[types.ListResourceTemplatesResult], ] | None = None, on_read_resource: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.ReadResourceRequestParams], Awaitable[types.ReadResourceResult], ] | None = None, on_subscribe_resource: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.SubscribeRequestParams], Awaitable[types.EmptyResult], ] | None = None, on_unsubscribe_resource: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.UnsubscribeRequestParams], Awaitable[types.EmptyResult], ] | None = None, # Tools on_list_tools: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.PaginatedRequestParams | None], Awaitable[types.ListToolsResult], ] | None = None, on_call_tool: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.CallToolRequestParams], Awaitable[types.CallToolResult], ] | None = None, # Logging on_set_logging_level: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.SetLevelRequestParams], Awaitable[types.EmptyResult], ] | None = None, # Completions on_completion: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.CompleteRequestParams], Awaitable[types.CompleteResult], ] | None = None, # Notifications on_progress_notification: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.ProgressNotificationParams], Awaitable[None], ] | None = None, ): ``` ### Handler Naming Convention | Current Decorator | Constructor Parameter | Params Type | Result Type | |-------------------|----------------------|-------------|-------------| | `@server.list_prompts()` | `on_list_prompts` | `PaginatedRequestParams \| None` | `ListPromptsResult` | | `@server.get_prompt()` | `on_get_prompt` | `GetPromptRequestParams` | `GetPromptResult` | | `@server.list_resources()` | `on_list_resources` | `PaginatedRequestParams \| None` | `ListResourcesResult` | | `@server.list_resource_templates()` | `on_list_resource_templates` | `PaginatedRequestParams \| None` | `ListResourceTemplatesResult` | | `@server.read_resource()` | `on_read_resource` | `ReadResourceRequestParams` | `ReadResourceResult` | | `@server.subscribe_resource()` | `on_subscribe_resource` | `SubscribeRequestParams` | `EmptyResult` | | `@server.unsubscribe_resource()` | `on_unsubscribe_resource` | `UnsubscribeRequestParams` | `EmptyResult` | | `@server.list_tools()` | `on_list_tools` | `PaginatedRequestParams \| None` | `ListToolsResult` | | `@server.call_tool()` | `on_call_tool` | `CallToolRequestParams` | `CallToolResult` | | `@server.set_logging_level()` | `on_set_logging_level` | `SetLevelRequestParams` | `EmptyResult` | | `@server.completion()` | `on_completion` | `CompleteRequestParams` | `CompleteResult` | | `@server.progress_notification()` | `on_progress_notification` | `ProgressNotificationParams` | `None` | --- ## Phase 2: Constructor Handler Registration **File:** `src/mcp/server/lowlevel/server.py` In `__init__`, register handlers passed via constructor parameters. Each handler wrapper: 1. Sets up the request context 2. Calls the user's handler with `(context, params)` 3. Returns the result directly (no transformation needed since handler returns proper result type) ```python def __init__(self, ...): # ... existing initialization ... # Register handlers from constructor parameters if on_list_prompts is not None: self._register_list_prompts_handler(on_list_prompts) if on_get_prompt is not None: self._register_get_prompt_handler(on_get_prompt) if on_list_resources is not None: self._register_list_resources_handler(on_list_resources) if on_list_resource_templates is not None: self._register_list_resource_templates_handler(on_list_resource_templates) if on_read_resource is not None: self._register_read_resource_handler(on_read_resource) if on_subscribe_resource is not None: self._register_subscribe_resource_handler(on_subscribe_resource) if on_unsubscribe_resource is not None: self._register_unsubscribe_resource_handler(on_unsubscribe_resource) if on_list_tools is not None: self._register_list_tools_handler(on_list_tools) if on_call_tool is not None: self._register_call_tool_handler(on_call_tool) if on_set_logging_level is not None: self._register_set_logging_level_handler(on_set_logging_level) if on_completion is not None: self._register_completion_handler(on_completion) if on_progress_notification is not None: self._register_progress_notification_handler(on_progress_notification) ``` ### Internal Registration Methods The key change is that the internal handlers now pass the context and params to the user's callback: ```python def _register_list_tools_handler( self, func: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.PaginatedRequestParams | None], Awaitable[types.ListToolsResult], ], ) -> None: """Register a list tools handler.""" logger.debug("Registering handler for ListToolsRequest") async def handler(req: types.ListToolsRequest) -> types.ListToolsResult: # Context is already set by _handle_request, retrieve it ctx = request_ctx.get() result = await func(ctx, req.params) # Validate tool names (existing behavior) for tool in result.tools: validate_and_warn_tool_name(tool.name) self._tool_cache[tool.name] = tool return result self.request_handlers[types.ListToolsRequest] = handler def _register_call_tool_handler( self, func: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.CallToolRequestParams], Awaitable[types.CallToolResult], ], ) -> None: """Register a call tool handler.""" logger.debug("Registering handler for CallToolRequest") async def handler(req: types.CallToolRequest) -> types.CallToolResult: ctx = request_ctx.get() # User handler is responsible for returning CallToolResult return await func(ctx, req.params) self.request_handlers[types.CallToolRequest] = handler def _register_get_prompt_handler( self, func: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.GetPromptRequestParams], Awaitable[types.GetPromptResult], ], ) -> None: """Register a get prompt handler.""" logger.debug("Registering handler for GetPromptRequest") async def handler(req: types.GetPromptRequest) -> types.GetPromptResult: ctx = request_ctx.get() return await func(ctx, req.params) self.request_handlers[types.GetPromptRequest] = handler def _register_read_resource_handler( self, func: Callable[ [RequestContext[ServerSession, LifespanResultT, RequestT], types.ReadResourceRequestParams], Awaitable[types.ReadResourceResult], ], ) -> None: """Register a read resource handler.""" logger.debug("Registering handler for ReadResourceRequest") async def handler(req: types.ReadResourceRequest) -> types.ReadResourceResult: ctx = request_ctx.get() return await func(ctx, req.params) self.request_handlers[types.ReadResourceRequest] = handler ``` --- ## Phase 3: Deprecate Decorator Methods **File:** `src/mcp/server/lowlevel/server.py` Keep decorators for backward compatibility but mark as deprecated: ```python def list_tools(self): """Register a list tools handler. .. deprecated:: Use the `on_list_tools` constructor parameter instead. """ warnings.warn( "The @server.list_tools() decorator is deprecated. " "Use the on_list_tools constructor parameter instead.", DeprecationWarning, stacklevel=2, ) def decorator( func: Callable[[], Awaitable[list[types.Tool]]] | Callable[[types.ListToolsRequest], Awaitable[types.ListToolsResult]], ): # Keep existing decorator logic for backward compatibility wrapper = create_call_wrapper(func, types.ListToolsRequest) async def handler(req: types.ListToolsRequest): result = await wrapper(req) if isinstance(result, types.ListToolsResult): for tool in result.tools: validate_and_warn_tool_name(tool.name) self._tool_cache[tool.name] = tool return result else: self._tool_cache.clear() for tool in result: validate_and_warn_tool_name(tool.name) self._tool_cache[tool.name] = tool return types.ListToolsResult(tools=result) self.request_handlers[types.ListToolsRequest] = handler return func return decorator ``` --- ## Phase 4: Update Tests **File:** `tests/server/lowlevel/test_constructor_handlers.py` (create new) ### Add Tests for Constructor-Based Registration ```python import pytest from mcp.server.lowlevel import Server from mcp.server.session import ServerSession from mcp.shared.context import RequestContext import mcp.types as types from typing import Any @pytest.mark.anyio async def test_constructor_list_tools_handler(): """Test registering list_tools via constructor.""" async def list_tools( ctx: RequestContext[ServerSession, Any, Any], params: types.PaginatedRequestParams | None, ) -> types.ListToolsResult: return types.ListToolsResult( tools=[types.Tool(name="test-tool", description="A test tool")] ) server = Server( name="test-server", on_list_tools=list_tools, ) assert types.ListToolsRequest in server.request_handlers @pytest.mark.anyio async def test_constructor_call_tool_handler(): """Test registering call_tool via constructor.""" async def call_tool( ctx: RequestContext[ServerSession, Any, Any], params: types.CallToolRequestParams, ) -> types.CallToolResult: return types.CallToolResult( content=[types.TextContent(type="text", text=f"Called {params.name}")], ) server = Server( name="test-server", on_call_tool=call_tool, ) assert types.CallToolRequest in server.request_handlers @pytest.mark.anyio async def test_decorator_deprecation_warning(): """Test that decorators emit deprecation warnings.""" server = Server(name="test-server") with pytest.warns(DeprecationWarning, match="on_list_tools constructor parameter"): @server.list_tools() async def list_tools(): return [] ``` ### E2E Tests Using mcp.client.Client Follow the pattern from `tests/client/test_client.py`: ```python @pytest.mark.anyio async def test_constructor_tools_e2e(): """E2E test for constructor-based tool handlers.""" async def list_tools( ctx: RequestContext[ServerSession, Any, Any], params: types.PaginatedRequestParams | None, ) -> types.ListToolsResult: return types.ListToolsResult( tools=[ types.Tool( name="echo", description="Echo input", input_schema={ "type": "object", "properties": {"message": {"type": "string"}}, }, ) ] ) async def call_tool( ctx: RequestContext[ServerSession, Any, Any], params: types.CallToolRequestParams, ) -> types.CallToolResult: if params.name == "echo": msg = (params.arguments or {}).get("message", "") return types.CallToolResult( content=[types.TextContent(type="text", text=msg)], ) return types.CallToolResult( content=[types.TextContent(type="text", text=f"Unknown tool: {params.name}")], is_error=True, ) server = Server( name="test-server", on_list_tools=list_tools, on_call_tool=call_tool, ) # Use in-memory transport for testing async with create_client_server_pair(server) as (client, _): tools = await client.list_tools() assert len(tools.tools) == 1 assert tools.tools[0].name == "echo" result = await client.call_tool("echo", {"message": "hello"}) assert result.content[0].text == "hello" ``` --- ## Phase 5: Update Documentation ### Update Module Docstring **File:** `src/mcp/server/lowlevel/server.py` ```python """MCP Server Module This module provides a framework for creating an MCP (Model Context Protocol) server. Usage: 1. Define handler functions that receive (context, params) and return result objects: async def list_tools( ctx: RequestContext[ServerSession, Any, Any], params: types.PaginatedRequestParams | None, ) -> types.ListToolsResult: return types.ListToolsResult(tools=[ types.Tool(name="my-tool", description="...") ]) async def call_tool( ctx: RequestContext[ServerSession, Any, Any], params: types.CallToolRequestParams, ) -> types.CallToolResult: # Access context for session, lifespan data, etc. db = ctx.lifespan_context["db"] return types.CallToolResult(content=[...]) 2. Create a Server instance with handlers: server = Server( name="your_server_name", on_list_tools=list_tools, on_call_tool=call_tool, ) 3. Run the server: async def main(): async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): await server.run( read_stream, write_stream, server.create_initialization_options(), ) asyncio.run(main()) Note: The decorator-based API is deprecated but still supported for backward compatibility. """ ``` ### Update Migration Guide **File:** `docs/migration.md` Add a new section documenting the change: ```markdown ## Low-Level Server API Changes ### Constructor-Based Handler Registration The low-level `Server` class now supports constructor-based handler registration, which is the recommended approach. The decorator-based API is deprecated. **Before (Deprecated):** ```python server = Server("my-server") @server.list_tools() async def list_tools(): return [types.Tool(name="tool", description="...")] @server.call_tool() async def call_tool(name: str, arguments: dict): return {"result": "..."} ``` **After (Recommended):** ```python async def list_tools( ctx: RequestContext[ServerSession, Any, Any], params: types.PaginatedRequestParams | None, ) -> types.ListToolsResult: return types.ListToolsResult(tools=[ types.Tool(name="tool", description="...") ]) async def call_tool( ctx: RequestContext[ServerSession, Any, Any], params: types.CallToolRequestParams, ) -> types.CallToolResult: return types.CallToolResult( content=[types.TextContent(type="text", text="result")] ) server = Server( "my-server", on_list_tools=list_tools, on_call_tool=call_tool, ) ``` **Key differences:** 1. Handlers receive `(context, params)` instead of extracted arguments 2. Handlers return proper result types (`ListToolsResult`, `CallToolResult`, etc.) 3. Context provides access to session, lifespan data, and request metadata **Migration steps:** 1. Update handler signatures to accept `(ctx, params)` 2. Update return types to use proper result classes 3. Pass handlers to the Server constructor using `on_*` parameters 4. Remove decorator calls **Benefits:** - Context available in all handlers (session, lifespan data, request metadata) - Type-safe params and return types - Clearer dependencies at construction time - Better testability (handlers can be mocked/replaced) ``` --- ## Phase 6: Implementation Checklist ### Files to Modify - [ ] `src/mcp/server/lowlevel/server.py` - Main server class - [ ] `docs/migration.md` - Document breaking changes ### Files to Create/Update for Tests - [ ] `tests/server/lowlevel/test_constructor_handlers.py` - New tests for constructor API - [ ] Update existing tests in `tests/server/` to use new API where appropriate ### Implementation Order 1. **Add private registration methods** (`_register_*_handler`) that accept the new signature 2. **Update constructor** to accept handler parameters with inline types 3. **Register handlers in constructor** by calling private methods 4. **Deprecate decorator methods** with warnings 5. **Write tests** for new constructor-based API 6. **Update documentation** and migration guide 7. **Run full test suite** to ensure backward compatibility --- ## Phase 7: Backward Compatibility Strategy ### Approach: Deprecation with Migration Period 1. **Keep decorators working** - They should continue to function but emit deprecation warnings 2. **Allow mixed usage** - Users can use constructor params for some handlers and decorators for others (during migration) 3. **Future removal** - Plan to remove decorator methods in a future major version ### Conflict Resolution If a handler is registered both via constructor and decorator, raise an error: ```python def _register_list_tools_handler(self, func) -> None: if types.ListToolsRequest in self.request_handlers: raise ValueError( "A list_tools handler is already registered. " "Cannot register multiple handlers for the same request type." ) # ... rest of registration ... ``` --- ## Summary of Changes | Component | Change Type | Description | |-----------|-------------|-------------| | `Server.__init__` | **Addition** | New `on_*` parameters with inline types for all handlers | | `Server._register_*_handler` | **Addition** | Private methods for handler registration | | `Server.list_tools`, etc. | **Deprecation** | Decorator methods emit warnings | | Tests | **Addition** | New tests for constructor-based API | | Documentation | **Update** | Migration guide and module docstring | --- ## Handler Signature Reference All handlers follow the pattern: `(context, params) -> result` | Handler | Context Type | Params Type | Return Type | |---------|--------------|-------------|-------------| | `on_list_prompts` | `RequestContext[...]` | `PaginatedRequestParams \| None` | `ListPromptsResult` | | `on_get_prompt` | `RequestContext[...]` | `GetPromptRequestParams` | `GetPromptResult` | | `on_list_resources` | `RequestContext[...]` | `PaginatedRequestParams \| None` | `ListResourcesResult` | | `on_list_resource_templates` | `RequestContext[...]` | `PaginatedRequestParams \| None` | `ListResourceTemplatesResult` | | `on_read_resource` | `RequestContext[...]` | `ReadResourceRequestParams` | `ReadResourceResult` | | `on_subscribe_resource` | `RequestContext[...]` | `SubscribeRequestParams` | `EmptyResult` | | `on_unsubscribe_resource` | `RequestContext[...]` | `UnsubscribeRequestParams` | `EmptyResult` | | `on_list_tools` | `RequestContext[...]` | `PaginatedRequestParams \| None` | `ListToolsResult` | | `on_call_tool` | `RequestContext[...]` | `CallToolRequestParams` | `CallToolResult` | | `on_set_logging_level` | `RequestContext[...]` | `SetLevelRequestParams` | `EmptyResult` | | `on_completion` | `RequestContext[...]` | `CompleteRequestParams` | `CompleteResult` | | `on_progress_notification` | `RequestContext[...]` | `ProgressNotificationParams` | `None` | Where `RequestContext[...]` is `RequestContext[ServerSession, LifespanResultT, RequestT]`. --- ## Open Questions 1. **Experimental handlers** - Should `server.experimental` handlers also move to constructor parameters, or stay separate? 2. **Should we keep the decorator API indefinitely?** Or plan a hard removal in v2.0?