@@ -171,7 +171,6 @@ async def _on_request(
171171 meta = _extract_meta (params )
172172 version = self .connection .protocol_version
173173 ctx = self ._make_context (dctx , method , params , meta , version )
174- is_spec_method = method in _methods .SPEC_CLIENT_METHODS
175174
176175 async def _inner (ctx : ServerRequestContext [LifespanT , Any ]) -> HandlerResult :
177176 # Read method/params off `ctx` so a middleware that rewrote them via
@@ -190,7 +189,7 @@ async def _inner(ctx: ServerRequestContext[LifespanT, Any]) -> HandlerResult:
190189 # the gate become a per-version legacy path then. Initialize runs inline
191190 # (read loop parked), so awaiting the peer anywhere on this path deadlocks.
192191 if method == "initialize" :
193- return self ._handle_initialize (params )
192+ return self ._serialize ( method , version , self . _handle_initialize (params ) )
194193 # Methods without a handler are METHOD_NOT_FOUND regardless of
195194 # initialization state: JSON-RPC 2.0 reserves -32601 for "not
196195 # available on this server", and clients probing a server before
@@ -209,25 +208,14 @@ async def _inner(ctx: ServerRequestContext[LifespanT, Any]) -> HandlerResult:
209208 if isinstance (result , ErrorData ):
210209 # Raise inside the chain so middleware observes the failure.
211210 raise MCPError .from_error_data (result )
212- return result
211+ # Dump and serialize inside the chain so the OpenTelemetry span (the
212+ # outermost middleware) records a failing handler return shape too.
213+ return self ._serialize (method , version , result )
213214
214215 call = self ._compose_server_middleware (_inner )
216+ # `_inner` already produced the wire dict; a middleware that short-circuited
217+ # without `call_next` is trusted to return its own well-formed result.
215218 result = _dump_result (await call (ctx ))
216- # TODO(L56): reject resultType values outside {"complete", "input_required"} unless the
217- # corresponding extension is in this request's _meta clientCapabilities.extensions; the
218- # explicit MUST-reject is client-side (basic/index.mdx ResultType), this enforces it proactively.
219- if is_spec_method :
220- try :
221- result = _methods .serialize_server_result (method , version , result )
222- except KeyError :
223- # Middleware short-circuited a wrong-version spec method without
224- # calling `call_next`; it owns the result shape.
225- pass
226- except ValidationError :
227- # Server bug, not client fault. Detail stays in the server log:
228- # pydantic messages echo the result body.
229- logger .exception ("handler for %r returned an invalid result" , method )
230- raise MCPError (code = INTERNAL_ERROR , message = "Handler returned an invalid result" ) from None
231219 if method == "initialize" :
232220 # Commit only on chain success, so a middleware veto leaves no state.
233221 # Race-free: the read loop is parked until this call returns.
@@ -335,6 +323,28 @@ def _make_context(
335323 close_standalone_sse_stream = close_standalone_sse_stream ,
336324 )
337325
326+ @staticmethod
327+ def _serialize (method : str , version : str , result : HandlerResult ) -> dict [str , Any ]:
328+ """Dump a handler result to the wire dict, serializing spec methods.
329+
330+ Runs inside the middleware chain so the OpenTelemetry span observes a
331+ failing return shape (unsupported type, malformed spec result) as an
332+ error rather than closing on a request that the client sees fail.
333+ """
334+ dumped = _dump_result (result )
335+ # TODO(L56): reject resultType values outside {"complete", "input_required"} unless the
336+ # corresponding extension is in this request's _meta clientCapabilities.extensions; the
337+ # explicit MUST-reject is client-side (basic/index.mdx ResultType), this enforces it proactively.
338+ if method not in _methods .SPEC_CLIENT_METHODS :
339+ return dumped
340+ try :
341+ return _methods .serialize_server_result (method , version , dumped )
342+ except ValidationError :
343+ # Server bug, not client fault. Detail stays in the server log:
344+ # pydantic messages echo the result body.
345+ logger .exception ("handler for %r returned an invalid result" , method )
346+ raise MCPError (code = INTERNAL_ERROR , message = "Handler returned an invalid result" ) from None
347+
338348 @staticmethod
339349 def _negotiate_initialize (params : Mapping [str , Any ] | None ) -> tuple [InitializeRequestParams , str ]:
340350 """Validate `initialize` params and pick the protocol version."""
0 commit comments