refactor: replace lowlevel Server decorators with on_* constructor kwargs (#1985)

This commit is contained in:
Max Isbey 2026-02-12 15:55:54 +00:00 • committed by GitHub
commit 0a22a9dc33
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 3563 additions and 5481 deletions

View file

@ -1,5 +1,6 @@
from .context import ServerRequestContext
from .lowlevel import NotificationOptions, Server
from .mcpserver import MCPServer
from .models import InitializationOptions
__all__ = ["Server", "MCPServer", "NotificationOptions", "InitializationOptions"]
__all__ = ["Server", "ServerRequestContext", "MCPServer", "NotificationOptions", "InitializationOptions"]

View file

@ -10,7 +10,7 @@ from mcp.server.session import ServerSession
from mcp.shared._context import RequestContext
from mcp.shared.message import CloseSSEStreamCallback
LifespanContextT = TypeVar("LifespanContextT")
LifespanContextT = TypeVar("LifespanContextT", default=dict[str, Any])
RequestT = TypeVar("RequestT", default=Any)

View file

@ -160,10 +160,7 @@ class Experimental:
RuntimeError: If task support is not enabled or task_metadata is missing
Example:
@server.call_tool()
async def handle_tool(name: str, args: dict):
ctx = server.request_context
async def handle_tool(ctx: RequestContext, params: CallToolRequestParams) -> CallToolResult:
async def work(task: ServerTaskContext) -> CallToolResult:
result = await task.elicit(
message="Are you sure?",

View file

@ -44,17 +44,14 @@ class TaskResultHandler:
5. Returns the final result
Usage:
# Create handler with store and queue
handler = TaskResultHandler(task_store, message_queue)
async def handle_task_result(
ctx: ServerRequestContext, params: GetTaskPayloadRequestParams
) -> GetTaskPayloadResult:
...
# Register it with the server
@server.experimental.get_task_result()
async def handle_task_result(req: GetTaskPayloadRequest) -> GetTaskPayloadResult:
ctx = server.request_context
return await handler.handle(req, ctx.session, ctx.request_id)
# Or use the convenience method
handler.register(server)
server.experimental.enable_tasks(
on_task_result=handle_task_result,
)
"""
def __init__(

View file

@ -1,3 +1,3 @@
from .server import NotificationOptions, Server
__all__ = ["Server", "NotificationOptions"]
__all__ = ["NotificationOptions", "Server"]

View file

@ -7,10 +7,12 @@ from __future__ import annotations
import logging
from collections.abc import Awaitable, Callable
from typing import TYPE_CHECKING
from typing import Any, Generic
from typing_extensions import TypeVar
from mcp.server.context import ServerRequestContext
from mcp.server.experimental.task_support import TaskSupport
from mcp.server.lowlevel.func_inspection import create_call_wrapper
from mcp.shared.exceptions import MCPError
from mcp.shared.experimental.tasks.helpers import cancel_task
from mcp.shared.experimental.tasks.in_memory_task_store import InMemoryTaskStore
@ -18,16 +20,16 @@ from mcp.shared.experimental.tasks.message_queue import InMemoryTaskMessageQueue
from mcp.shared.experimental.tasks.store import TaskStore
from mcp.types import (
INVALID_PARAMS,
CancelTaskRequest,
CancelTaskRequestParams,
CancelTaskResult,
GetTaskPayloadRequest,
GetTaskPayloadRequestParams,
GetTaskPayloadResult,
GetTaskRequest,
GetTaskRequestParams,
GetTaskResult,
ListTasksRequest,
ListTasksResult,
PaginatedRequestParams,
ServerCapabilities,
ServerResult,
ServerTasksCapability,
ServerTasksRequestsCapability,
TasksCallCapability,
@ -36,13 +38,12 @@ from mcp.types import (
TasksToolsCapability,
)
if TYPE_CHECKING:
from mcp.server.lowlevel.server import Server
logger = logging.getLogger(__name__)
LifespanResultT = TypeVar("LifespanResultT", default=Any)
class ExperimentalHandlers:
class ExperimentalHandlers(Generic[LifespanResultT]):
"""Experimental request/notification handlers.
WARNING: These APIs are experimental and may change without notice.
@ -50,13 +51,13 @@ class ExperimentalHandlers:
def __init__(
self,
server: Server,
request_handlers: dict[type, Callable[..., Awaitable[ServerResult]]],
notification_handlers: dict[type, Callable[..., Awaitable[None]]],
):
self._server = server
self._request_handlers = request_handlers
self._notification_handlers = notification_handlers
add_request_handler: Callable[
[str, Callable[[ServerRequestContext[LifespanResultT], Any], Awaitable[Any]]], None
],
has_handler: Callable[[str], bool],
) -> None:
self._add_request_handler = add_request_handler
self._has_handler = has_handler
self._task_support: TaskSupport | None = None
@property
@ -66,16 +67,13 @@ class ExperimentalHandlers:
def update_capabilities(self, capabilities: ServerCapabilities) -> None:
# Only add tasks capability if handlers are registered
if not any(
req_type in self._request_handlers
for req_type in [GetTaskRequest, ListTasksRequest, CancelTaskRequest, GetTaskPayloadRequest]
):
if not any(self._has_handler(method) for method in ["tasks/get", "tasks/list", "tasks/cancel", "tasks/result"]):
return
capabilities.tasks = ServerTasksCapability()
if ListTasksRequest in self._request_handlers:
if self._has_handler("tasks/list"):
capabilities.tasks.list = TasksListCapability()
if CancelTaskRequest in self._request_handlers:
if self._has_handler("tasks/cancel"):
capabilities.tasks.cancel = TasksCancelCapability()
capabilities.tasks.requests = ServerTasksRequestsCapability(
@ -86,15 +84,35 @@ class ExperimentalHandlers:
self,
store: TaskStore | None = None,
queue: TaskMessageQueue | None = None,
*,
on_get_task: Callable[[ServerRequestContext[LifespanResultT], GetTaskRequestParams], Awaitable[GetTaskResult]]
| None = None,
on_task_result: Callable[
[ServerRequestContext[LifespanResultT], GetTaskPayloadRequestParams], Awaitable[GetTaskPayloadResult]
]
| None = None,
on_list_tasks: Callable[
[ServerRequestContext[LifespanResultT], PaginatedRequestParams | None], Awaitable[ListTasksResult]
]
| None = None,
on_cancel_task: Callable[
[ServerRequestContext[LifespanResultT], CancelTaskRequestParams], Awaitable[CancelTaskResult]
]
| None = None,
) -> TaskSupport:
"""Enable experimental task support.
This sets up the task infrastructure and auto-registers default handlers
for tasks/get, tasks/result, tasks/list, and tasks/cancel.
This sets up the task infrastructure and registers handlers for
tasks/get, tasks/result, tasks/list, and tasks/cancel. Custom handlers
can be provided via the on_* kwargs; any not provided will use defaults.
Args:
store: Custom TaskStore implementation (defaults to InMemoryTaskStore)
queue: Custom TaskMessageQueue implementation (defaults to InMemoryTaskMessageQueue)
on_get_task: Custom handler for tasks/get
on_task_result: Custom handler for tasks/result
on_list_tasks: Custom handler for tasks/list
on_cancel_task: Custom handler for tasks/cancel
Returns:
The TaskSupport configuration object
@ -117,24 +135,27 @@ class ExperimentalHandlers:
queue = InMemoryTaskMessageQueue()
self._task_support = TaskSupport(store=store, queue=queue)
task_support = self._task_support
# Auto-register default handlers
self._register_default_task_handlers()
# Register user-provided handlers
if on_get_task is not None:
self._add_request_handler("tasks/get", on_get_task)
if on_task_result is not None:
self._add_request_handler("tasks/result", on_task_result)
if on_list_tasks is not None:
self._add_request_handler("tasks/list", on_list_tasks)
if on_cancel_task is not None:
self._add_request_handler("tasks/cancel", on_cancel_task)
return self._task_support
# Fill in defaults for any not provided
if not self._has_handler("tasks/get"):
def _register_default_task_handlers(self) -> None:
"""Register default handlers for task operations."""
assert self._task_support is not None
support = self._task_support
# Register get_task handler if not already registered
if GetTaskRequest not in self._request_handlers:
async def _default_get_task(req: GetTaskRequest) -> ServerResult:
task = await support.store.get_task(req.params.task_id)
async def _default_get_task(
ctx: ServerRequestContext[LifespanResultT], params: GetTaskRequestParams
) -> GetTaskResult:
task = await task_support.store.get_task(params.task_id)
if task is None:
raise MCPError(code=INVALID_PARAMS, message=f"Task not found: {req.params.task_id}")
raise MCPError(code=INVALID_PARAMS, message=f"Task not found: {params.task_id}")
return GetTaskResult(
task_id=task.task_id,
status=task.status,
@ -145,136 +166,39 @@ class ExperimentalHandlers:
poll_interval=task.poll_interval,
)
self._request_handlers[GetTaskRequest] = _default_get_task
self._add_request_handler("tasks/get", _default_get_task)
# Register get_task_result handler if not already registered
if GetTaskPayloadRequest not in self._request_handlers:
if not self._has_handler("tasks/result"):
async def _default_get_task_result(req: GetTaskPayloadRequest) -> GetTaskPayloadResult:
ctx = self._server.request_context
result = await support.handler.handle(req, ctx.session, ctx.request_id)
async def _default_get_task_result(
ctx: ServerRequestContext[LifespanResultT], params: GetTaskPayloadRequestParams
) -> GetTaskPayloadResult:
assert ctx.request_id is not None
req = GetTaskPayloadRequest(params=params)
result = await task_support.handler.handle(req, ctx.session, ctx.request_id)
return result
self._request_handlers[GetTaskPayloadRequest] = _default_get_task_result
self._add_request_handler("tasks/result", _default_get_task_result)
# Register list_tasks handler if not already registered
if ListTasksRequest not in self._request_handlers:
if not self._has_handler("tasks/list"):
async def _default_list_tasks(req: ListTasksRequest) -> ListTasksResult:
cursor = req.params.cursor if req.params else None
tasks, next_cursor = await support.store.list_tasks(cursor)
async def _default_list_tasks(
ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None
) -> ListTasksResult:
cursor = params.cursor if params else None
tasks, next_cursor = await task_support.store.list_tasks(cursor)
return ListTasksResult(tasks=tasks, next_cursor=next_cursor)
self._request_handlers[ListTasksRequest] = _default_list_tasks
self._add_request_handler("tasks/list", _default_list_tasks)
# Register cancel_task handler if not already registered
if CancelTaskRequest not in self._request_handlers:
if not self._has_handler("tasks/cancel"):
async def _default_cancel_task(req: CancelTaskRequest) -> CancelTaskResult:
result = await cancel_task(support.store, req.params.task_id)
async def _default_cancel_task(
ctx: ServerRequestContext[LifespanResultT], params: CancelTaskRequestParams
) -> CancelTaskResult:
result = await cancel_task(task_support.store, params.task_id)
return result
self._request_handlers[CancelTaskRequest] = _default_cancel_task
self._add_request_handler("tasks/cancel", _default_cancel_task)
def list_tasks(
self,
) -> Callable[
[Callable[[ListTasksRequest], Awaitable[ListTasksResult]]],
Callable[[ListTasksRequest], Awaitable[ListTasksResult]],
]:
"""Register a handler for listing tasks.
WARNING: This API is experimental and may change without notice.
"""
def decorator(
func: Callable[[ListTasksRequest], Awaitable[ListTasksResult]],
) -> Callable[[ListTasksRequest], Awaitable[ListTasksResult]]:
logger.debug("Registering handler for ListTasksRequest")
wrapper = create_call_wrapper(func, ListTasksRequest)
async def handler(req: ListTasksRequest) -> ListTasksResult:
result = await wrapper(req)
return result
self._request_handlers[ListTasksRequest] = handler
return func
return decorator
def get_task(
self,
) -> Callable[
[Callable[[GetTaskRequest], Awaitable[GetTaskResult]]], Callable[[GetTaskRequest], Awaitable[GetTaskResult]]
]:
"""Register a handler for getting task status.
WARNING: This API is experimental and may change without notice.
"""
def decorator(
func: Callable[[GetTaskRequest], Awaitable[GetTaskResult]],
) -> Callable[[GetTaskRequest], Awaitable[GetTaskResult]]:
logger.debug("Registering handler for GetTaskRequest")
wrapper = create_call_wrapper(func, GetTaskRequest)
async def handler(req: GetTaskRequest) -> GetTaskResult:
result = await wrapper(req)
return result
self._request_handlers[GetTaskRequest] = handler
return func
return decorator
def get_task_result(
self,
) -> Callable[
[Callable[[GetTaskPayloadRequest], Awaitable[GetTaskPayloadResult]]],
Callable[[GetTaskPayloadRequest], Awaitable[GetTaskPayloadResult]],
]:
"""Register a handler for getting task results/payload.
WARNING: This API is experimental and may change without notice.
"""
def decorator(
func: Callable[[GetTaskPayloadRequest], Awaitable[GetTaskPayloadResult]],
) -> Callable[[GetTaskPayloadRequest], Awaitable[GetTaskPayloadResult]]:
logger.debug("Registering handler for GetTaskPayloadRequest")
wrapper = create_call_wrapper(func, GetTaskPayloadRequest)
async def handler(req: GetTaskPayloadRequest) -> GetTaskPayloadResult:
result = await wrapper(req)
return result
self._request_handlers[GetTaskPayloadRequest] = handler
return func
return decorator
def cancel_task(
self,
) -> Callable[
[Callable[[CancelTaskRequest], Awaitable[CancelTaskResult]]],
Callable[[CancelTaskRequest], Awaitable[CancelTaskResult]],
]:
"""Register a handler for cancelling tasks.
WARNING: This API is experimental and may change without notice.
"""
def decorator(
func: Callable[[CancelTaskRequest], Awaitable[CancelTaskResult]],
) -> Callable[[CancelTaskRequest], Awaitable[CancelTaskResult]]:
logger.debug("Registering handler for CancelTaskRequest")
wrapper = create_call_wrapper(func, CancelTaskRequest)
async def handler(req: CancelTaskRequest) -> CancelTaskResult:
result = await wrapper(req)
return result
self._request_handlers[CancelTaskRequest] = handler
return func
return decorator
return task_support

View file

@ -1,53 +0,0 @@
import inspect
from collections.abc import Callable
from typing import Any, TypeVar, get_type_hints
T = TypeVar("T")
R = TypeVar("R")
def create_call_wrapper(func: Callable[..., R], request_type: type[T]) -> Callable[[T], R]:
"""Create a wrapper function that knows how to call func with the request object.
Returns a wrapper function that takes the request and calls func appropriately.
The wrapper handles three calling patterns:
1. Positional-only parameter typed as request_type (no default): func(req)
2. Positional/keyword parameter typed as request_type (no default): func(**{param_name: req})
3. No request parameter or parameter with default: func()
"""
try:
sig = inspect.signature(func)
type_hints = get_type_hints(func)
except (ValueError, TypeError, NameError): # pragma: no cover
return lambda _: func()
# Check for positional-only parameter typed as request_type
for param_name, param in sig.parameters.items():
if param.kind == inspect.Parameter.POSITIONAL_ONLY:
param_type = type_hints.get(param_name)
if param_type == request_type: # pragma: no branch
# Check if it has a default - if so, treat as old style
if param.default is not inspect.Parameter.empty: # pragma: no cover
return lambda _: func()
# Found positional-only parameter with correct type and no default
return lambda req: func(req)
# Check for any positional/keyword parameter typed as request_type
for param_name, param in sig.parameters.items():
if param.kind in (inspect.Parameter.POSITIONAL_OR_KEYWORD, inspect.Parameter.KEYWORD_ONLY): # pragma: no branch
param_type = type_hints.get(param_name)
if param_type == request_type:
# Check if it has a default - if so, treat as old style
if param.default is not inspect.Parameter.empty: # pragma: no cover
return lambda _: func()
# Found keyword parameter with correct type and no default
# Need to capture param_name in closure properly
def make_keyword_wrapper(name: str) -> Callable[[Any], Any]:
return lambda req: func(**{name: req})
return make_keyword_wrapper(param_name)
# No request parameter found - use old style
return lambda _: func()

View file

@ -2,82 +2,49 @@
This module provides a framework for creating an MCP (Model Context Protocol) server.
It allows you to easily define and handle various types of requests and notifications
in an asynchronous manner.
using constructor-based handler registration.
Usage:
1. Create a Server instance:
server = Server("your_server_name")
1. Define handler functions:
async def my_list_tools(ctx, params):
return types.ListToolsResult(tools=[...])
2. Define request handlers using decorators:
@server.list_prompts()
async def handle_list_prompts(request: types.ListPromptsRequest) -> types.ListPromptsResult:
# Implementation
async def my_call_tool(ctx, params):
return types.CallToolResult(content=[...])
@server.get_prompt()
async def handle_get_prompt(
name: str, arguments: dict[str, str] | None
) -> types.GetPromptResult:
# Implementation
2. Create a Server instance with on_* handlers:
server = Server(
"your_server_name",
on_list_tools=my_list_tools,
on_call_tool=my_call_tool,
)
@server.list_tools()
async def handle_list_tools(request: types.ListToolsRequest) -> types.ListToolsResult:
# Implementation
@server.call_tool()
async def handle_call_tool(
name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
# Implementation
@server.list_resource_templates()
async def handle_list_resource_templates() -> list[types.ResourceTemplate]:
# Implementation
3. Define notification handlers if needed:
@server.progress_notification()
async def handle_progress(
progress_token: str | int, progress: float, total: float | None,
message: str | None
) -> None:
# Implementation
4. Run the server:
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,
InitializationOptions(
server_name="your_server_name",
server_version="your_version",
capabilities=server.get_capabilities(
notification_options=NotificationOptions(),
experimental_capabilities={},
),
),
server.create_initialization_options(),
)
asyncio.run(main())
The Server class provides methods to register handlers for various MCP requests and
notifications. It automatically manages the request context and handles incoming
messages from the client.
The Server class dispatches incoming requests and notifications to registered
handler callables by method string.
"""
from __future__ import annotations
import base64
import contextvars
import json
import logging
import warnings
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
from collections.abc import AsyncIterator, Awaitable, Callable
from contextlib import AbstractAsyncContextManager, AsyncExitStack, asynccontextmanager
from importlib.metadata import version as importlib_version
from typing import Any, Generic, TypeAlias, cast
from typing import Any, Generic
import anyio
import jsonschema
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from starlette.applications import Starlette
from starlette.middleware import Middleware
@ -94,30 +61,20 @@ from mcp.server.auth.settings import AuthSettings
from mcp.server.context import ServerRequestContext
from mcp.server.experimental.request_context import Experimental
from mcp.server.lowlevel.experimental import ExperimentalHandlers
from mcp.server.lowlevel.func_inspection import create_call_wrapper
from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.server.models import InitializationOptions
from mcp.server.session import ServerSession
from mcp.server.streamable_http import EventStore
from mcp.server.streamable_http_manager import StreamableHTTPASGIApp, StreamableHTTPSessionManager
from mcp.server.transport_security import TransportSecuritySettings
from mcp.shared.exceptions import MCPError, UrlElicitationRequiredError
from mcp.shared.exceptions import MCPError
from mcp.shared.message import ServerMessageMetadata, SessionMessage
from mcp.shared.session import RequestResponder
from mcp.shared.tool_name_validation import validate_and_warn_tool_name
logger = logging.getLogger(__name__)
LifespanResultT = TypeVar("LifespanResultT", default=Any)
RequestT = TypeVar("RequestT", default=Any)
# type aliases for tool call results
StructuredContent: TypeAlias = dict[str, Any]
UnstructuredContent: TypeAlias = Iterable[types.ContentBlock]
CombinationContent: TypeAlias = tuple[UnstructuredContent, StructuredContent]
# This will be properly typed in each Server instance's context
request_ctx: contextvars.ContextVar[ServerRequestContext[Any, Any]] = contextvars.ContextVar("request_ctx")
request_ctx: contextvars.ContextVar[ServerRequestContext[Any]] = contextvars.ContextVar("request_ctx")
class NotificationOptions:
@ -128,7 +85,7 @@ class NotificationOptions:
@asynccontextmanager
async def lifespan(_: Server[LifespanResultT, RequestT]) -> AsyncIterator[dict[str, Any]]:
async def lifespan(_: Server[LifespanResultT]) -> AsyncIterator[dict[str, Any]]:
"""Default lifespan context manager that does nothing.
Args:
@ -140,10 +97,15 @@ async def lifespan(_: Server[LifespanResultT, RequestT]) -> AsyncIterator[dict[s
yield {}
class Server(Generic[LifespanResultT, RequestT]):
async def _ping_handler(ctx: ServerRequestContext[Any], params: types.RequestParams | None) -> types.EmptyResult:
return types.EmptyResult()
class Server(Generic[LifespanResultT]):
def __init__(
self,
name: str,
*,
version: str | None = None,
title: str | None = None,
description: str | None = None,
@ -151,9 +113,80 @@ class Server(Generic[LifespanResultT, RequestT]):
website_url: str | None = None,
icons: list[types.Icon] | None = None,
lifespan: Callable[
[Server[LifespanResultT, RequestT]],
[Server[LifespanResultT]],
AbstractAsyncContextManager[LifespanResultT],
] = lifespan,
# Request handlers
on_list_tools: Callable[
[ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None],
Awaitable[types.ListToolsResult],
]
| None = None,
on_call_tool: Callable[
[ServerRequestContext[LifespanResultT], types.CallToolRequestParams],
Awaitable[types.CallToolResult | types.CreateTaskResult],
]
| None = None,
on_list_resources: Callable[
[ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None],
Awaitable[types.ListResourcesResult],
]
| None = None,
on_list_resource_templates: Callable[
[ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None],
Awaitable[types.ListResourceTemplatesResult],
]
| None = None,
on_read_resource: Callable[
[ServerRequestContext[LifespanResultT], types.ReadResourceRequestParams],
Awaitable[types.ReadResourceResult],
]
| None = None,
on_subscribe_resource: Callable[
[ServerRequestContext[LifespanResultT], types.SubscribeRequestParams],
Awaitable[types.EmptyResult],
]
| None = None,
on_unsubscribe_resource: Callable[
[ServerRequestContext[LifespanResultT], types.UnsubscribeRequestParams],
Awaitable[types.EmptyResult],
]
| None = None,
on_list_prompts: Callable[
[ServerRequestContext[LifespanResultT], types.PaginatedRequestParams | None],
Awaitable[types.ListPromptsResult],
]
| None = None,
on_get_prompt: Callable[
[ServerRequestContext[LifespanResultT], types.GetPromptRequestParams],
Awaitable[types.GetPromptResult],
]
| None = None,
on_completion: Callable[
[ServerRequestContext[LifespanResultT], types.CompleteRequestParams],
Awaitable[types.CompleteResult],
]
| None = None,
on_set_logging_level: Callable[
[ServerRequestContext[LifespanResultT], types.SetLevelRequestParams],
Awaitable[types.EmptyResult],
]
| None = None,
on_ping: Callable[
[ServerRequestContext[LifespanResultT], types.RequestParams | None],
Awaitable[types.EmptyResult],
] = _ping_handler,
# Notification handlers
on_roots_list_changed: Callable[
[ServerRequestContext[LifespanResultT], types.NotificationParams | None],
Awaitable[None],
]
| None = None,
on_progress: Callable[
[ServerRequestContext[LifespanResultT], types.ProgressNotificationParams],
Awaitable[None],
]
| None = None,
):
self.name = name
self.version = version
@ -163,15 +196,64 @@ class Server(Generic[LifespanResultT, RequestT]):
self.website_url = website_url
self.icons = icons
self.lifespan = lifespan
self.request_handlers: dict[type, Callable[..., Awaitable[types.ServerResult]]] = {
types.PingRequest: _ping_handler,
}
self.notification_handlers: dict[type, Callable[..., Awaitable[None]]] = {}
self._tool_cache: dict[str, types.Tool] = {}
self._experimental_handlers: ExperimentalHandlers | None = None
self._request_handlers: dict[str, Callable[[ServerRequestContext[LifespanResultT], Any], Awaitable[Any]]] = {}
self._notification_handlers: dict[
str, Callable[[ServerRequestContext[LifespanResultT], Any], Awaitable[None]]
] = {}
self._experimental_handlers: ExperimentalHandlers[LifespanResultT] | None = None
self._session_manager: StreamableHTTPSessionManager | None = None
logger.debug("Initializing server %r", name)
# Populate internal handler dicts from on_* kwargs
self._request_handlers.update(
{
method: handler
for method, handler in {
"ping": on_ping,
"prompts/list": on_list_prompts,
"prompts/get": on_get_prompt,
"resources/list": on_list_resources,
"resources/templates/list": on_list_resource_templates,
"resources/read": on_read_resource,
"resources/subscribe": on_subscribe_resource,
"resources/unsubscribe": on_unsubscribe_resource,
"tools/list": on_list_tools,
"tools/call": on_call_tool,
"logging/setLevel": on_set_logging_level,
"completion/complete": on_completion,
}.items()
if handler is not None
}
)
self._notification_handlers.update(
{
method: handler
for method, handler in {
"notifications/roots/list_changed": on_roots_list_changed,
"notifications/progress": on_progress,
}.items()
if handler is not None
}
)
def _add_request_handler(
self,
method: str,
handler: Callable[[ServerRequestContext[LifespanResultT], Any], Awaitable[Any]],
) -> None:
"""Add a request handler, silently replacing any existing handler for the same method."""
self._request_handlers[method] = handler
def _has_handler(self, method: str) -> bool:
"""Check if a handler is registered for the given method."""
return method in self._request_handlers or method in self._notification_handlers
# TODO: Rethink capabilities API. Currently capabilities are derived from registered
# handlers but require NotificationOptions to be passed externally for list_changed
# flags, and experimental_capabilities as a separate dict. Consider deriving capabilities
# entirely from server state (e.g. constructor params for list_changed) instead of
# requiring callers to assemble them at create_initialization_options() time.
def create_initialization_options(
self,
notification_options: NotificationOptions | None = None,
@ -214,25 +296,26 @@ class Server(Generic[LifespanResultT, RequestT]):
completions_capability = None
# Set prompt capabilities if handler exists
if types.ListPromptsRequest in self.request_handlers:
if "prompts/list" in self._request_handlers:
prompts_capability = types.PromptsCapability(list_changed=notification_options.prompts_changed)
# Set resource capabilities if handler exists
if types.ListResourcesRequest in self.request_handlers:
if "resources/list" in self._request_handlers:
resources_capability = types.ResourcesCapability(
subscribe=False, list_changed=notification_options.resources_changed
subscribe="resources/subscribe" in self._request_handlers,
list_changed=notification_options.resources_changed,
)
# Set tool capabilities if handler exists
if types.ListToolsRequest in self.request_handlers:
if "tools/list" in self._request_handlers:
tools_capability = types.ToolsCapability(list_changed=notification_options.tools_changed)
# Set logging capabilities if handler exists
if types.SetLevelRequest in self.request_handlers:
if "logging/setLevel" in self._request_handlers:
logging_capability = types.LoggingCapability()
# Set completions capabilities if handler exists
if types.CompleteRequest in self.request_handlers:
if "completion/complete" in self._request_handlers:
completions_capability = types.CompletionsCapability()
capabilities = types.ServerCapabilities(
@ -248,12 +331,7 @@ class Server(Generic[LifespanResultT, RequestT]):
return capabilities
@property
def request_context(self) -> ServerRequestContext[LifespanResultT, RequestT]:
"""If called outside of a request context, this will raise a LookupError."""
return request_ctx.get()
@property
def experimental(self) -> ExperimentalHandlers:
def experimental(self) -> ExperimentalHandlers[LifespanResultT]:
"""Experimental APIs for tasks and other features.
WARNING: These APIs are experimental and may change without notice.
@ -261,7 +339,10 @@ class Server(Generic[LifespanResultT, RequestT]):
# We create this inline so we only add these capabilities _if_ they're actually used
if self._experimental_handlers is None:
self._experimental_handlers = ExperimentalHandlers(self, self.request_handlers, self.notification_handlers)
self._experimental_handlers = ExperimentalHandlers(
add_request_handler=self._add_request_handler,
has_handler=self._has_handler,
)
return self._experimental_handlers
@property
@ -278,374 +359,6 @@ class Server(Generic[LifespanResultT, RequestT]):
)
return self._session_manager # pragma: no cover
def list_prompts(self):
def decorator(
func: Callable[[], Awaitable[list[types.Prompt]]]
| Callable[[types.ListPromptsRequest], Awaitable[types.ListPromptsResult]],
):
logger.debug("Registering handler for PromptListRequest")
wrapper = create_call_wrapper(func, types.ListPromptsRequest)
async def handler(req: types.ListPromptsRequest):
result = await wrapper(req)
# Handle both old style (list[Prompt]) and new style (ListPromptsResult)
if isinstance(result, types.ListPromptsResult):
return result
else:
# Old style returns list[Prompt]
return types.ListPromptsResult(prompts=result)
self.request_handlers[types.ListPromptsRequest] = handler
return func
return decorator
def get_prompt(self):
def decorator(
func: Callable[[str, dict[str, str] | None], Awaitable[types.GetPromptResult]],
):
logger.debug("Registering handler for GetPromptRequest")
async def handler(req: types.GetPromptRequest):
prompt_get = await func(req.params.name, req.params.arguments)
return prompt_get
self.request_handlers[types.GetPromptRequest] = handler
return func
return decorator
def list_resources(self):
def decorator(
func: Callable[[], Awaitable[list[types.Resource]]]
| Callable[[types.ListResourcesRequest], Awaitable[types.ListResourcesResult]],
):
logger.debug("Registering handler for ListResourcesRequest")
wrapper = create_call_wrapper(func, types.ListResourcesRequest)
async def handler(req: types.ListResourcesRequest):
result = await wrapper(req)
# Handle both old style (list[Resource]) and new style (ListResourcesResult)
if isinstance(result, types.ListResourcesResult):
return result
else:
# Old style returns list[Resource]
return types.ListResourcesResult(resources=result)
self.request_handlers[types.ListResourcesRequest] = handler
return func
return decorator
def list_resource_templates(self):
def decorator(func: Callable[[], Awaitable[list[types.ResourceTemplate]]]):
logger.debug("Registering handler for ListResourceTemplatesRequest")
async def handler(_: Any):
templates = await func()
return types.ListResourceTemplatesResult(resource_templates=templates)
self.request_handlers[types.ListResourceTemplatesRequest] = handler
return func
return decorator
def read_resource(self):
def decorator(
func: Callable[[str], Awaitable[str | bytes | Iterable[ReadResourceContents]]],
):
logger.debug("Registering handler for ReadResourceRequest")
async def handler(req: types.ReadResourceRequest):
result = await func(req.params.uri)
def create_content(data: str | bytes, mime_type: str | None, meta: dict[str, Any] | None = None):
# Note: ResourceContents uses Field(alias="_meta"), so we must use the alias key
meta_kwargs: dict[str, Any] = {"_meta": meta} if meta is not None else {}
match data:
case str() as data:
return types.TextResourceContents(
uri=req.params.uri,
text=data,
mime_type=mime_type or "text/plain",
**meta_kwargs,
)
case bytes() as data: # pragma: no branch
return types.BlobResourceContents(
uri=req.params.uri,
blob=base64.b64encode(data).decode(),
mime_type=mime_type or "application/octet-stream",
**meta_kwargs,
)
match result:
case str() | bytes() as data: # pragma: lax no cover
warnings.warn(
"Returning str or bytes from read_resource is deprecated. "
"Use Iterable[ReadResourceContents] instead.",
DeprecationWarning,
stacklevel=2,
)
content = create_content(data, None)
case Iterable() as contents:
contents_list = [
create_content(
content_item.content, content_item.mime_type, getattr(content_item, "meta", None)
)
for content_item in contents
]
return types.ReadResourceResult(contents=contents_list)
case _: # pragma: no cover
raise ValueError(f"Unexpected return type from read_resource: {type(result)}")
return types.ReadResourceResult(contents=[content]) # pragma: no cover
self.request_handlers[types.ReadResourceRequest] = handler
return func
return decorator
def set_logging_level(self):
def decorator(func: Callable[[types.LoggingLevel], Awaitable[None]]):
logger.debug("Registering handler for SetLevelRequest")
async def handler(req: types.SetLevelRequest):
await func(req.params.level)
return types.EmptyResult()
self.request_handlers[types.SetLevelRequest] = handler
return func
return decorator
def subscribe_resource(self):
def decorator(func: Callable[[str], Awaitable[None]]):
logger.debug("Registering handler for SubscribeRequest")
async def handler(req: types.SubscribeRequest):
await func(req.params.uri)
return types.EmptyResult()
self.request_handlers[types.SubscribeRequest] = handler
return func
return decorator
def unsubscribe_resource(self):
def decorator(func: Callable[[str], Awaitable[None]]):
logger.debug("Registering handler for UnsubscribeRequest")
async def handler(req: types.UnsubscribeRequest):
await func(req.params.uri)
return types.EmptyResult()
self.request_handlers[types.UnsubscribeRequest] = handler
return func
return decorator
def list_tools(self):
def decorator(
func: Callable[[], Awaitable[list[types.Tool]]]
| Callable[[types.ListToolsRequest], Awaitable[types.ListToolsResult]],
):
logger.debug("Registering handler for ListToolsRequest")
wrapper = create_call_wrapper(func, types.ListToolsRequest)
async def handler(req: types.ListToolsRequest):
result = await wrapper(req)
# Handle both old style (list[Tool]) and new style (ListToolsResult)
if isinstance(result, types.ListToolsResult):
# Refresh the tool cache with returned tools
for tool in result.tools:
validate_and_warn_tool_name(tool.name)
self._tool_cache[tool.name] = tool
return result
else:
# Old style returns list[Tool]
# Clear and refresh the entire tool cache
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
def _make_error_result(self, error_message: str) -> types.CallToolResult:
"""Create a CallToolResult with an error."""
return types.CallToolResult(
content=[types.TextContent(type="text", text=error_message)],
is_error=True,
)
async def _get_cached_tool_definition(self, tool_name: str) -> types.Tool | None:
"""Get tool definition from cache, refreshing if necessary.
Returns the Tool object if found, None otherwise.
"""
if tool_name not in self._tool_cache:
if types.ListToolsRequest in self.request_handlers:
logger.debug("Tool cache miss for %s, refreshing cache", tool_name)
await self.request_handlers[types.ListToolsRequest](None)
tool = self._tool_cache.get(tool_name)
if tool is None:
logger.warning("Tool '%s' not listed, no validation will be performed", tool_name)
return tool
def call_tool(self, *, validate_input: bool = True):
"""Register a tool call handler.
Args:
validate_input: If True, validates input against inputSchema. Default is True.
The handler validates input against inputSchema (if validate_input=True), calls the tool function,
and builds a CallToolResult with the results:
- Unstructured content (iterable of ContentBlock): returned in content
- Structured content (dict): returned in structuredContent, serialized JSON text returned in content
- Both: returned in content and structuredContent
If outputSchema is defined, validates structuredContent or errors if missing.
"""
def decorator(
func: Callable[
[str, dict[str, Any]],
Awaitable[
UnstructuredContent
| StructuredContent
| CombinationContent
| types.CallToolResult
| types.CreateTaskResult
],
],
):
logger.debug("Registering handler for CallToolRequest")
async def handler(req: types.CallToolRequest):
try:
tool_name = req.params.name
arguments = req.params.arguments or {}
tool = await self._get_cached_tool_definition(tool_name)
# input validation
if validate_input and tool:
try:
jsonschema.validate(instance=arguments, schema=tool.input_schema)
except jsonschema.ValidationError as e:
return self._make_error_result(f"Input validation error: {e.message}")
# tool call
results = await func(tool_name, arguments)
# output normalization
unstructured_content: UnstructuredContent
maybe_structured_content: StructuredContent | None
if isinstance(results, types.CallToolResult):
return results
elif isinstance(results, types.CreateTaskResult):
# Task-augmented execution returns task info instead of result
return results
elif isinstance(results, tuple) and len(results) == 2:
# tool returned both structured and unstructured content
unstructured_content, maybe_structured_content = cast(CombinationContent, results)
elif isinstance(results, dict):
# tool returned structured content only
maybe_structured_content = cast(StructuredContent, results)
unstructured_content = [types.TextContent(type="text", text=json.dumps(results, indent=2))]
elif hasattr(results, "__iter__"):
# tool returned unstructured content only
unstructured_content = cast(UnstructuredContent, results)
maybe_structured_content = None
else: # pragma: no cover
return self._make_error_result(f"Unexpected return type from tool: {type(results).__name__}")
# output validation
if tool and tool.output_schema is not None:
if maybe_structured_content is None:
return self._make_error_result(
"Output validation error: outputSchema defined but no structured output returned"
)
else:
try:
jsonschema.validate(instance=maybe_structured_content, schema=tool.output_schema)
except jsonschema.ValidationError as e:
return self._make_error_result(f"Output validation error: {e.message}")
# result
return types.CallToolResult(
content=list(unstructured_content),
structured_content=maybe_structured_content,
is_error=False,
)
except UrlElicitationRequiredError:
# Re-raise UrlElicitationRequiredError so it can be properly handled
# by _handle_request, which converts it to an error response with code -32042
raise
except Exception as e:
return self._make_error_result(str(e))
self.request_handlers[types.CallToolRequest] = handler
return func
return decorator
def progress_notification(self):
def decorator(
func: Callable[[str | int, float, float | None, str | None], Awaitable[None]],
):
logger.debug("Registering handler for ProgressNotification")
async def handler(req: types.ProgressNotification):
await func(
req.params.progress_token,
req.params.progress,
req.params.total,
req.params.message,
)
self.notification_handlers[types.ProgressNotification] = handler
return func
return decorator
def completion(self):
"""Provides completions for prompts and resource templates"""
def decorator(
func: Callable[
[
types.PromptReference | types.ResourceTemplateReference,
types.CompletionArgument,
types.CompletionContext | None,
],
Awaitable[types.Completion | None],
],
):
logger.debug("Registering handler for CompleteRequest")
async def handler(req: types.CompleteRequest):
completion = await func(req.params.ref, req.params.argument, req.params.context)
return types.CompleteResult(
completion=completion
if completion is not None
else types.Completion(values=[], total=None, has_more=None),
)
self.request_handlers[types.CompleteRequest] = handler
return func
return decorator
async def run(
self,
read_stream: MemoryObjectReceiveStream[SessionMessage | Exception],
@ -715,7 +428,7 @@ class Server(Generic[LifespanResultT, RequestT]):
if raise_exceptions:
raise message
case _:
await self._handle_notification(message)
await self._handle_notification(message, session, lifespan_context)
for warning in w: # pragma: lax no cover
logger.info("Warning: %s: %s", warning.category.__name__, warning.message)
@ -730,10 +443,9 @@ class Server(Generic[LifespanResultT, RequestT]):
):
logger.info("Processing request of type %s", type(req).__name__)
if handler := self.request_handlers.get(type(req)):
if handler := self._request_handlers.get(req.method):
logger.debug("Dispatching request of type %s", type(req).__name__)
token = None
try:
# Extract request context and close_sse_stream from message metadata
request_data = None
@ -744,32 +456,32 @@ class Server(Generic[LifespanResultT, RequestT]):
close_sse_stream_cb = message.message_metadata.close_sse_stream
close_standalone_sse_stream_cb = message.message_metadata.close_standalone_sse_stream
# Set our global state that can be retrieved via
# app.get_request_context()
client_capabilities = session.client_params.capabilities if session.client_params else None
task_support = self._experimental_handlers.task_support if self._experimental_handlers else None
# Get task metadata from request params if present
task_metadata = None
if hasattr(req, "params") and req.params is not None:
task_metadata = getattr(req.params, "task", None)
token = request_ctx.set(
ServerRequestContext(
request_id=message.request_id,
meta=message.request_meta,
session=session,
lifespan_context=lifespan_context,
experimental=Experimental(
task_metadata=task_metadata,
_client_capabilities=client_capabilities,
_session=session,
_task_support=task_support,
),
request=request_data,
close_sse_stream=close_sse_stream_cb,
close_standalone_sse_stream=close_standalone_sse_stream_cb,
)
ctx = ServerRequestContext(
request_id=message.request_id,
meta=message.request_meta,
session=session,
lifespan_context=lifespan_context,
experimental=Experimental(
task_metadata=task_metadata,
_client_capabilities=client_capabilities,
_session=session,
_task_support=task_support,
),
request=request_data,
close_sse_stream=close_sse_stream_cb,
close_standalone_sse_stream=close_standalone_sse_stream_cb,
)
response = await handler(req)
token = request_ctx.set(ctx)
try:
response = await handler(ctx, req.params)
finally:
request_ctx.reset(token)
except MCPError as err:
response = err.error
except anyio.get_cancelled_exc_class():
@ -779,10 +491,6 @@ class Server(Generic[LifespanResultT, RequestT]):
if raise_exceptions: # pragma: no cover
raise err
response = types.ErrorData(code=0, message=str(err), data=None)
finally:
# Reset the global state after we are done
if token is not None: # pragma: no branch
request_ctx.reset(token)
await message.respond(response)
else: # pragma: no cover
@ -790,12 +498,29 @@ class Server(Generic[LifespanResultT, RequestT]):
logger.debug("Response sent")
async def _handle_notification(self, notify: Any):
if handler := self.notification_handlers.get(type(notify)): # type: ignore
async def _handle_notification(
self,
notify: types.ClientNotification,
session: ServerSession,
lifespan_context: LifespanResultT,
) -> None:
if handler := self._notification_handlers.get(notify.method):
logger.debug("Dispatching notification of type %s", type(notify).__name__)
try:
await handler(notify)
client_capabilities = session.client_params.capabilities if session.client_params else None
task_support = self._experimental_handlers.task_support if self._experimental_handlers else None
ctx = ServerRequestContext(
session=session,
lifespan_context=lifespan_context,
experimental=Experimental(
task_metadata=None,
_client_capabilities=client_capabilities,
_session=session,
_task_support=task_support,
),
)
await handler(ctx, notify.params)
except Exception: # pragma: no cover
logger.exception("Uncaught exception in notification handler")
@ -910,7 +635,3 @@ class Server(Generic[LifespanResultT, RequestT]):
middleware=middleware,
lifespan=lambda app: session_manager.run(),
)
async def _ping_handler(request: types.PingRequest) -> types.ServerResult:
return types.EmptyResult()

View file

@ -2,7 +2,9 @@
from __future__ import annotations
import base64
import inspect
import json
import re
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable, Sequence
from contextlib import AbstractAsyncContextManager, asynccontextmanager
@ -29,7 +31,7 @@ from mcp.server.context import LifespanContextT, RequestT, ServerRequestContext
from mcp.server.elicitation import ElicitationResult, ElicitSchemaModelT, UrlElicitationResult, elicit_with_validation
from mcp.server.elicitation import elicit_url as _elicit_url
from mcp.server.lowlevel.helper_types import ReadResourceContents
from mcp.server.lowlevel.server import LifespanResultT, Server
from mcp.server.lowlevel.server import LifespanResultT, Server, request_ctx
from mcp.server.lowlevel.server import lifespan as default_lifespan
from mcp.server.mcpserver.exceptions import ResourceError
from mcp.server.mcpserver.prompts import Prompt, PromptManager
@ -42,7 +44,30 @@ from mcp.server.stdio import stdio_server
from mcp.server.streamable_http import EventStore
from mcp.server.streamable_http_manager import StreamableHTTPSessionManager
from mcp.server.transport_security import TransportSecuritySettings
from mcp.types import Annotations, ContentBlock, GetPromptResult, Icon, ToolAnnotations
from mcp.shared.exceptions import MCPError
from mcp.types import (
Annotations,
BlobResourceContents,
CallToolRequestParams,
CallToolResult,
CompleteRequestParams,
CompleteResult,
Completion,
ContentBlock,
GetPromptRequestParams,
GetPromptResult,
Icon,
ListPromptsResult,
ListResourcesResult,
ListResourceTemplatesResult,
ListToolsResult,
PaginatedRequestParams,
ReadResourceRequestParams,
ReadResourceResult,
TextContent,
TextResourceContents,
ToolAnnotations,
)
from mcp.types import Prompt as MCPPrompt
from mcp.types import PromptArgument as MCPPromptArgument
from mcp.types import Resource as MCPResource
@ -91,9 +116,9 @@ class Settings(BaseSettings, Generic[LifespanResultT]):
def lifespan_wrapper(
app: MCPServer[LifespanResultT],
lifespan: Callable[[MCPServer[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]],
) -> Callable[[Server[LifespanResultT, Request]], AbstractAsyncContextManager[LifespanResultT]]:
) -> Callable[[Server[LifespanResultT]], AbstractAsyncContextManager[LifespanResultT]]:
@asynccontextmanager
async def wrap(_: Server[LifespanResultT, Request]) -> AsyncIterator[LifespanResultT]:
async def wrap(_: Server[LifespanResultT]) -> AsyncIterator[LifespanResultT]:
async with lifespan(app) as context:
yield context
@ -132,6 +157,9 @@ class MCPServer(Generic[LifespanResultT]):
auth=auth,
)
self._tool_manager = ToolManager(tools=tools, warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools)
self._resource_manager = ResourceManager(warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources)
self._prompt_manager = PromptManager(warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts)
self._lowlevel_server = Server(
name=name or "mcp-server",
title=title,
@ -140,13 +168,17 @@ class MCPServer(Generic[LifespanResultT]):
website_url=website_url,
icons=icons,
version=version,
on_list_tools=self._handle_list_tools,
on_call_tool=self._handle_call_tool,
on_list_resources=self._handle_list_resources,
on_read_resource=self._handle_read_resource,
on_list_resource_templates=self._handle_list_resource_templates,
on_list_prompts=self._handle_list_prompts,
on_get_prompt=self._handle_get_prompt,
# TODO(Marcelo): It seems there's a type mismatch between the lifespan type from an MCPServer and Server.
# We need to create a Lifespan type that is a generic on the server type, like Starlette does.
lifespan=(lifespan_wrapper(self, self.settings.lifespan) if self.settings.lifespan else default_lifespan), # type: ignore
)
self._tool_manager = ToolManager(tools=tools, warn_on_duplicate_tools=self.settings.warn_on_duplicate_tools)
self._resource_manager = ResourceManager(warn_on_duplicate_resources=self.settings.warn_on_duplicate_resources)
self._prompt_manager = PromptManager(warn_on_duplicate_prompts=self.settings.warn_on_duplicate_prompts)
# Validate auth configuration
if self.settings.auth is not None:
if auth_server_provider and token_verifier: # pragma: no cover
@ -164,9 +196,6 @@ class MCPServer(Generic[LifespanResultT]):
self._token_verifier = ProviderTokenVerifier(auth_server_provider)
self._custom_starlette_routes: list[Route] = []
# Set up MCP protocol handlers
self._setup_handlers()
# Configure logging
configure_logging(self.settings.log_level)
@ -263,18 +292,83 @@ class MCPServer(Generic[LifespanResultT]):
case "streamable-http": # pragma: no cover
anyio.run(lambda: self.run_streamable_http_async(**kwargs))
def _setup_handlers(self) -> None:
"""Set up core MCP protocol handlers."""
self._lowlevel_server.list_tools()(self.list_tools)
# Note: we disable the lowlevel server's input validation.
# MCPServer does ad hoc conversion of incoming data before validating -
# for now we preserve this for backwards compatibility.
self._lowlevel_server.call_tool(validate_input=False)(self.call_tool)
self._lowlevel_server.list_resources()(self.list_resources)
self._lowlevel_server.read_resource()(self.read_resource)
self._lowlevel_server.list_prompts()(self.list_prompts)
self._lowlevel_server.get_prompt()(self.get_prompt)
self._lowlevel_server.list_resource_templates()(self.list_resource_templates)
async def _handle_list_tools(
self, ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None
) -> ListToolsResult:
return ListToolsResult(tools=await self.list_tools())
async def _handle_call_tool(
self, ctx: ServerRequestContext[LifespanResultT], params: CallToolRequestParams
) -> CallToolResult:
try:
result = await self.call_tool(params.name, params.arguments or {})
except MCPError:
raise
except Exception as e:
return CallToolResult(content=[TextContent(type="text", text=str(e))], is_error=True)
if isinstance(result, CallToolResult):
return result
if isinstance(result, tuple) and len(result) == 2:
unstructured_content, structured_content = result
return CallToolResult(
content=list(unstructured_content), # type: ignore[arg-type]
structured_content=structured_content, # type: ignore[arg-type]
)
if isinstance(result, dict): # pragma: no cover
# TODO: this code path is unreachable — convert_result never returns a raw dict.
# The call_tool return type (Sequence[ContentBlock] | dict[str, Any]) is wrong
# and needs to be cleaned up.
return CallToolResult(
content=[TextContent(type="text", text=json.dumps(result, indent=2))],
structured_content=result,
)
return CallToolResult(content=list(result))
async def _handle_list_resources(
self, ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None
) -> ListResourcesResult:
return ListResourcesResult(resources=await self.list_resources())
async def _handle_read_resource(
self, ctx: ServerRequestContext[LifespanResultT], params: ReadResourceRequestParams
) -> ReadResourceResult:
results = await self.read_resource(params.uri)
contents: list[TextResourceContents | BlobResourceContents] = []
for item in results:
if isinstance(item.content, bytes):
contents.append(
BlobResourceContents(
uri=params.uri,
blob=base64.b64encode(item.content).decode(),
mime_type=item.mime_type or "application/octet-stream",
_meta=item.meta,
)
)
else:
contents.append(
TextResourceContents(
uri=params.uri,
text=item.content,
mime_type=item.mime_type or "text/plain",
_meta=item.meta,
)
)
return ReadResourceResult(contents=contents)
async def _handle_list_resource_templates(
self, ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None
) -> ListResourceTemplatesResult:
return ListResourceTemplatesResult(resource_templates=await self.list_resource_templates())
async def _handle_list_prompts(
self, ctx: ServerRequestContext[LifespanResultT], params: PaginatedRequestParams | None
) -> ListPromptsResult:
return ListPromptsResult(prompts=await self.list_prompts())
async def _handle_get_prompt(
self, ctx: ServerRequestContext[LifespanResultT], params: GetPromptRequestParams
) -> GetPromptResult:
return await self.get_prompt(params.name, params.arguments)
async def list_tools(self) -> list[MCPTool]:
"""List all available tools."""
@ -298,7 +392,7 @@ class MCPServer(Generic[LifespanResultT]):
during a request; outside a request, most methods will error.
"""
try:
request_context = self._lowlevel_server.request_context
request_context = request_ctx.get()
except LookupError:
request_context = None
return Context(request_context=request_context, mcp_server=self)
@ -486,7 +580,24 @@ class MCPServer(Generic[LifespanResultT]):
return Completion(values=["option1", "option2"])
return None
"""
return self._lowlevel_server.completion()
def decorator(func: _CallableT) -> _CallableT:
async def handler(
ctx: ServerRequestContext[LifespanResultT], params: CompleteRequestParams
) -> CompleteResult:
result = await func(params.ref, params.argument, params.context)
return CompleteResult(
completion=result if result is not None else Completion(values=[], total=None, has_more=None),
)
# TODO(maxisbey): remove private access — completion needs post-construction
# handler registration, find a better pattern for this
self._lowlevel_server._add_request_handler( # pyright: ignore[reportPrivateUsage]
"completion/complete", handler
)
return func
return decorator
def add_resource(self, resource: Resource) -> None:
"""Add a resource to the server.

View file

@ -6,30 +6,22 @@ used in MCP servers to interact with the client.
Common usage pattern:
```
server = Server(name)
@server.call_tool()
async def handle_tool_call(ctx: RequestContext, arguments: dict[str, Any]) -> Any:
async def handle_call_tool(ctx: RequestContext, params: CallToolRequestParams) -> CallToolResult:
# Check client capabilities before proceeding
if ctx.session.check_client_capability(
types.ClientCapabilities(experimental={"advanced_tools": dict()})
):
# Perform advanced tool operations
result = await perform_advanced_tool_operation(arguments)
result = await perform_advanced_tool_operation(params.arguments)
else:
# Fall back to basic tool operations
result = await perform_basic_tool_operation(arguments)
result = await perform_basic_tool_operation(params.arguments)
return result
@server.list_prompts()
async def handle_list_prompts(ctx: RequestContext) -> list[types.Prompt]:
# Access session for any necessary checks or operations
async def handle_list_prompts(ctx: RequestContext, params) -> ListPromptsResult:
if ctx.session.client_params:
# Customize prompts based on client initialization parameters
return generate_custom_prompts(ctx.session.client_params)
else:
return default_prompts
return ListPromptsResult(prompts=generate_custom_prompts(ctx.session.client_params))
return ListPromptsResult(prompts=default_prompts)
server = Server(name, on_call_tool=handle_call_tool, on_list_prompts=handle_list_prompts)
```
The ServerSession class is typically used internally by the Server class and should not

View file

@ -60,7 +60,7 @@ class StreamableHTTPSessionManager:
def __init__(
self,
app: Server[Any, Any],
app: Server[Any],
event_store: EventStore | None = None,
json_response: bool = False,
stateless: bool = False,

View file

@ -13,8 +13,12 @@ SessionT = TypeVar("SessionT", bound=BaseSession[Any, Any, Any, Any, Any])
@dataclass(kw_only=True)
class RequestContext(Generic[SessionT]):
"""Common context for handling incoming requests."""
"""Common context for handling incoming requests.
For request handlers, request_id is always populated.
For notification handlers, request_id is None.
"""
request_id: RequestId
meta: RequestParamsMeta | None
session: SessionT
request_id: RequestId | None = None
meta: RequestParamsMeta | None = None

View file

@ -72,9 +72,8 @@ async def cancel_task(
- Task is already in a terminal state (completed, failed, cancelled)
Example:
@server.experimental.cancel_task()
async def handle_cancel(request: CancelTaskRequest) -> CancelTaskResult:
return await cancel_task(store, request.params.taskId)
async def handle_cancel(ctx, params: CancelTaskRequestParams) -> CancelTaskResult:
return await cancel_task(store, params.task_id)
"""
task = await store.get_task(task_id)
if task is None:

View file

@ -6,6 +6,7 @@ to support transport-specific features like resumability.
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from mcp.types import JSONRPCMessage, RequestId
@ -30,8 +31,10 @@ class ServerMessageMetadata:
"""Metadata specific to server messages."""
related_request_id: RequestId | None = None
# Request-specific context (e.g., headers, auth info)
request_context: object | None = None
# Transport-specific request context (e.g. starlette Request for HTTP
# transports, None for stdio). Typed as Any because the server layer is
# transport-agnostic.
request_context: Any = None
# Callback to close SSE stream for the current request without terminating
close_sse_stream: CloseSSEStreamCallback | None = None
# Callback to close the standalone GET SSE stream (for unsolicited notifications)