chore: import upstream snapshot with attribution
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
Deploy Docs / deploy-docs (push) Failing after 1s
Conformance Tests / client-conformance (push) Failing after 3s
Conformance Tests / server-conformance (push) Failing after 1s
GitHub Actions Security Analysis / zizmor (push) Failing after 1s
CI / checks (push) Failing after 59m20s
CI / all-green (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
# dual-era
|
||||
|
||||
One server factory, both protocol eras. A `mode="legacy"` client runs the
|
||||
`initialize` handshake; a `mode="auto"` client probes `server/discover` and
|
||||
adopts the 2026 stateless era — the same `greet` tool answers both and reports
|
||||
which era served it via `ctx.request_context.protocol_version`. **Start here**
|
||||
when migrating a v1 server: the entry owns the era decision, the server body
|
||||
stays era-agnostic.
|
||||
|
||||
## Run it
|
||||
|
||||
```bash
|
||||
# over HTTP — the same /mcp endpoint serves both eras; the client self-hosts
|
||||
# the server on a free port, runs, then tears it down
|
||||
uv run python -m stories.dual_era.client --http
|
||||
# same, against the lowlevel-API server variant
|
||||
uv run python -m stories.dual_era.client --http --server server_lowlevel
|
||||
```
|
||||
|
||||
The bare stdio invocation (`uv run python -m stories.dual_era.client`) is
|
||||
legacy-only until the SDK's stdio entry can negotiate the era, so the modern
|
||||
leg fails there today — run over `--http`.
|
||||
|
||||
## What to look at
|
||||
|
||||
- `client.py` — both connections are visible, against the same `targets()`
|
||||
factory: `Client(targets(), mode=mode)` (default `"auto"`, the
|
||||
discover-then-fallback ladder) and `Client(targets(), mode="legacy")` (forces
|
||||
the `initialize` handshake). The era decision is one explicit `mode=` argument
|
||||
at construction; no date strings appear in the body.
|
||||
- `client.py` — `client.protocol_version` / `client.server_info` /
|
||||
`client.server_capabilities` are era-neutral: populated by `initialize` *or*
|
||||
`server/discover`, whichever ran.
|
||||
- `server.py` — `ctx.request_context.protocol_version` is the era branch key
|
||||
(lowlevel: `ctx.protocol_version` directly). Compare against
|
||||
`MODERN_PROTOCOL_VERSIONS`, never a date literal.
|
||||
- **Where to read the negotiated version.** One value, three read paths:
|
||||
`client.protocol_version` on the client after connect; `ctx.protocol_version`
|
||||
inside a lowlevel handler; `ctx.request_context.protocol_version` inside an
|
||||
`MCPServer` handler.
|
||||
|
||||
## Caveats
|
||||
|
||||
- `ctx.request_context.protocol_version` is the current way to read the
|
||||
negotiated version; a later release will shorten it to `ctx.transport.*`.
|
||||
- Over HTTP the built-in era branch is currently header-only — a 2026 client
|
||||
that omits the `MCP-Protocol-Version` header is mis-routed to the legacy
|
||||
path. The body-primary classifier lands in a later release.
|
||||
|
||||
## Spec
|
||||
|
||||
- [Versioning — backward compatibility](https://modelcontextprotocol.io/specification/draft/basic/versioning)
|
||||
- [`server/discover`](https://modelcontextprotocol.io/specification/draft/server/discover)
|
||||
|
||||
## See also
|
||||
|
||||
`legacy_routing/` (route eras yourself), `reconnect/` (persist `DiscoverResult`
|
||||
for zero-RTT reconnect).
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Connect to the same server factory twice — once per era, so `main` takes `targets` — and assert both are served."""
|
||||
|
||||
import mcp_types as types
|
||||
from mcp_types.version import LATEST_HANDSHAKE_VERSION, LATEST_MODERN_VERSION
|
||||
|
||||
from mcp.client import Client
|
||||
from stories._harness import TargetFactory, run_client
|
||||
|
||||
|
||||
async def main(targets: TargetFactory, *, mode: str = "auto") -> None:
|
||||
# ── modern arm: the caller's mode (the real-user "auto" default) probes
|
||||
# ``server/discover`` and adopts the result — no ``initialize`` handshake runs.
|
||||
# The version/info/capabilities accessors are era-neutral.
|
||||
async with Client(targets(), mode=mode) as modern:
|
||||
assert modern.protocol_version == LATEST_MODERN_VERSION
|
||||
assert modern.server_info.name == "dual-era-example"
|
||||
assert modern.server_capabilities.tools is not None
|
||||
|
||||
listed = await modern.list_tools()
|
||||
assert [t.name for t in listed.tools] == ["greet"]
|
||||
|
||||
result = await modern.call_tool("greet", {"name": "2026 client"})
|
||||
first = result.content[0]
|
||||
assert isinstance(first, types.TextContent)
|
||||
assert first.text == f"Hello, 2026 client! (served on the modern era at {LATEST_MODERN_VERSION})"
|
||||
|
||||
# ── legacy arm: a fresh connection to the SAME server, pinned to the handshake era.
|
||||
# The same accessors are populated identically — here by ``initialize``.
|
||||
async with Client(targets(), mode="legacy") as legacy:
|
||||
assert legacy.protocol_version == LATEST_HANDSHAKE_VERSION
|
||||
assert legacy.server_info.name == "dual-era-example"
|
||||
assert legacy.server_capabilities.tools is not None
|
||||
|
||||
result = await legacy.call_tool("greet", {"name": "2025 client"})
|
||||
first = result.content[0]
|
||||
assert isinstance(first, types.TextContent)
|
||||
assert first.text == f"Hello, 2025 client! (served on the legacy era at {LATEST_HANDSHAKE_VERSION})"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_client(main)
|
||||
@@ -0,0 +1,25 @@
|
||||
"""One MCPServer factory that serves both the 2025 handshake era and the 2026 stateless era."""
|
||||
|
||||
from mcp_types.version import MODERN_PROTOCOL_VERSIONS
|
||||
|
||||
from mcp.server.mcpserver import Context, MCPServer
|
||||
from stories._hosting import run_server_from_args
|
||||
|
||||
|
||||
def build_server() -> MCPServer:
|
||||
# The same factory serves both eras with no configuration. Which era a request is
|
||||
# on is decided by the entry point / transport, never by the server.
|
||||
mcp = MCPServer("dual-era-example", instructions="A small dual-era demo server.")
|
||||
|
||||
@mcp.tool()
|
||||
async def greet(name: str, ctx: Context) -> str:
|
||||
"""Greet the caller and report which protocol era served the request."""
|
||||
pv = ctx.request_context.protocol_version
|
||||
era = "modern" if pv in MODERN_PROTOCOL_VERSIONS else "legacy"
|
||||
return f"Hello, {name}! (served on the {era} era at {pv})"
|
||||
|
||||
return mcp
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_server_from_args(build_server)
|
||||
@@ -0,0 +1,50 @@
|
||||
"""One lowlevel Server factory that serves both the 2025 handshake era and the 2026 stateless era."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import mcp_types as types
|
||||
from mcp_types.version import MODERN_PROTOCOL_VERSIONS
|
||||
|
||||
from mcp.server.context import ServerRequestContext
|
||||
from mcp.server.lowlevel import Server
|
||||
from stories._hosting import run_server_from_args
|
||||
|
||||
GREET_INPUT_SCHEMA: dict[str, Any] = {
|
||||
"type": "object",
|
||||
"properties": {"name": {"type": "string"}},
|
||||
"required": ["name"],
|
||||
}
|
||||
|
||||
|
||||
def build_server() -> Server[Any]:
|
||||
async def list_tools(
|
||||
ctx: ServerRequestContext[Any], params: types.PaginatedRequestParams | None
|
||||
) -> types.ListToolsResult:
|
||||
return types.ListToolsResult(
|
||||
tools=[
|
||||
types.Tool(
|
||||
name="greet",
|
||||
description="Greet the caller and report which protocol era served the request.",
|
||||
input_schema=GREET_INPUT_SCHEMA,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
async def call_tool(ctx: ServerRequestContext[Any], params: types.CallToolRequestParams) -> types.CallToolResult:
|
||||
assert params.name == "greet" and params.arguments is not None
|
||||
era = "modern" if ctx.protocol_version in MODERN_PROTOCOL_VERSIONS else "legacy"
|
||||
text = f"Hello, {params.arguments['name']}! (served on the {era} era at {ctx.protocol_version})"
|
||||
return types.CallToolResult(content=[types.TextContent(text=text)])
|
||||
|
||||
# The same factory serves both eras with no configuration. Which era a request is
|
||||
# on is decided by the entry point / transport, never by the server.
|
||||
return Server(
|
||||
"dual-era-example",
|
||||
instructions="A small dual-era demo server.",
|
||||
on_list_tools=list_tools,
|
||||
on_call_tool=call_tool,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_server_from_args(build_server)
|
||||
Reference in New Issue
Block a user