From e6944a5fc0d5aaa7e37a90afd4366552d8b675ea Mon Sep 17 00:00:00 2001 From: Mohit Kumar Date: Tue, 25 Aug 2026 23:17:38 +0530 Subject: [PATCH] fix(server): expose request cancellation on MCPServer context --- src/mcp/server/context.py | 4 +++- src/mcp/server/mcpserver/context.py | 6 ++++++ src/mcp/server/runner.py | 1 + tests/server/mcpserver/test_server.py | 27 +++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/mcp/server/context.py b/src/mcp/server/context.py index bfcb9c9ca4..a7a4ace0fb 100644 --- a/src/mcp/server/context.py +++ b/src/mcp/server/context.py @@ -1,8 +1,9 @@ import logging from collections.abc import Awaitable, Callable, Mapping -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Any, Generic, Protocol +import anyio from mcp_types import LoggingLevel, RequestId, RequestParamsMeta from pydantic import BaseModel from typing_extensions import TypeVar, deprecated @@ -41,6 +42,7 @@ class ServerRequestContext(Generic[LifespanContextT, RequestT]): lifespan_context: LifespanContextT protocol_version: str method: str + cancel_requested: anyio.Event = field(default_factory=anyio.Event) params: Mapping[str, Any] | None = None request_id: RequestId | None = None meta: RequestParamsMeta | None = None diff --git a/src/mcp/server/mcpserver/context.py b/src/mcp/server/mcpserver/context.py index 07c4799dc1..d9ac6d3c27 100644 --- a/src/mcp/server/mcpserver/context.py +++ b/src/mcp/server/mcpserver/context.py @@ -3,6 +3,7 @@ from collections.abc import Iterable, Mapping from typing import TYPE_CHECKING, Any, Generic, cast +import anyio from mcp_types import ClientCapabilities, InputRequiredResult, InputResponseRequestParams, InputResponses, LoggingLevel from pydantic import AnyUrl, BaseModel from typing_extensions import deprecated @@ -98,6 +99,11 @@ def request_context(self) -> ServerRequestContext[LifespanContextT, RequestT]: raise ValueError("Context is not available outside of a request") return self._request_context + @property + def cancel_requested(self) -> anyio.Event: + """Set when the client requests cancellation of the current request.""" + return self.request_context.cancel_requested + def _nested_invocation(self) -> Context[LifespanContextT, RequestT]: """A Context for invoking another handler's function from inside this request. diff --git a/src/mcp/server/runner.py b/src/mcp/server/runner.py index 26e8efbe57..9dcfd27481 100644 --- a/src/mcp/server/runner.py +++ b/src/mcp/server/runner.py @@ -329,6 +329,7 @@ def _make_context( session=session, lifespan_context=self.lifespan_state, method=method, + cancel_requested=dctx.cancel_requested, params=params, request_id=dctx.request_id, meta=meta, diff --git a/tests/server/mcpserver/test_server.py b/tests/server/mcpserver/test_server.py index 3f90ce1368..36d4875b0c 100644 --- a/tests/server/mcpserver/test_server.py +++ b/tests/server/mcpserver/test_server.py @@ -1970,6 +1970,33 @@ def _request_context(request: object | None) -> ServerRequestContext[None, objec ) +def test_context_cancel_requested_exposes_request_cancellation_event(): + cancellation_event = anyio.Event() + + request_context = ServerRequestContext( + session=AsyncMock(), + method="tools/call", + lifespan_context=None, + protocol_version="2025-11-25", + cancel_requested=cancellation_event, + ) + + ctx = Context(request_context=request_context, mcp_server=MagicMock()) + + assert ctx.cancel_requested is cancellation_event + + cancellation_event.set() + + assert ctx.cancel_requested.is_set() + + +def test_context_cancel_requested_raises_outside_request(): + ctx = Context(mcp_server=MagicMock()) + + with pytest.raises(ValueError, match="Context is not available outside of a request"): + _ = ctx.cancel_requested + + def test_context_headers_returns_request_headers(): request = SimpleNamespace(headers={"x-github-user": "octocat"}) ctx = Context(request_context=_request_context(request), mcp_server=MagicMock())