Files
kefu/wechat_rpa/.venv/Lib/site-packages/mcp/server/context.py
T
2026-09-21 10:34:06 +08:00

195 lines
8.3 KiB
Python

import logging
from collections.abc import Awaitable, Callable, Mapping
from dataclasses import dataclass
from typing import Any, Generic, Protocol
from mcp_types import LoggingLevel, RequestId, RequestParamsMeta
from pydantic import BaseModel
from typing_extensions import TypeVar, deprecated
from mcp.server.connection import Connection, allowed_log_levels
from mcp.server.session import ServerSession
from mcp.shared.context import BaseContext
from mcp.shared.dispatcher import DispatchContext
from mcp.shared.exceptions import MCPDeprecationWarning
from mcp.shared.message import CloseSSEStreamCallback
from mcp.shared.peer import Meta
from mcp.shared.transport_context import TransportContext
logger = logging.getLogger(__name__)
# `Context.log`'s `logger` parameter (public API, the spec's logger-name
# field) shadows the module logger inside that method; this alias keeps it
# reachable there.
_logger = logger
# Invariant: parametrizes a mutable dataclass field; dict default matches the default lifespan.
LifespanContextT = TypeVar("LifespanContextT", default=dict[str, Any])
RequestT = TypeVar("RequestT", default=Any)
@dataclass(kw_only=True)
class ServerRequestContext(Generic[LifespanContextT, RequestT]):
"""Per-request context handed to lowlevel request and notification handlers.
Built by `ServerRunner._make_context` for each inbound message. Carries the
connection-scoped `ServerSession` (server-to-client requests and
notifications), per-request metadata, and any per-message data the
transport attached (the HTTP request, SSE stream-close callbacks).
"""
session: ServerSession
lifespan_context: LifespanContextT
protocol_version: str
method: str
params: Mapping[str, Any] | None = None
request_id: RequestId | None = None
meta: RequestParamsMeta | None = None
request: RequestT | None = None
close_sse_stream: CloseSSEStreamCallback | None = None
close_standalone_sse_stream: CloseSSEStreamCallback | None = None
# Covariant: `lifespan` is exposed read-only, so a `Context[AppState]` passes as `Context[object]`.
LifespanT_co = TypeVar("LifespanT_co", default=Any, covariant=True)
class Context(BaseContext[TransportContext], Generic[LifespanT_co]):
"""Server-side per-request context.
Extends `BaseContext` (transport metadata, the raw back-channel, progress
reporting) with `lifespan`, `connection`, and request-scoped `log`.
Not currently constructed by `ServerRunner`, which hands handlers a
`ServerRequestContext` instead.
"""
def __init__(
self,
dctx: DispatchContext[TransportContext],
*,
lifespan: LifespanT_co,
connection: Connection,
meta: RequestParamsMeta | None = None,
) -> None:
super().__init__(dctx, meta=meta)
self._lifespan = lifespan
self._connection = connection
# Same per-request log gate as `ServerSession`: fixed at construction
# from this request's `_meta` log-level opt-in and the connection's era.
self._allowed_log_levels = allowed_log_levels(connection.protocol_version, meta)
@property
def lifespan(self) -> LifespanT_co:
"""The server-wide lifespan output (what `Server(..., lifespan=...)` yielded)."""
return self._lifespan
@property
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
@deprecated("The logging capability is deprecated as of 2026-07-28 (SEP-2577).", category=MCPDeprecationWarning)
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.
Uses this request's back-channel (so the entry rides the request's SSE
stream in streamable HTTP), not the standalone stream - use
`ctx.connection.log(...)` for that.
On 2026-07-28+ delivery is a per-request opt-in: nothing is sent
unless this request's `_meta` carried the reserved log-level key, and
entries below the requested level are dropped (debug-logged).
Handshake versions send unconditionally, as before.
"""
if level not in self._allowed_log_levels:
_logger.debug("dropped notifications/message at %r: not opted in at that level on this request", level)
return
params: dict[str, Any] = {"level": level, "data": data}
if logger is not None:
params["logger"] = logger
if meta:
params["_meta"] = meta
await self.notify("notifications/message", params)
HandlerResult = BaseModel | dict[str, Any] | None
"""What a request handler (or middleware) may return. `ServerRunner` serializes
all three to a result dict."""
CallNext = Callable[["ServerRequestContext[Any, Any]"], Awaitable[HandlerResult]]
"""Invokes the rest of the chain with the given context. What a context
rewrite (`dataclasses.replace(ctx, ...)`) can alter depends on the tier:
`ServerMiddleware` runs before params validation, so its rewrites change what
the handler is invoked with; an `Extension` interceptor runs after, so its
rewrites change only what the handler observes on `ctx`."""
_MwLifespanT = TypeVar("_MwLifespanT")
class ServerMiddleware(Protocol[_MwLifespanT]):
"""Context-tier middleware: `(ctx, call_next) -> result`.
Runs at the top of `ServerRunner._on_request` / `_on_notify` after `ctx`
is built but before any validation, lookup, or handshake. Wraps every
inbound request and notification: `initialize`, the pre-init gate,
`METHOD_NOT_FOUND`, params validation, the handler call, and
`notifications/initialized` all run inside `call_next(ctx)`.
`notifications/cancelled` is observed too; the dispatcher applies the
cancellation itself, then forwards the notification. A request-side
failure reaches the middleware as a raised `MCPError` (or
`ValidationError` for malformed params) so observation/logging middleware
can record it. Listed outermost-first on `Server.middleware`.
The method and the raw inbound params are `ctx.method` and `ctx.params` (no
model validation has happened yet). To rewrite either before the handler
runs, pass an adjusted context: `await call_next(replace(ctx, params=...))`.
`ctx.request_id is None` distinguishes a notification from a request. For
notifications `call_next(ctx)` returns `None` (a dropped or unhandled
notification also returns `None`) and the middleware's own return value is
discarded.
!!! warning
`initialize` is handled inline - the dispatcher does not read
further inbound messages until the middleware chain returns. Awaiting a
server-to-client request (`ctx.session.send_request`, `send_ping`, ...)
while handling `initialize` therefore deadlocks the connection: the
response can never be dequeued. Send-and-forget notifications are safe.
`initialize` is observed but not rewritable: the post-chain handshake
commit reads the wire params, so to veto the handshake raise *before*
`call_next()`.
`Server[L].middleware` holds `ServerMiddleware[L]`, so an app-specific
middleware sees `ctx.lifespan_context: L`. While the context is the
mutable `ServerRequestContext` dataclass it is invariant in `L`, so a
reusable middleware should be typed `ServerMiddleware[Any]` to register on
any `Server[L]`.
"""
# TODO(maxisbey): once `_make_context` returns the (covariant) `Context[L]`
# again, restore `_MwLifespanT` to `contravariant=True` and retype `ctx`
# below to `Context[_MwLifespanT]` so reusable middleware can be
# `ServerMiddleware[object]` instead of `ServerMiddleware[Any]`.
async def __call__(
self,
ctx: ServerRequestContext[_MwLifespanT, Any],
call_next: CallNext,
) -> HandlerResult: ...