4949 InitializeRequestParams ,
5050 InitializeResult ,
5151 JSONRPCError ,
52- JSONRPCRequest ,
5352 JSONRPCResponse ,
5453 RequestId ,
5554 RequestParams ,
@@ -183,20 +182,26 @@ async def aclose_shielded(connection: Connection) -> None:
183182 )
184183
185184
186- async def to_jsonrpc_response (request_id : RequestId , coro : Awaitable [dict [str , Any ]]) -> JSONRPCResponse | JSONRPCError :
185+ async def to_jsonrpc_response (
186+ request_id : RequestId , coro : Awaitable [dict [str , Any ]], * , raise_unhandled : bool = False
187+ ) -> JSONRPCResponse | JSONRPCError :
187188 """Await ``coro`` and wrap its outcome as the JSON-RPC reply for ``request_id``.
188189
189190 The exception-to-wire boundary for the request-per-call drivers
190191 (`serve_one`, the modern HTTP entry). `MCPError` and `ValidationError`
191192 map via the shared `handler_exception_to_error_data` ladder; any other
192193 exception is logged and surfaced as `INTERNAL_ERROR` so handler internals
193- never reach the wire.
194+ never reach the wire. Set ``raise_unhandled`` to let unmapped exceptions
195+ propagate instead of being sanitized — used by the in-process test path so
196+ handler tracebacks reach the caller.
194197 """
195198 try :
196199 result = await coro
197200 except Exception as exc :
198201 error = handler_exception_to_error_data (exc )
199202 if error is None :
203+ if raise_unhandled :
204+ raise
200205 logger .exception ("request handler raised" )
201206 error = ErrorData (code = INTERNAL_ERROR , message = "Internal server error" )
202207 return JSONRPCError (jsonrpc = "2.0" , id = request_id , error = error )
@@ -497,34 +502,45 @@ async def serve_loop(
497502
498503async def serve_one (
499504 server : Server [LifespanT ],
500- request : JSONRPCRequest ,
505+ dctx : DispatchContext [TransportContext ],
506+ method : str ,
507+ params : Mapping [str , Any ] | None ,
501508 * ,
502509 connection : Connection ,
503- dctx : DispatchContext [TransportContext ],
504510 lifespan_state : LifespanT ,
511+ raise_exceptions : bool = False ,
505512) -> JSONRPCResponse | JSONRPCError :
506- """Handle a single ``request`` and return its JSON-RPC reply.
507-
508- The single-exchange driver: builds the kernel, runs `on_request` once for
509- `request` under `dctx`, maps the outcome to a `JSONRPCResponse` /
510- `JSONRPCError` via `to_jsonrpc_response`, and tears down
511- `connection.exit_stack` (shielded) on the way out. The entry constructs
512- the (born-ready) `Connection` and the `dctx`; this only consumes them.
513+ """Handle a single request ``(method, params)`` and return its JSON-RPC reply.
514+
515+ The single-exchange driver: builds the kernel, runs `on_request` once under
516+ `dctx`, maps the outcome to a `JSONRPCResponse` / `JSONRPCError` via
517+ `to_jsonrpc_response`, and tears down `connection.exit_stack` (shielded) on
518+ the way out. The entry constructs the (born-ready) `Connection` and the
519+ `dctx`; this only consumes them. ``raise_exceptions`` lets unmapped handler
520+ exceptions propagate instead of being sanitized to `INTERNAL_ERROR`.
513521 """
514522 runner = ServerRunner (server , connection , lifespan_state )
515523 try :
516- return await to_jsonrpc_response (request .id , runner .on_request (dctx , request .method , request .params ))
524+ # Single-exchange driver only handles requests; both entries populate `request_id`.
525+ # TODO(L54): drop once `DispatchContext` is split so `OnRequest` carries a non-Optional id.
526+ assert dctx .request_id is not None
527+ return await to_jsonrpc_response (
528+ dctx .request_id , runner .on_request (dctx , method , params ), raise_unhandled = raise_exceptions
529+ )
517530 finally :
518531 await aclose_shielded (connection )
519532
520533
521- def modern_on_request (server : Server [LifespanT ], lifespan_state : LifespanT ) -> OnRequest :
534+ def modern_on_request (
535+ server : Server [LifespanT ], lifespan_state : LifespanT , * , raise_exceptions : bool = False
536+ ) -> OnRequest :
522537 """Return an `OnRequest` callback that serves each call via `serve_one` with a fresh per-request `Connection`.
523538
524539 Wire this into the server side of a `DirectDispatcher` peer-pair to drive an
525540 in-process server on the modern per-request-envelope path (each request
526541 carries protocol version, client info, and capabilities in `params._meta`;
527- no `initialize` handshake).
542+ no `initialize` handshake). ``raise_exceptions`` lets unmapped handler
543+ exceptions propagate to the caller for debuggable in-process testing.
528544 """
529545
530546 async def handle (
@@ -536,12 +552,15 @@ async def handle(
536552 meta .get (CLIENT_INFO_META_KEY ),
537553 meta .get (CLIENT_CAPABILITIES_META_KEY ),
538554 )
539- # `OnRequest` is invoked for requests only, so `request_id` is always set.
540- assert dctx .request_id is not None
541- req = JSONRPCRequest (
542- jsonrpc = "2.0" , id = dctx .request_id , method = method , params = dict (params ) if params is not None else None
555+ msg = await serve_one (
556+ server ,
557+ dctx ,
558+ method ,
559+ params ,
560+ connection = connection ,
561+ lifespan_state = lifespan_state ,
562+ raise_exceptions = raise_exceptions ,
543563 )
544- msg = await serve_one (server , req , connection = connection , dctx = dctx , lifespan_state = lifespan_state )
545564 if isinstance (msg , JSONRPCError ):
546565 raise MCPError (code = msg .error .code , message = msg .error .message , data = msg .error .data )
547566 return msg .result
0 commit comments