Server-side 2026-07-28 stateless support: classifier, driver split, server/discover (#2928)
This commit is contained in:
parent
44724284b3
commit
2397319a68
1 changed files with 2256 additions and 1088 deletions
|
|
@ -49,7 +49,7 @@ from mcp.types import (
|
|||
RequestId,
|
||||
)
|
||||
|
||||
__all__ = ["JSONRPCDispatcher"]
|
||||
__all__ = ["JSONRPCDispatcher", "handler_exception_to_error_data"]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -67,6 +67,23 @@ PeerCancelMode = Literal["interrupt", "signal"]
|
|||
the handler's scope; `"signal"` only sets `ctx.cancel_requested`."""
|
||||
|
||||
|
||||
def handler_exception_to_error_data(exc: BaseException) -> ErrorData | None:
|
||||
"""Map a handler-raised exception to its wire `ErrorData`.
|
||||
|
||||
The two rungs every dispatcher shares: an `MCPError` carries its own
|
||||
`ErrorData`; a pydantic `ValidationError` is the spec's INVALID_PARAMS
|
||||
with empty ``data`` (no pydantic text on the wire). Returns ``None`` for
|
||||
any other exception so each caller applies its own catch-all -
|
||||
`JSONRPCDispatcher` currently pins ``code=0`` for v1 compat,
|
||||
`to_jsonrpc_response` uses `INTERNAL_ERROR`.
|
||||
"""
|
||||
if isinstance(exc, MCPError):
|
||||
return exc.error
|
||||
if isinstance(exc, ValidationError):
|
||||
return ErrorData(code=INVALID_PARAMS, message="Invalid request parameters", data="")
|
||||
return None
|
||||
|
||||
|
||||
def _coerce_id(request_id: RequestId) -> RequestId:
|
||||
"""Coerce a stringified int request ID back to int so a peer-echoed ID still correlates (matches the TS SDK)."""
|
||||
if isinstance(request_id, str):
|
||||
|
|
@ -667,7 +684,7 @@ class JSONRPCDispatcher(Dispatcher[TransportT]):
|
|||
# anyio absorbs the scope's own cancel at __exit__, and
|
||||
# `cancelled_caught` (unlike `cancel_called`) guarantees the
|
||||
# result write above did not happen - no double response.
|
||||
# TODO(maxisbey): spec says SHOULD NOT respond after cancel;
|
||||
# TODO(L38): spec says SHOULD NOT respond after cancel;
|
||||
# the existing server always has, so match that for now.
|
||||
answer_write_started = True
|
||||
await self._write_error(req.id, ErrorData(code=0, message="Request cancelled"))
|
||||
|
|
@ -684,21 +701,17 @@ class JSONRPCDispatcher(Dispatcher[TransportT]):
|
|||
describe=f"shutdown error response for request {req.id!r}",
|
||||
)
|
||||
raise
|
||||
except MCPError as e:
|
||||
await self._write_error(req.id, e.error)
|
||||
except ValidationError:
|
||||
# TODO(maxisbey): data="" pins existing-server compat (no pydantic
|
||||
# text on the wire); revisit per the suite's divergence entry.
|
||||
await self._write_error(
|
||||
req.id, ErrorData(code=INVALID_PARAMS, message="Invalid request parameters", data="")
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception("handler for %r raised", req.method)
|
||||
# TODO(maxisbey): code=0 pins existing-server compat; JSON-RPC says
|
||||
# INTERNAL_ERROR. Revisit per the suite's divergence entry.
|
||||
await self._write_error(req.id, ErrorData(code=0, message=str(e)))
|
||||
if self._raise_handler_exceptions:
|
||||
raise
|
||||
error = handler_exception_to_error_data(e)
|
||||
if error is not None:
|
||||
await self._write_error(req.id, error)
|
||||
else:
|
||||
logger.exception("handler for %r raised", req.method)
|
||||
# TODO(L58): code=0 pins existing-server compat; JSON-RPC says
|
||||
# INTERNAL_ERROR. Revisit per the suite's divergence entry.
|
||||
await self._write_error(req.id, ErrorData(code=0, message=str(e)))
|
||||
if self._raise_handler_exceptions:
|
||||
raise
|
||||
# No `_in_flight` pop here: the inner finally covers every path, and a late pop could evict a reused id.
|
||||
|
||||
def _allocate_id(self) -> int:
|
||||
|
|
|
|||
Loading…
Reference in a new issue