"""`ServerRunner` - the per-connection handler kernel.

`ServerRunner` bridges the dispatch layer (`on_request` / `on_notify`, untyped
dicts) and the user's handler layer (typed `Context`, typed params). It is a
pure kernel: it holds a pre-populated `Connection` and reads
`connection.protocol_version` / `connection.outbound` as facts. Driving a
dispatcher loop and tearing down the connection live in the free-function
drivers (`serve_connection`, `serve_loop`, `serve_dual_era_loop`, `serve_one`);
the entry constructs the `Connection`, the driver tears it down.

`ServerRunner` holds a `Server` directly - `Server` is the registry.
"""

from __future__ import annotations

import contextvars
import logging
from collections.abc import AsyncIterator, Awaitable, Mapping
from contextlib import asynccontextmanager
from dataclasses import KW_ONLY, dataclass, replace
from functools import cached_property, partial
from typing import TYPE_CHECKING, Any, Generic, cast

import anyio
import anyio.abc
from mcp_types import (
    CLIENT_CAPABILITIES_META_KEY,
    CLIENT_INFO_META_KEY,
    CORE_RESULT_TYPES,
    INTERNAL_ERROR,
    INVALID_PARAMS,
    INVALID_REQUEST,
    METHOD_NOT_FOUND,
    PROTOCOL_VERSION_META_KEY,
    SERVER_INFO_META_KEY,
    UNSUPPORTED_PROTOCOL_VERSION,
    CacheableResult,
    ErrorData,
    Implementation,
    InitializeRequestParams,
    InitializeResult,
    JSONRPCRequest,
    RequestId,
    RequestParams,
    RequestParamsMeta,
    UnsupportedProtocolVersionErrorData,
)
from mcp_types import methods as _methods
from mcp_types.version import (
    HANDSHAKE_PROTOCOL_VERSIONS,
    LATEST_HANDSHAKE_VERSION,
    LATEST_MODERN_VERSION,
    MODERN_PROTOCOL_VERSIONS,
)
from pydantic import BaseModel, ValidationError
from typing_extensions import TypeVar

from mcp.server.caching import apply_cache_hint
from mcp.server.connection import Connection, NotifyOnlyOutbound
from mcp.server.context import CallNext, HandlerResult, ServerMiddleware, ServerRequestContext
from mcp.server.models import InitializationOptions
from mcp.server.session import ServerSession
from mcp.shared._context_streams import ContextReceiveStream
from mcp.shared._stream_protocols import ReadStream, WriteStream
from mcp.shared.dispatcher import CallOptions, DispatchContext, Dispatcher, OnNotify, OnRequest
from mcp.shared.exceptions import MCPError, NoBackChannelError
from mcp.shared.inbound import InboundLadderRejection, classify_inbound_request
from mcp.shared.jsonrpc_dispatcher import JSONRPCDispatcher, handler_exception_to_error_data
from mcp.shared.message import MessageMetadata, ServerMessageMetadata, SessionMessage
from mcp.shared.transport_context import TransportContext

if TYPE_CHECKING:
    from mcp.server.lowlevel.server import Server

__all__ = [
    "CallNext",
    "ServerMiddleware",
    "ServerRunner",
    "aclose_shielded",
    "modern_on_request",
    "serve_connection",
    "serve_dual_era_loop",
    "serve_loop",
    "serve_one",
]

logger = logging.getLogger(__name__)

LifespanT = TypeVar("LifespanT", default=Any)


_INIT_EXEMPT: frozenset[str] = frozenset({"ping"})

_EXIT_STACK_CLOSE_TIMEOUT: float = 5
"""Bound for `aclose_shielded`'s exit-stack unwind; a hung cleanup callback
must not wedge shutdown."""


def _extract_meta(params: Mapping[str, Any] | None) -> RequestParamsMeta | None:
    """Lift `_meta` from raw params; `None` when absent or malformed, so
    context construction is independent of params validity."""
    if not params or "_meta" not in params:
        return None
    try:
        return RequestParams.model_validate(params, by_name=False).meta
    except ValidationError:
        return None


def _dump_result(result: Any) -> dict[str, Any]:
    if result is None:
        return {}
    if isinstance(result, ErrorData):
        # ErrorData is a JSON-RPC error, not a success result. Handler returns
        # already raise in `_inner`; this catches middleware returning one.
        raise MCPError.from_error_data(result)
    if isinstance(result, BaseModel):
        return result.model_dump(by_alias=True, mode="json", exclude_none=True)
    if isinstance(result, dict):
        # Copied so callers own the returned dict: handlers and middleware may
        # retain the object they returned, and the outbound pipeline shapes the
        # wire form without reaching into anything the handler still holds.
        return dict(cast(dict[str, Any], result))
    raise TypeError(f"handler returned {type(result).__name__}; expected BaseModel, dict, or None")


async def aclose_shielded(connection: Connection) -> None:
    """Unwind ``connection.exit_stack`` under a shielded, bounded scope.

    Called from a driver's ``finally``: the shield lets per-connection cleanup
    callbacks run even when the driver itself is being cancelled, the
    `_EXIT_STACK_CLOSE_TIMEOUT` bound stops a hung callback wedging shutdown,
    and a raising callback is logged-and-swallowed so it never masks the
    driver's own exception.
    """
    with anyio.move_on_after(_EXIT_STACK_CLOSE_TIMEOUT, shield=True) as scope:
        try:
            await connection.exit_stack.aclose()
        except Exception:
            logger.exception("connection exit_stack cleanup raised")
    if scope.cancelled_caught:
        logger.warning(
            "connection exit_stack cleanup exceeded %s seconds; abandoning remaining callbacks",
            _EXIT_STACK_CLOSE_TIMEOUT,
        )


def _apply_middleware(
    middleware: ServerMiddleware[Any], call_next: CallNext, ctx: ServerRequestContext[Any, Any]
) -> Awaitable[HandlerResult]:
    """Adapt one middleware to the `CallNext` shape: bind `call_next`, take
    `ctx` at call time so a rewritten context flows down the chain."""
    return middleware(ctx, call_next)


@dataclass
class ServerRunner(Generic[LifespanT]):
    """Per-connection handler kernel. One instance per client connection."""

    server: Server[LifespanT]
    connection: Connection
    lifespan_state: LifespanT
    _: KW_ONLY
    init_options: InitializationOptions | None = None
    """`InitializeResult` payload. Defaults to `server.create_initialization_options()`."""

    @cached_property
    def on_request(self) -> OnRequest:
        return self._on_request

    @cached_property
    def on_notify(self) -> OnNotify:
        return self._on_notify

    async def _on_request(
        self,
        dctx: DispatchContext[TransportContext],
        method: str,
        params: Mapping[str, Any] | None,
    ) -> dict[str, Any]:
        meta = _extract_meta(params)
        version = self.connection.protocol_version
        ctx = self._make_context(dctx, method, params, meta, version)

        async def _inner(ctx: ServerRequestContext[LifespanT, Any]) -> HandlerResult:
            # Read method/params off `ctx` so a middleware that rewrote them via
            # `call_next(replace(ctx, ...))` reaches lookup and the handler.
            method, params = ctx.method, ctx.params
            # Pinned compat: spec methods are surface-validated before lookup,
            # so malformed params are INVALID_PARAMS even with no handler
            # registered. Custom methods miss the monolith map and fall through
            # to `entry.params_type` exactly as before.
            if method in _methods.SPEC_CLIENT_METHODS:
                try:
                    _methods.validate_client_request(method, version, params)
                except KeyError:
                    raise MCPError(code=METHOD_NOT_FOUND, message="Method not found", data=method) from None
            # TODO(L29): the 2026-07-28 spec drops the handshake; this branch and
            # the gate become a per-version legacy path then. Initialize runs inline
            # (read loop parked), so awaiting the peer anywhere on this path deadlocks.
            if method == "initialize":
                return self._serialize(method, version, self._handle_initialize(params))
            # Methods without a handler are METHOD_NOT_FOUND regardless of
            # initialization state: JSON-RPC 2.0 reserves -32601 for "not
            # available on this server", and clients probing a server before
            # the handshake key off that code. The init gate below therefore
            # only ever applies to methods the server actually serves.
            entry = self.server.get_request_handler(method)
            if entry is None:
                raise MCPError(code=METHOD_NOT_FOUND, message="Method not found", data=method)
            if not self.connection.initialize_accepted and method not in _INIT_EXEMPT:
                # Pinned compat: the same error shape the union validation produced.
                raise MCPError(code=INVALID_PARAMS, message="Invalid request parameters", data="")
            # Absent params validate as {} (required fields still reject), so
            # the handler receives the model with its defaults, never None.
            typed_params = entry.params_type.model_validate({} if params is None else params, by_name=False)
            result = await entry.handler(ctx, typed_params)
            if isinstance(result, ErrorData):
                # Raise inside the chain so middleware observes the failure.
                raise MCPError.from_error_data(result)
            # Shape for the wire inside the chain so the OpenTelemetry span (the
            # outermost middleware) records a failing handler return shape too.
            return self._serialize(method, version, result)

        call = self._compose_server_middleware(_inner)
        # `_inner` already produced the wire dict; a middleware that short-circuited
        # without `call_next` is trusted to return its own well-formed result -
        # including its response envelope. The pipeline never patches it up after
        # the fact.
        result = _dump_result(await call(ctx))
        if method == "initialize":
            # Commit only on chain success, so a middleware veto leaves no state.
            # Race-free: the read loop is parked until this call returns.
            # TODO: this re-reads the wire `params`, so a middleware that rewrote
            # `ctx.params` (or `ctx.method`, or short-circuited without `call_next`)
            # can leave `connection.protocol_version` out of step with the
            # `InitializeResult` `_inner` produced. Resolve when `initialize` becomes
            # a built-in handler so commit and result derive from one negotiation.
            self.connection.client_params, self.connection.protocol_version = self._negotiate_initialize(params)
        return result

    async def _on_notify(
        self,
        dctx: DispatchContext[TransportContext],
        method: str,
        params: Mapping[str, Any] | None,
    ) -> None:
        meta = _extract_meta(params)
        version = self.connection.protocol_version
        ctx = self._make_context(dctx, method, params, meta, version)

        async def _inner(ctx: ServerRequestContext[LifespanT, Any]) -> None:
            method, params = ctx.method, ctx.params
            if method in _methods.SPEC_CLIENT_NOTIFICATION_METHODS:
                try:
                    _methods.validate_client_notification(method, version, params)
                except KeyError:
                    logger.debug("dropped %r: not defined at %s", method, version)
                    return
                except ValidationError:
                    logger.warning("dropped %r: malformed params", method)
                    return
            if method == "notifications/initialized":
                # Surface validation above already rejected a malformed body, so
                # commit; fall through so a registered handler observes an
                # initialized connection.
                self.connection.initialized.set()
            elif not self.connection.initialize_accepted:
                logger.debug("dropped %s: received before initialization", method)
                return
            entry = self.server.get_notification_handler(method)
            if entry is None:
                logger.debug("no handler for notification %s", method)
                return
            # Same absent-params contract as requests.
            try:
                typed_params = entry.params_type.model_validate({} if params is None else params, by_name=False)
            except ValidationError:
                logger.warning("dropped %r: malformed params", method)
                return
            await entry.handler(ctx, typed_params)

        call = self._compose_server_middleware(_inner)
        try:
            await call(ctx)
        except Exception:
            # A crashing handler must not cancel the dispatcher's task group;
            # middleware saw the raise out of call_next() first.
            logger.exception("notification handler for %r raised", method)

    def _compose_server_middleware(self, inner: CallNext) -> CallNext:
        """Wrap `inner` in `Server.middleware`, outermost-first.

        Shared by `_on_request` and `_on_notify` so the same middleware chain
        observes every inbound message. The composed callable takes the `ctx`
        at call time, so a middleware can rewrite it for the rest of the chain.
        """
        call = inner
        for middleware in reversed(self.server.middleware):
            call = partial(_apply_middleware, middleware, call)
        return call

    def _make_context(
        self,
        dctx: DispatchContext[TransportContext],
        method: str,
        params: Mapping[str, Any] | None,
        meta: RequestParamsMeta | None,
        protocol_version: str,
    ) -> ServerRequestContext[LifespanT, Any]:
        # TODO(L54): remove for Context rework. Reads the SHTTP per-request
        # data off the raw `dctx.message_metadata` carrier; replace with the
        # per-transport context once that lands.
        md = dctx.message_metadata
        if isinstance(md, ServerMessageMetadata):
            request = md.request_context
            close_sse_stream = md.close_sse_stream
            close_standalone_sse_stream = md.close_standalone_sse_stream
        else:
            request = close_sse_stream = close_standalone_sse_stream = None
        # Per-request session: `dctx` is the request-scoped channel (auto-threads
        # its own request_id on streamable HTTP); the standalone channel is read
        # off `connection.outbound`. `related_request_id` on the public API selects.
        # `meta` carries a request's log-level opt-in for the session's log gate. A
        # notification has no request to opt in (and no response stream to carry
        # the log entry), so its `_meta` never opens the gate.
        session = ServerSession(dctx, self.connection, request_meta=meta if dctx.request_id is not None else None)
        return ServerRequestContext(
            session=session,
            lifespan_context=self.lifespan_state,
            method=method,
            params=params,
            request_id=dctx.request_id,
            meta=meta,
            protocol_version=protocol_version,
            request=request,
            close_sse_stream=close_sse_stream,
            close_standalone_sse_stream=close_standalone_sse_stream,
        )

    def _serialize(self, method: str, version: str, result: HandlerResult) -> dict[str, Any]:
        """Shape a handler result into its wire form: the outbound counterpart
        of the inbound classification ladder.

        One pass owns the whole response envelope, in order: cache hints fill
        `ttlMs`/`cacheScope` the handler left unset, core-vocabulary spec-method
        results are validated and sieved by the per-version surface (a claimed
        extension `resultType` shape is the extension's to own), and 2026-era
        results get the `serverInfo` `_meta` stamp (spec #3002). Runs inside the
        middleware chain so the OpenTelemetry span observes a failing return
        shape (unsupported type, malformed spec result) as an error rather
        than closing on a request that the client sees fail - and so a
        middleware that short-circuits without `call_next` owns its result,
        envelope included.
        """
        # MRTR carve-out: `input_required` interim results, typed or mapping, never get hints.
        if (hint := self.server.cache_hints.get(method)) is not None:
            if isinstance(result, CacheableResult):
                result = apply_cache_hint(result, hint)
            elif isinstance(result, Mapping) and not _methods.is_input_required(result):
                # Hint keys first so wire keys the handler set win, matching `apply_cache_hint` precedence.
                result = {"ttlMs": hint.ttl_ms, "cacheScope": hint.scope, **result}
        dumped = _dump_result(result)
        # A modern-era extension `resultType` (outside the core vocabulary) marks
        # a claimed shape owned by the extension that defined it: the per-version
        # surface doesn't describe it, so the sieve applies to core results only.
        # Legacy connections sieve everything - claimed shapes are 2026-era
        # vocabulary and cannot be delivered on a legacy wire (mirrors the
        # client-side ResultClaim rule).
        # TODO(L56): reject extension resultType values unless the corresponding
        # extension is in this request's _meta clientCapabilities.extensions; the
        # explicit MUST-reject is client-side (basic/index.mdx ResultType), this enforces it proactively.
        result_type = dumped.get("resultType")
        core_shape = (
            version not in MODERN_PROTOCOL_VERSIONS
            or not isinstance(result_type, str)
            or result_type in CORE_RESULT_TYPES
        )
        if method in _methods.SPEC_CLIENT_METHODS and core_shape:
            try:
                dumped = _methods.serialize_server_result(method, version, dumped)
            except ValidationError:
                # Server bug, not client fault. Detail stays in the server log:
                # pydantic messages echo the result body.
                logger.exception("handler for %r returned an invalid result", method)
                raise MCPError(code=INTERNAL_ERROR, message="Handler returned an invalid result") from None
        if version in MODERN_PROTOCOL_VERSIONS and dumped.get("resultType") is None:
            # Spec 2026-07-28: `Result.resultType` is required - servers MUST
            # include it (the absent-means-complete bridge is for clients of
            # older servers only). The sieve guarantees it for core methods;
            # this covers everything else: custom methods, extension methods,
            # and empty results.
            dumped["resultType"] = "complete"
        return self._stamp_server_info(version, dumped)

    def _stamp_server_info(self, version: str, result: dict[str, Any]) -> dict[str, Any]:
        """Fill the `serverInfo` `_meta` stamp on a 2026-era result (spec #3002).

        A handler-authored value wins; an explicit `null` reads as absent and
        is stamped over, mirroring the request-side `clientInfo` posture (a
        `null` is not a valid `Implementation`, so presence means a value). A
        non-mapping `_meta` is the handler's to own, and handshake-era results
        are never stamped. `result` is
        pipeline-owned (`_dump_result` copies dicts; the spec-method sieve
        re-dumps), but `_meta` may still be the handler's object, so the stamp
        replaces it rather than writing into it. `server_info_stamp` is a
        fresh dict per access, so the response never aliases server state.
        """
        if version not in MODERN_PROTOCOL_VERSIONS:
            return result
        raw_meta = result.get("_meta")
        if raw_meta is None:
            result["_meta"] = {SERVER_INFO_META_KEY: self.server.server_info_stamp}
        elif isinstance(raw_meta, dict):
            meta = cast("dict[str, Any]", raw_meta)
            if meta.get(SERVER_INFO_META_KEY) is None:
                result["_meta"] = {**meta, SERVER_INFO_META_KEY: self.server.server_info_stamp}
        return result

    @staticmethod
    def _negotiate_initialize(params: Mapping[str, Any] | None) -> tuple[InitializeRequestParams, str]:
        """Validate `initialize` params and pick the protocol version."""
        init = InitializeRequestParams.model_validate(params or {}, by_name=False)
        requested = init.protocol_version
        negotiated = requested if requested in HANDSHAKE_PROTOCOL_VERSIONS else LATEST_HANDSHAKE_VERSION
        return init, negotiated

    def _handle_initialize(self, params: Mapping[str, Any] | None) -> InitializeResult:
        """Build the `initialize` result; state commits later in `_on_request`."""
        _, negotiated = self._negotiate_initialize(params)
        opts = self.init_options if self.init_options is not None else self.server.create_initialization_options()
        return InitializeResult(
            protocol_version=negotiated,
            capabilities=opts.capabilities,
            server_info=Implementation(
                name=opts.server_name,
                title=opts.title,
                description=opts.description,
                version=opts.server_version,
                website_url=opts.website_url,
                icons=opts.icons,
            ),
            instructions=opts.instructions,
        )


async def serve_connection(
    server: Server[LifespanT],
    dispatcher: Dispatcher[Any],
    *,
    connection: Connection,
    lifespan_state: LifespanT,
    init_options: InitializationOptions | None = None,
    task_status: anyio.abc.TaskStatus[None] = anyio.TASK_STATUS_IGNORED,
) -> None:
    """Drive ``dispatcher`` until the underlying channel closes.

    The loop-mode driver: builds the kernel, hands `on_request`/`on_notify`
    to `dispatcher.run()`, and tears down `connection.exit_stack` (shielded)
    on the way out. The entry constructs the `Connection`; this only consumes
    it.
    """
    runner = ServerRunner(server, connection, lifespan_state, init_options=init_options)
    try:
        await dispatcher.run(runner.on_request, runner.on_notify, task_status=task_status)
    finally:
        await aclose_shielded(connection)


async def serve_loop(
    server: Server[LifespanT],
    read_stream: ReadStream[SessionMessage | Exception],
    write_stream: WriteStream[SessionMessage],
    *,
    lifespan_state: LifespanT,
    session_id: str | None = None,
    init_options: InitializationOptions | None = None,
    raise_exceptions: bool = False,
) -> None:
    """Drive ``server`` in handshake-only loop mode over a stream pair until the channel closes.

    Builds the loop-mode `JSONRPCDispatcher` + `Connection` and hands them to
    `serve_connection`. The streamable-HTTP manager (which owns its lifespan
    and serves the modern era on the single-exchange entry instead) calls
    this; `Server.run` drives `serve_dual_era_loop`, which extends the same
    dispatcher recipe (notably the `inline_methods={"initialize"}` rule) with
    era routing.
    """
    dispatcher: JSONRPCDispatcher[TransportContext] = JSONRPCDispatcher(
        read_stream,
        write_stream,
        raise_handler_exceptions=raise_exceptions,
        # Handle `initialize` inline so a client that pipelines it with the
        # next request (spec: SHOULD NOT, not MUST NOT) sees the initialized
        # state instead of failing the init-gate.
        inline_methods=frozenset({"initialize"}),
    )
    connection = Connection.for_loop(dispatcher, session_id=session_id)
    await serve_connection(
        server, dispatcher, connection=connection, lifespan_state=lifespan_state, init_options=init_options
    )


def _has_modern_envelope(params: Mapping[str, Any] | None) -> bool:
    """Whether `params._meta` carries the reserved protocol-version key.

    The `io.modelcontextprotocol/protocolVersion` key exists only in
    2026-07-28+ envelopes and its prefix is spec-reserved, so legacy traffic
    never mints it (a bare `_meta` is not evidence - legacy requests carry
    `progressToken` there). The version key alone is the signal, not the full
    required pair, so a half-built envelope still routes modern and gets the
    classifier's INVALID_PARAMS naming the missing key.
    """
    if not params:
        return False
    meta = params.get("_meta")
    return isinstance(meta, Mapping) and PROTOCOL_VERSION_META_KEY in meta


def _initialize_after_modern_data(params: Mapping[str, Any] | None) -> dict[str, Any]:
    """Error data for an `initialize` arriving on a modern-locked connection.

    The typed -32022 payload when the client's proposed version is parseable;
    otherwise just the supported list (the point is naming what we serve).
    """
    requested = (params or {}).get("protocolVersion")
    if isinstance(requested, str):
        return UnsupportedProtocolVersionErrorData(
            supported=list(MODERN_PROTOCOL_VERSIONS), requested=requested
        ).model_dump(mode="json")
    return {"supported": list(MODERN_PROTOCOL_VERSIONS)}


def modern_error_data(exc: Exception) -> ErrorData:
    """Map a modern request's handler exception to its wire `ErrorData`.

    The exception-to-wire fact shared by the modern entries (the
    single-exchange HTTP path and the dual-era stream loop), so an identical
    modern request fails identically on every transport: `MCPError` and
    `ValidationError` map via the shared `handler_exception_to_error_data`
    ladder; anything else is logged server-side and surfaced as a generic
    INTERNAL_ERROR so handler internals never reach the wire.
    """
    error = handler_exception_to_error_data(exc)
    if error is not None:
        return error
    logger.exception("modern request handler raised")
    return ErrorData(code=INTERNAL_ERROR, message="Internal server error")


@dataclass
class _NoServerRequestsDispatchContext:
    """Delegating `DispatchContext` that refuses server-initiated requests.

    Wraps the loop dispatcher's per-message context for modern-era dispatch:
    the modern protocol forbids server-initiated JSON-RPC requests, so
    `send_raw_request` refuses while notifications and progress still ride
    the duplex pipe.
    """

    _inner: DispatchContext[TransportContext]

    @property
    def transport(self) -> TransportContext:
        # Mask the per-message flag so the transport metadata agrees with this
        # wrapper's denial: the modern HTTP entry builds its context with
        # can_send_request=False, while the loop's default builder says True.
        transport = self._inner.transport
        return replace(transport, can_send_request=False) if transport.can_send_request else transport

    @property
    def can_send_request(self) -> bool:
        return False

    @property
    def request_id(self) -> RequestId | None:
        return self._inner.request_id

    @property
    def message_metadata(self) -> MessageMetadata:
        return self._inner.message_metadata

    @property
    def cancel_requested(self) -> anyio.Event:
        return self._inner.cancel_requested

    async def send_raw_request(
        self,
        method: str,
        params: Mapping[str, Any] | None,
        opts: CallOptions | None = None,
    ) -> dict[str, Any]:
        raise NoBackChannelError(method)

    async def notify(self, method: str, params: Mapping[str, Any] | None, opts: CallOptions | None = None) -> None:
        await self._inner.notify(method, params, opts)

    async def progress(self, progress: float, total: float | None = None, message: str | None = None) -> None:
        await self._inner.progress(progress, total, message)


async def serve_dual_era_loop(
    server: Server[LifespanT],
    read_stream: ReadStream[SessionMessage | Exception],
    write_stream: WriteStream[SessionMessage],
    *,
    lifespan_state: LifespanT,
    session_id: str | None = None,
    init_options: InitializationOptions | None = None,
    raise_exceptions: bool = False,
) -> None:
    """Drive `server` over a duplex stream pair, in the era the client opens with.

    The client's first request decides the connection's protocol era, once:
    a request carrying the 2026-07-28 per-request `_meta` envelope opens a
    modern connection, and anything else - the `initialize` handshake, which
    does not exist at 2026 versions even when a client stamps the envelope on
    it - opens a legacy one. The deciding frame is replayed into the chosen
    serving loop along with everything the client sent before it. A later
    claim from the other era is refused: `initialize` on a modern connection
    gets UNSUPPORTED_PROTOCOL_VERSION naming the served versions, and an
    enveloped request on a legacy connection gets INVALID_REQUEST.
    """
    # This loop owns both streams from the moment it is called, so the write
    # stream is closed even if the client leaves before sending any request.
    try:
        async with _replay_from_opening_request(read_stream) as (opening, replayed):
            opens_modern = (
                opening is not None and opening.method != "initialize" and _has_modern_envelope(opening.params)
            )
            if opens_modern:
                await _serve_modern_stream(
                    server, replayed, write_stream, lifespan_state=lifespan_state, raise_exceptions=raise_exceptions
                )
            else:
                await _serve_legacy_stream(
                    server,
                    replayed,
                    write_stream,
                    lifespan_state=lifespan_state,
                    session_id=session_id,
                    init_options=init_options,
                    raise_exceptions=raise_exceptions,
                )
    finally:
        await write_stream.aclose()


_PRE_REQUEST_REPLAY_LIMIT: int = 8
"""How many frames arriving ahead of the client's first request are kept
for the chosen era's loop (a bare `notifications/initialized` is the one that
matters); further ones are dropped and never decide the era."""


def _sender_context(stream: ReadStream[Any]) -> contextvars.Context:
    """The per-message sender context a context-aware stream carries, else the current one."""
    ctx = getattr(stream, "last_context", None)
    return ctx if ctx is not None else contextvars.copy_context()


@asynccontextmanager
async def _replay_from_opening_request(
    read_stream: ReadStream[SessionMessage | Exception],
) -> AsyncIterator[tuple[JSONRPCRequest | None, ReadStream[SessionMessage | Exception]]]:
    """Peek at the client's first request without consuming it.

    Yields that request together with a stream that replays it - preceded by
    up to `_PRE_REQUEST_REPLAY_LIMIT` earlier frames - and relays the rest of
    `read_stream` behind it, sender contexts included. The request is `None`
    if the channel closes before one arrives.
    """
    lead: list[tuple[contextvars.Context, SessionMessage | Exception]] = []
    opening_request: JSONRPCRequest | None = None
    replay_send, replay_receive = anyio.create_memory_object_stream[
        tuple[contextvars.Context, SessionMessage | Exception]
    ]()
    replayed = ContextReceiveStream(replay_receive)

    async def replay_then_relay() -> None:
        async with replay_send:
            for envelope in lead:
                await replay_send.send(envelope)
            try:
                async for item in read_stream:
                    await replay_send.send((_sender_context(read_stream), item))
            except anyio.ClosedResourceError:
                # Receive end closed under us (stateless SHTTP teardown); same as EOF.
                logger.debug("read stream closed by transport; treating as EOF")

    # This helper takes ownership of `read_stream` from the serving loop, so
    # every exit - including cancellation while awaiting the first request -
    # closes it and the replay channel.
    try:
        try:
            async for item in read_stream:
                if isinstance(item, SessionMessage) and isinstance(item.message, JSONRPCRequest):
                    opening_request = item.message
                elif len(lead) >= _PRE_REQUEST_REPLAY_LIMIT:
                    logger.debug("dropped a frame received before the first request: %r", item)
                    continue
                lead.append((_sender_context(read_stream), item))
                if opening_request is not None:
                    break
        except anyio.ClosedResourceError:
            # Receive end closed under us (stateless SHTTP teardown); same as EOF.
            logger.debug("read stream closed by transport; treating as EOF")
        async with anyio.create_task_group() as tg:
            tg.start_soon(replay_then_relay)
            yield opening_request, replayed
            tg.cancel_scope.cancel()
    finally:
        await read_stream.aclose()
        replay_send.close()
        replay_receive.close()


async def _serve_legacy_stream(
    server: Server[LifespanT],
    read_stream: ReadStream[SessionMessage | Exception],
    write_stream: WriteStream[SessionMessage],
    *,
    lifespan_state: LifespanT,
    session_id: str | None,
    init_options: InitializationOptions | None,
    raise_exceptions: bool,
) -> None:
    """Serve a 2025 handshake connection; enveloped requests are refused."""
    dispatcher: JSONRPCDispatcher[TransportContext] = JSONRPCDispatcher(
        read_stream,
        write_stream,
        raise_handler_exceptions=raise_exceptions,
        # `initialize` inline for the same pipelining reason as `serve_loop`.
        inline_methods=frozenset({"initialize"}),
    )
    connection = Connection.for_loop(dispatcher, session_id=session_id)
    runner = ServerRunner(server, connection, lifespan_state, init_options=init_options)

    async def on_request(
        dctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None
    ) -> dict[str, Any]:
        if method != "initialize" and _has_modern_envelope(params):
            raise MCPError(
                code=INVALID_REQUEST,
                message="this connection serves the handshake protocol era; "
                "requests carrying the 2026-07-28 envelope are not accepted on it",
            )
        return await runner.on_request(dctx, method, params)

    try:
        await dispatcher.run(on_request, runner.on_notify)
    finally:
        await aclose_shielded(connection)


async def _serve_modern_stream(
    server: Server[LifespanT],
    read_stream: ReadStream[SessionMessage | Exception],
    write_stream: WriteStream[SessionMessage],
    *,
    lifespan_state: LifespanT,
    raise_exceptions: bool,
) -> None:
    """Serve a 2026-07-28 connection: every request carries its own envelope."""
    dispatcher: JSONRPCDispatcher[TransportContext] = JSONRPCDispatcher(
        read_stream, write_stream, raise_handler_exceptions=raise_exceptions
    )
    outbound = NotifyOnlyOutbound(dispatcher)

    async def on_request(
        dctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None
    ) -> dict[str, Any]:
        if method == "initialize":
            raise MCPError(
                code=UNSUPPORTED_PROTOCOL_VERSION,
                message="connection is serving the 2026-07-28 protocol; the initialize handshake is not accepted",
                data=_initialize_after_modern_data(params),
            )
        route = classify_inbound_request({"method": method, "params": params})
        if isinstance(route, InboundLadderRejection):
            raise MCPError(code=route.code, message=route.message, data=route.data)
        connection = Connection.from_envelope(
            route.protocol_version, route.client_info, route.client_capabilities, outbound=outbound
        )
        try:
            return await serve_one(
                server,
                _NoServerRequestsDispatchContext(dctx),
                method,
                params,
                connection=connection,
                lifespan_state=lifespan_state,
            )
        except (MCPError, ValidationError):
            # The dispatcher's shared ladder maps these to the wire error.
            raise
        except Exception as exc:
            if raise_exceptions:
                raise
            error = modern_error_data(exc)
            raise MCPError(code=error.code, message=error.message, data=error.data) from exc

    async def on_notify(dctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None) -> None:
        # The envelope is request-only, so a notification runs at the latest
        # served version; the modern protocol has nothing version-specific here.
        connection = Connection.from_envelope(LATEST_MODERN_VERSION, None, None, outbound=outbound)
        notify_runner = ServerRunner(server, connection, lifespan_state)
        try:
            await notify_runner.on_notify(_NoServerRequestsDispatchContext(dctx), method, params)
        finally:
            await aclose_shielded(connection)

    await dispatcher.run(on_request, on_notify)


async def serve_one(
    server: Server[LifespanT],
    dctx: DispatchContext[TransportContext],
    method: str,
    params: Mapping[str, Any] | None,
    *,
    connection: Connection,
    lifespan_state: LifespanT,
) -> dict[str, Any]:
    """Handle a single request ``(method, params)`` and return its result dict.

    The single-exchange driver: builds the kernel, runs `on_request` once under
    `dctx`, and tears down `connection.exit_stack` (shielded) on the way out.
    The entry constructs the (born-ready) `Connection` and the `dctx`; this
    only consumes them.

    Raises whatever the handler chain raises (`MCPError` / `ValidationError` /
    unmapped); callers own the exception-to-wire mapping.
    """
    runner = ServerRunner(server, connection, lifespan_state)
    try:
        return await runner.on_request(dctx, method, params)
    finally:
        await aclose_shielded(connection)


def modern_on_request(server: Server[LifespanT], lifespan_state: LifespanT) -> OnRequest:
    """Return an `OnRequest` callback that serves each call via `serve_one` with a fresh per-request `Connection`.

    Wire this into the server side of a `DirectDispatcher` peer-pair to drive an
    in-process server on the modern per-request-envelope path (each request
    carries protocol version, client info, and capabilities in `params._meta`;
    no `initialize` handshake). The dispatch context is wrapped in the
    server-requests denial, so the modern prohibition on server-initiated
    JSON-RPC requests holds on this entry like on the others. Like `serve_one`,
    this raises whatever the handler chain raises - the dispatcher owns the
    exception-to-error mapping.
    """

    async def handle(
        dctx: DispatchContext[TransportContext], method: str, params: Mapping[str, Any] | None
    ) -> dict[str, Any]:
        meta = (params or {}).get("_meta", {})
        connection = Connection.from_envelope(
            meta.get(PROTOCOL_VERSION_META_KEY, LATEST_MODERN_VERSION),
            meta.get(CLIENT_INFO_META_KEY),
            meta.get(CLIENT_CAPABILITIES_META_KEY),
        )
        return await serve_one(
            server,
            _NoServerRequestsDispatchContext(dctx),
            method,
            params,
            connection=connection,
            lifespan_state=lifespan_state,
        )

    return handle
