This commit is contained in:
parent
3a6f2996cd
commit
837ef904f8
6 changed files with 1757 additions and 567 deletions
|
|
@ -102,6 +102,7 @@ _REQUEST_STATE_KEY = b"everything-server-fixture-request-state-key"
|
|||
|
||||
mcp = MCPServer(
|
||||
name="mcp-conformance-test-server",
|
||||
version="0.1.0",
|
||||
request_state_security=RequestStateSecurity(keys=[_REQUEST_STATE_KEY]),
|
||||
)
|
||||
|
||||
|
|
@ -357,13 +358,13 @@ async def test_missing_capability(ctx: Context) -> str:
|
|||
``CallToolResult.isError``) so the conformance harness observes a protocol-level
|
||||
error response with ``data.requiredCapabilities``.
|
||||
"""
|
||||
client_params = ctx.session.client_params
|
||||
sampling_declared = client_params is not None and client_params.capabilities.sampling is not None
|
||||
capabilities = ctx.session.client_capabilities
|
||||
sampling_declared = capabilities is not None and capabilities.sampling is not None
|
||||
if not sampling_declared:
|
||||
raise MCPError(
|
||||
code=MISSING_REQUIRED_CLIENT_CAPABILITY,
|
||||
message="This tool requires the client 'sampling' capability",
|
||||
data={"requiredCapabilities": ["sampling"]},
|
||||
data={"requiredCapabilities": {"sampling": {}}},
|
||||
)
|
||||
return "Client declared sampling capability; proceeding."
|
||||
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@ def run_client(main: Callable[..., Awaitable[None]]) -> None:
|
|||
if cfg["era"] == "dual-in-body":
|
||||
# The story pins its connection modes inside ``main`` itself, so hand it "auto"
|
||||
# (the ``Client`` default) and let those in-body pins decide. A hard version pin
|
||||
# here would skip the discover probe and leave ``server_info`` blank.
|
||||
# here would skip the discover probe and leave `server_info` None.
|
||||
era = "in-body"
|
||||
mode = {"modern": LATEST_MODERN_VERSION, "legacy": "legacy", "in-body": "auto"}[era]
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@ leg fails there today — run over `--http`.
|
|||
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/discover`, whichever ran. On the 2026 era `server_info` comes from
|
||||
the optional `serverInfo` `_meta` stamp (`None` for a server that does not
|
||||
identify itself); `initialize` always carries it.
|
||||
- `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.
|
||||
|
|
|
|||
|
|
@ -13,7 +13,11 @@ async def main(targets: TargetFactory, *, mode: str = "auto") -> None:
|
|||
# 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"
|
||||
# On the 2026 era, server identity is an optional serverInfo stamp in the
|
||||
# result _meta (None for an anonymous server); this server stamps it.
|
||||
info = modern.server_info
|
||||
assert info is not None, "the server stamps serverInfo into its results"
|
||||
assert info.name == "dual-era-example"
|
||||
assert modern.server_capabilities.tools is not None
|
||||
|
||||
listed = await modern.list_tools()
|
||||
|
|
@ -28,7 +32,9 @@ async def main(targets: TargetFactory, *, mode: str = "auto") -> None:
|
|||
# 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"
|
||||
info = legacy.server_info
|
||||
assert info is not None, "initialize always carries serverInfo"
|
||||
assert info.name == "dual-era-example"
|
||||
assert legacy.server_capabilities.tools is not None
|
||||
|
||||
result = await legacy.call_tool("greet", {"name": "2025 client"})
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ uv run python -m stories.reconnect.client --http --server server_lowlevel
|
|||
## Caveats
|
||||
|
||||
- `mode=<version-pin>` *without* `prior_discover=` synthesizes a placeholder
|
||||
whose `server_info` is `Implementation(name="", version="")`. Pass the cached
|
||||
with no `serverInfo` stamp, so `server_info` reads `None`. Pass the cached
|
||||
result to get real identity on reconnect. Whether `Client` should expose a
|
||||
public synthesizer (or refuse the bare pin) is open.
|
||||
- `client.session.discover_result` is a one-hop reach into the mechanics layer;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,11 @@ async def main(targets: TargetFactory, *, mode: str = "auto") -> None:
|
|||
discovered = client.session.discover_result
|
||||
assert discovered is not None, "mode='auto' against a modern server populates discover_result"
|
||||
assert client.protocol_version == LATEST_MODERN_VERSION
|
||||
assert client.server_info.name == "reconnect-example"
|
||||
# On the 2026 era, server identity is an optional serverInfo stamp in the
|
||||
# result _meta; an anonymous server reads as None. This one stamps it.
|
||||
info = client.server_info
|
||||
assert info is not None, "the server stamps serverInfo into its results"
|
||||
assert info.name == "reconnect-example"
|
||||
assert LATEST_MODERN_VERSION in discovered.supported_versions
|
||||
|
||||
result = await client.call_tool("add", {"a": 2, "b": 3})
|
||||
|
|
@ -28,11 +32,13 @@ async def main(targets: TargetFactory, *, mode: str = "auto") -> None:
|
|||
|
||||
# Reconnect: a version pin plus the cached DiscoverResult adopts the prior state with
|
||||
# zero round-trips on entry. A Client cannot be re-entered after exit, so targets()
|
||||
# yields a fresh one. Without prior_discover= a bare pin would synthesize a blank
|
||||
# server_info — the cache is what makes the era-neutral accessors useful here.
|
||||
# yields a fresh one. Without prior_discover= a bare pin would leave server_info
|
||||
# None — the cache is what carries the server's identity stamp across reconnects.
|
||||
async with Client(targets(), mode=LATEST_MODERN_VERSION, prior_discover=rehydrated) as second:
|
||||
assert second.protocol_version == LATEST_MODERN_VERSION
|
||||
assert second.server_info.name == "reconnect-example"
|
||||
info = second.server_info
|
||||
assert info is not None, "the cached DiscoverResult carries the serverInfo stamp"
|
||||
assert info.name == "reconnect-example"
|
||||
assert second.server_capabilities.tools is not None
|
||||
assert second.session.discover_result == rehydrated
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue