Per-connection handler kernel. One instance per client connection.
| 188 | |
| 189 | @dataclass |
| 190 | class ServerRunner(Generic[LifespanT]): |
| 191 | """Per-connection handler kernel. One instance per client connection.""" |
| 192 | |
| 193 | server: Server[LifespanT] |
| 194 | connection: Connection |
| 195 | lifespan_state: LifespanT |
| 196 | _: KW_ONLY |
| 197 | init_options: InitializationOptions | None = None |
| 198 | """`InitializeResult` payload. Defaults to `server.create_initialization_options()`.""" |
| 199 | dispatch_middleware: Sequence[DispatchMiddleware] = (otel_middleware,) |
| 200 | |
| 201 | @cached_property |
| 202 | def on_request(self) -> OnRequest: |
| 203 | """`_on_request` wrapped in `dispatch_middleware`, outermost-first. |
| 204 | |
| 205 | Dispatch-tier middleware sees raw `(dctx, method, params) -> dict` and |
| 206 | wraps everything - initialize, METHOD_NOT_FOUND, validation failures |
| 207 | included. |
| 208 | """ |
| 209 | return reduce( |
| 210 | lambda handler, middleware: middleware(handler), reversed(self.dispatch_middleware), self._on_request |
| 211 | ) |
| 212 | |
| 213 | @cached_property |
| 214 | def on_notify(self) -> OnNotify: |
| 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 |
no outgoing calls