(
self,
dctx: DispatchContext[TransportContext],
method: str,
params: Mapping[str, Any] | None,
)
| 215 | return self._on_notify |
| 216 | |
| 217 | async def _on_request( |
| 218 | self, |
| 219 | dctx: DispatchContext[TransportContext], |
| 220 | method: str, |
| 221 | params: Mapping[str, Any] | None, |
| 222 | ) -> dict[str, Any]: |
| 223 | meta = _extract_meta(params) |
| 224 | version = self.connection.protocol_version |
| 225 | ctx = self._make_context(dctx, method, params, meta, version) |
| 226 | is_spec_method = method in _methods.SPEC_CLIENT_METHODS |
| 227 | |
| 228 | async def _inner(ctx: ServerRequestContext[LifespanT, Any]) -> HandlerResult: |
| 229 | # Read method/params off `ctx` so a middleware that rewrote them via |
| 230 | # `call_next(replace(ctx, ...))` reaches lookup and the handler. |
| 231 | method, params = ctx.method, ctx.params |
| 232 | # Pinned compat: spec methods are surface-validated before lookup, |
| 233 | # so malformed params are INVALID_PARAMS even with no handler |
| 234 | # registered. Custom methods miss the monolith map and fall through |
| 235 | # to `entry.params_type` exactly as before. |
| 236 | if method in _methods.SPEC_CLIENT_METHODS: |
| 237 | try: |
| 238 | _methods.validate_client_request(method, version, params) |
| 239 | except KeyError: |
| 240 | raise MCPError(code=METHOD_NOT_FOUND, message="Method not found", data=method) from None |
| 241 | # TODO(L29): the 2026-07-28 spec drops the handshake; this branch and |
| 242 | # the gate become a per-version legacy path then. Initialize runs inline |
| 243 | # (read loop parked), so awaiting the peer anywhere on this path deadlocks. |
| 244 | if method == "initialize": |
| 245 | return self._handle_initialize(params) |
| 246 | # Methods without a handler are METHOD_NOT_FOUND regardless of |
| 247 | # initialization state: JSON-RPC 2.0 reserves -32601 for "not |
| 248 | # available on this server", and clients probing a server before |
| 249 | # the handshake key off that code. The init gate below therefore |
| 250 | # only ever applies to methods the server actually serves. |
| 251 | entry = self.server.get_request_handler(method) |
| 252 | if entry is None: |
| 253 | raise MCPError(code=METHOD_NOT_FOUND, message="Method not found", data=method) |
| 254 | if not self.connection.initialize_accepted and method not in _INIT_EXEMPT: |
| 255 | # Pinned compat: the same error shape the union validation produced. |
| 256 | raise MCPError(code=INVALID_PARAMS, message="Invalid request parameters", data="") |
| 257 | # Absent params validate as {} (required fields still reject), so |
| 258 | # the handler receives the model with its defaults, never None. |
| 259 | typed_params = entry.params_type.model_validate({} if params is None else params, by_name=False) |
| 260 | result = await entry.handler(ctx, typed_params) |
| 261 | if isinstance(result, ErrorData): |
| 262 | # Raise inside the chain so middleware observes the failure. |
| 263 | raise MCPError.from_error_data(result) |
| 264 | return result |
| 265 | |
| 266 | call = self._compose_server_middleware(_inner) |
| 267 | result = _dump_result(await call(ctx)) |
| 268 | # TODO(L56): reject resultType values outside {"complete", "input_required"} unless the |
| 269 | # corresponding extension is in this request's _meta clientCapabilities.extensions; the |
| 270 | # explicit MUST-reject is client-side (basic/index.mdx ResultType), this enforces it proactively. |
| 271 | if is_spec_method: |
| 272 | try: |
| 273 | result = _methods.serialize_server_result(method, version, result) |
| 274 | except KeyError: |
no test coverage detected