[v2] Server registry + per-connection state; ServerRunner consumes Server[L] directly by maxisbey · Pull Request #2562 · modelcontextprotocol/python-sdk · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions src/mcp/server/connection.py
36 changes: 26 additions & 10 deletions src/mcp/server/context.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from collections.abc import Awaitable, Callable
from collections.abc import Awaitable, Callable, Mapping
from dataclasses import dataclass
from typing import Any, Generic, Protocol

Expand Down Expand Up @@ -33,10 +33,9 @@ class ServerRequestContext(RequestContext[ServerSession], Generic[LifespanContex


LifespanT = TypeVar("LifespanT", default=Any, covariant=True)
TransportT = TypeVar("TransportT", bound=TransportContext, default=TransportContext, covariant=True)


class Context(BaseContext[TransportT], PeerMixin, TypedServerRequestMixin, Generic[LifespanT, TransportT]):
class Context(BaseContext[TransportContext], PeerMixin, TypedServerRequestMixin, Generic[LifespanT]):
"""Server-side per-request context.

Composes `BaseContext` (forwards to `DispatchContext`, satisfies `Outbound`),
Expand All @@ -50,7 +49,7 @@ class Context(BaseContext[TransportT], PeerMixin, TypedServerRequestMixin, Gener

def __init__(
self,
dctx: DispatchContext[TransportT],
dctx: DispatchContext[TransportContext],
*,
lifespan: LifespanT,
connection: Connection,
Expand All @@ -70,6 +69,23 @@ def connection(self) -> Connection:
"""The per-client `Connection` for this request's connection."""
return self._connection

@property
def session_id(self) -> str | None:
"""The transport's session id for this connection, when one exists.

Convenience for ``ctx.connection.session_id``. ``None`` on stdio and
stateless HTTP.
"""
return self._connection.session_id

@property
def headers(self) -> Mapping[str, str] | None:
"""Request headers carried by this message, when the transport has them.

Convenience for ``ctx.transport.headers``. ``None`` on stdio.
"""
return self.transport.headers

async def log(self, level: LoggingLevel, data: Any, logger: str | None = None, *, meta: Meta | None = None) -> None:
"""Send a request-scoped ``notifications/message`` log entry.

Expand All @@ -94,23 +110,23 @@ async def log(self, level: LoggingLevel, data: Any, logger: str | None = None, *
_MwLifespanT = TypeVar("_MwLifespanT", contravariant=True)


class ContextMiddleware(Protocol[_MwLifespanT]):
class ServerMiddleware(Protocol[_MwLifespanT]):
"""Context-tier middleware: ``(ctx, method, typed_params, call_next) -> result``.

Runs *inside* `ServerRunner._on_request` after params validation and
`Context` construction. Wraps registered handlers (including ``ping``) but
not ``initialize``, ``METHOD_NOT_FOUND``, or validation failures. Listed
outermost-first on `Server.middleware`.

`Server[L].middleware` holds `ContextMiddleware[L]`, so an app-specific
middleware sees `ctx.lifespan: L`. A reusable middleware (no app-specific
types) can be typed `ContextMiddleware[object]` — `Context` is covariant in
`LifespanT`, so it registers on any `Server[L]`.
`Server[L].middleware` holds `ServerMiddleware[L]`, so an app-specific
middleware sees `ctx.lifespan: L`. A reusable middleware can be typed
`ServerMiddleware[object]` — `Context` is covariant in `LifespanT`, so it
registers on any `Server[L]`.
"""

async def __call__(
self,
ctx: Context[_MwLifespanT, TransportContext],
ctx: Context[_MwLifespanT],
method: str,
params: BaseModel,
call_next: CallNext,
Expand Down
147 changes: 101 additions & 46 deletions src/mcp/server/lowlevel/server.py
Loading
Loading