refactor: replace lowlevel Server decorators with on_* constructor kwargs (#1985)
This commit is contained in:
parent
d6d3ad9a7c
commit
0a22a9dc33
14 changed files with 3563 additions and 5481 deletions
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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?",
|
||||
|
|
|
|||
|
|
@ -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__(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
from .server import NotificationOptions, Server
|
||||
|
||||
__all__ = ["Server", "NotificationOptions"]
|
||||
__all__ = ["NotificationOptions", "Server"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue