Back to Python Sdk

serve-one

examples/stories/serve_one/README.md

2.0.0b12.7 KB
Original Source

serve-one

The kernel layer beneath MCPServer.run() / run_server_from_args. Every transport entry composes the same three pieces: a lowlevel.Server (the handler registry), a Connection (per-peer state), and a driver — serve_one for one request → result dict, or serve_connection for a dispatcher loop. This is what you write to bring up MCP over a custom transport. Uniquely, the server file here builds the stdio entry by hand instead of importing stories._hosting.

Run it

bash
# stdio (default — the client spawns server.py as a subprocess; its __main__
# is the hand-built serve_connection loop)
uv run python -m stories.serve_one.client

What to look at

  • server.py::handle_oneConnection.from_envelope(...) + serve_one(...) returns the raw result dict for one request. No handshake, no streams; the entry owns wire encoding and exception→error mapping.
  • server.py::mainJSONRPCDispatcher + Connection.for_loop(...) + serve_connection(...): exactly what Server.run() does internally for stdio.
  • server.py::SingleExchangeContext — the per-request DispatchContext a custom entry must supply. The SDK ships no public concrete class for this yet.
  • client.py — drives handle_one directly and asserts the raw result-dict shape (structuredContent / content), then proves the loop-mode driver works over the wire.

Caveats

  • Deep importsserve_one, serve_connection, and Connection are only reachable at mcp.server.runner / mcp.server.connection today; a shorter mcp.server.* re-export is tracked for beta.
  • Lowlevel-only. The drivers take a lowlevel.Server and MCPServer has no public accessor for its underlying one (_lowlevel_server is private), so there is no MCPServer-tier variant of this story. Build the lowlevel Server directly until that accessor lands.
  • No public DispatchContextSingleExchangeContext is hand-rolled boilerplate; a public helper (or a serve_one overload that builds one) is tracked for beta.
  • Lifespan — the transport entry enters server.lifespan(server) once and threads lifespan_state to every handle_one() call; never enter it per-request.
  • ServerRunner is kernel-internal; never construct it directly. The free-function drivers are the supported surface.

Spec

Architecture — lifecycle · 2026 versioning — discover

See also

legacy_routing/ (composing serve_one behind classify_inbound_request), dual_era/ (Connection.protocol_version in handlers).