Align with spec #3002: optional clientInfo, serverInfo in result _meta (#3143)

This commit is contained in:
Max 2026-07-23 12:00:36 +01:00 • committed by GitHub
commit 837ef904f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
99 changed files with 1757 additions and 567 deletions

View file

@ -10,7 +10,24 @@
# scenarios start passing and MUST be removed from this list (the runner fails
# on stale entries), so the baseline burns down per milestone.
client: []
client:
# SEP-1932 (DPoP): the SDK's OAuth client does not implement DPoP proofs.
# These scenarios are new in the da56f663 referee pin. The entries are
# per-check (conformance #406) because both scenarios pass their
# non-DPoP checks (discovery, token acquisition, request flow) live.
- auth/dpop:sep-1932-client-token-request-proof
- auth/dpop:sep-1932-client-dpop-auth-scheme
- auth/dpop:sep-1932-client-fresh-proof
- auth/dpop-nonce:sep-1932-client-token-request-proof
- auth/dpop-nonce:sep-1932-client-dpop-auth-scheme
- auth/dpop-nonce:sep-1932-client-fresh-proof
- auth/dpop-nonce:sep-1932-client-as-nonce
- auth/dpop-nonce:sep-1932-client-rs-nonce
# Workload identity federation: the OAuth client does not implement the
# urn:ietf:params:oauth:grant-type:jwt-bearer grant (it answers with
# authorization_code). Also new in the da56f663 referee pin; per-check for
# the same reason.
- auth/wif-jwt-bearer:wif-grant-type
server:
# SEP-2663 (io.modelcontextprotocol/tasks): the SDK does not implement the

View file

@ -19,17 +19,19 @@ env:
# Bump deliberately and reconcile both
# .github/actions/conformance/expected-failures*.yml files in the same change.
#
# Temporarily pinned to the pkg.pr.new build of conformance main@4944b268
# (0.2.0-alpha.8, which includes #372: fail checks whose prerequisite is
# missing instead of skipping them) — alpha.8 is not published to npm yet.
# Pinned by commit SHA so the tarball cannot move under us;
# Temporarily pinned to the pkg.pr.new build of conformance main@da56f663,
# which includes #403 (sep-2575 checks flipped to the post-spec-#3002 shape:
# clientInfo optional on requests, serverInfo in result _meta instead of the
# discover body) and #406 (per-check expected-failures granularity) — the
# last published npm release (0.2.0-alpha.9) still enforces the pre-#3002
# shape. Pinned by commit SHA so the tarball cannot move under us;
# CONFORMANCE_PKG_SHA256 pins the bytes and the fetch-and-verify step below
# downloads, checks the digest, and repoints CONFORMANCE_PKG at the
# verified local copy. Repin to the next published @modelcontextprotocol/
# conformance release (>=0.2.0-alpha.8) once it ships, then drop
# conformance release (>=0.2.0-alpha.10) once it ships, then drop
# CONFORMANCE_PKG_SHA256 and the fetch-and-verify steps.
CONFORMANCE_PKG: "https://pkg.pr.new/@modelcontextprotocol/conformance@4944b268"
CONFORMANCE_PKG_SHA256: "0f70c035782d319d72ab427653c5275db5c50429d59fae0241a645b33aeda1a7"
CONFORMANCE_PKG: "https://pkg.pr.new/@modelcontextprotocol/conformance@da56f663"
CONFORMANCE_PKG_SHA256: "bbc94678033071df4bc9851ce2054f9dff918f4501661d1d60e2d09d8c515e6d"
jobs:
server-conformance:

View file

@ -133,14 +133,21 @@ or veto a tool call:
```
* `params` is the validated `CallToolRequestParams`: you get `params.name` and
`params.arguments` without touching raw JSON.
* `call_next(ctx)` runs the rest of the chain. Return its result unchanged (observe),
return something else (replace), or raise an `MCPError` (refuse).
`params.arguments` without touching raw JSON. It is also what decides which
tool call runs: passing a rewritten context through `call_next` changes what
the handler observes on `ctx`, not the tool invocation. Wire-level request
rewriting belongs to [Middleware](middleware.md).
* `call_next(ctx)` runs the rest of the chain and returns the handler's result.
Return it unchanged (observe), return something else (replace), or raise an
`MCPError` (refuse). Whatever you return is serialized like any handler
result, including the 2026-era `serverInfo` identity stamp, so a
short-circuiting interceptor never produces an anonymous or off-schema
response.
* With several extensions, interceptors nest in registration order: the first
extension in `extensions=[...]` is outermost.
* The default implementation is a pass-through, and a server whose extensions never
override this hook installs **no** middleware at all. You don't pay for what
you don't use.
override this hook keeps the bare `tools/call` handler untouched. You don't
pay for what you don't use.
The hook wraps `tools/call` and nothing else. For every-message concerns, use
[Middleware](middleware.md). That is what it is for.

View file

@ -102,10 +102,13 @@ Call it and the result carries both representations:
"content": [{"type": "text", "text": "Found 3 books matching 'dune'."}],
"structuredContent": {"matches": 3, "query": "dune"},
"isError": false,
"resultType": "complete"
"resultType": "complete",
"_meta": {"io.modelcontextprotocol/serverInfo": {"name": "Bookshop", "version": "2.0.0"}}
}
```
The `_meta` block is the server's identity stamp: the SDK adds it to every 2026-era result, with the `version` from the constructor (a server that sets none reports an empty string). A server that must not identify itself can strip the key with a middleware, which owns the results it returns.
The server never compares the two fields. This SDK's `Client` does: return `structured_content` that doesn't satisfy the `output_schema` you declared and `call_tool` raises a `RuntimeError` that starts with `Invalid structured content returned by tool search_books` and goes on to quote the `jsonschema` failure. Promising a schema is cheap; keeping it is on you. The whole ladder of return types and schemas is in **[Structured Output](../servers/structured-output.md)**.
## `_meta`: for the application, not the model

View file

@ -63,6 +63,10 @@ In increasing order of how much you should hesitate:
`initialize`: the result the client gets back is built from your rewritten params, but the
server commits its connection state from the original wire params. The two sides can finish
the handshake disagreeing about what they negotiated.
* **Answer.** Return a result without calling `call_next(ctx)` and it goes to the client as
your response. `call_next` hands you the finished wire form, and the pipeline never patches
what you return, so the whole envelope is yours: on a 2026-era connection that includes the
`serverInfo` `_meta` stamp, which the SDK adds to handler results but not to yours.
!!! check
`initialize` is one of the things middleware wraps, and it is the *only* hook you get

View file

@ -30,7 +30,7 @@ Everything else on this page is identical across all three. Headers, subprocesse
Four read-only properties, populated the moment you enter the block:
* `client.server_info`: the server's identity. `server_info.name` here is `"Bookshop"`, `server_info.version` is whatever the server reports.
* `client.server_info`: the server's identity, or `None` for a 2026-era server that does not report one (python-sdk servers do by default). `server_info.name` here is `"Bookshop"`, `server_info.version` is whatever the server reports.
* `client.server_capabilities`: what the server can do (`tools`, `resources`, `prompts`, `completions`, ...). A capability the server doesn't have is `None`.
* `client.protocol_version`: the protocol version the two sides agreed on. Here it is `"2026-07-28"`.
* `client.instructions`: the server's `instructions=` string, or `None` if it didn't set one.
@ -202,7 +202,7 @@ There is one constructor flag built for that: `Client(mcp, raise_exceptions=True
## Recap
* `Client(x)` connects in-memory to a server object, over Streamable HTTP to a URL string, and over anything else via a transport.
* `async with` is the whole lifecycle. Inside it, `server_info`, `server_capabilities`, `protocol_version` and `instructions` are already populated.
* `async with` is the whole lifecycle. Inside it, `server_capabilities` and `protocol_version` are already populated; `server_info` and `instructions` are too when the server provides them.
* `list_tools()` gives you each tool's `name`, `title`, `description` and `input_schema`.
* `call_tool()` returns `content` for the model, `structured_content` for your code, and `is_error`. A raising tool is a result, not an exception.
* `content` is a union of block types; narrow with `isinstance` before reading.

View file

@ -64,7 +64,7 @@ Run it again. `print(sorted(group.tools))` now shows both:
`connect_to_server` returns the `ClientSession` it opened. Keep it if you ever want that server gone: `await group.disconnect_from_server(session)` removes its tools, resources, and prompts from the group.
If you already hold a connected `ClientSession` (`Client.session` is one), hand it to `await group.connect_with_session(server_info, session)` instead of opening a new transport. It aggregates the same way. The group never closes a session it didn't open.
If you already hold a connected `ClientSession` (`Client.session` is one), hand it to `await group.connect_with_session(server_info, session)` instead of opening a new transport. It aggregates the same way. The group never closes a session it didn't open. `server_info` names the server for component prefixes; on a 2026-era connection `client.server_info` can be `None` (identity is optional), so pass your own `Implementation(name=..., version=...)` in that case.
## The classic handshake

View file

@ -59,6 +59,8 @@ async def client(): # (2)!
@pytest.mark.anyio
async def test_call_add_tool(client: Client):
result = await client.call_tool("add", {"a": 1, "b": 2})
# Drop the server identity stamp in `_meta`; it is not what this test is about.
result.meta = None
assert result == snapshot(
CallToolResult(
content=[TextContent(type="text", text="3")],

View file

@ -606,6 +606,14 @@ mcp = MCPServer("Demo", instructions="You answer questions about the weather.")
Keep `name` positional and pass everything else by keyword.
### Unversioned servers report an empty version
In v1, a server constructed without a `version` reported the installed `mcp`
package's version as its own in the `initialize` result's `serverInfo`. In v2
it reports an empty string instead: the SDK's version is not your server's
version. Pass `version="..."` to `Server(...)` or `MCPServer(...)` to identify
your server properly. The field is display-only; nothing breaks either way.
### `mount_path` parameter removed from MCPServer
The `mount_path` parameter has been removed from `MCPServer.__init__()`, `MCPServer.run()`, `MCPServer.run_sse_async()`, and `MCPServer.sse_app()`. It was also removed from the `Settings` class.
@ -1498,7 +1506,7 @@ version = session.protocol_version
The raw handshake result is also retained: `session.initialize_result` is set after `initialize()` (≤2025-11-25 servers — including `stateless_http=True` servers, which still answer `initialize`); `session.discover_result` is set after `discover()` (2026-07-28+ servers). At most one is non-`None`.
On the high-level `Client`, `client.server_capabilities`, `client.server_info`, and `client.protocol_version` are non-nullable inside the context manager. `client.instructions` remains `str | None` since the server may omit it. (The lowlevel `ClientSession` still lets you call methods before any handshake, as in v1; `Client` always connects on enter — by default it probes `server/discover` and falls back to the initialize handshake.)
On the high-level `Client`, `client.server_capabilities` and `client.protocol_version` are non-nullable inside the context manager. `client.instructions` remains `str | None` since the server may omit it, and `client.server_info` is `Implementation | None`: on 2026-era connections identity is optional wire metadata, so a server that does not report it reads as `None`. (The lowlevel `ClientSession` still lets you call methods before any handshake, as in v1; `Client` always connects on enter — by default it probes `server/discover` and falls back to the initialize handshake.)
### `cursor` parameter removed from `ClientSession` list methods

View file

@ -68,10 +68,10 @@ A pin is a promise *you* make: you already know the server speaks that version.
A pin is not a discovery. Print `client.server_info` and the price is right there:
```text
name='' title=None version='' description=None website_url=None icons=None
None
```
The client never asked the server who it is, so `server_info` is a blank. `client.server_capabilities`
The client never asked the server who it is, so `server_info` is `None`. `client.server_capabilities`
is the same story: every capability is `None`. Tool calls still work (the protocol needs none of it);
code that reads `server_capabilities` to decide what to offer does not.
@ -87,7 +87,7 @@ ValueError: mode must be 'legacy', 'auto', or one of ['2026-07-28']; got '2025-0
The probe is cheap, but it is still a round trip you pay on every reconnect, and the answer almost never changes.
So keep it. After an `auto` connection, `client.session.discover_result` holds the exact `DiscoverResult` the server sent: its `supported_versions`, its `capabilities`, its `server_info`, its `instructions`. Hand it back as `prior_discover=` the next time:
So keep it. After an `auto` connection, `client.session.discover_result` holds the exact `DiscoverResult` the server sent: its `supported_versions`, its `capabilities`, its `instructions`, and the identity the server stamped into the result's `_meta`. Hand it back as `prior_discover=` the next time:
```python title="client.py" hl_lines="15 17"
--8<-- "docs_src/protocol_versions/tutorial004.py"
@ -112,7 +112,7 @@ The second connection made **zero** negotiation round trips and still knows exac
| --- | --- | --- |
| `Client(target)` | one `server/discover` probe; the `initialize` handshake if it fails | the newest version both sides speak, whichever era |
| `Client(target, mode="legacy")` | the `initialize` handshake | a handshake-era version; server-initiated requests work |
| `Client(target, mode="2026-07-28")` | none | that version, pinned, with a blank `server_info` |
| `Client(target, mode="2026-07-28")` | none | that version, pinned, with `server_info` as `None` |
| `Client(target, mode="2026-07-28", prior_discover=saved)` | none | that version, pinned, *and* the identity you saved last time |
## Recap
@ -121,7 +121,7 @@ The second connection made **zero** negotiation round trips and still knows exac
* `mode="auto"` is the default: probe, fall back. Leave it alone unless one of the other three rows describes you.
* `client.protocol_version` is always the answer to "what did I get?".
* `mode="legacy"` forces the handshake. It is what you need for server-initiated requests: sampling, push elicitation, `message_handler`.
* A version pin (`mode="2026-07-28"`) sends no negotiation traffic at all, at the cost of a blank `server_info`.
* A version pin (`mode="2026-07-28"`) sends no negotiation traffic at all, at the cost of `client.server_info` being `None`.
* `prior_discover=` pays that cost back: save `client.session.discover_result`, reconnect with it, get both.
A modern connection has no push channel, so how does a 2026 server ask you a question mid-call? It returns it: **[Multi-round-trip requests](handlers/multi-round-trip.md)**.

View file

@ -97,9 +97,10 @@ The same `icons=[...]` keyword is accepted by `MCPServer(...)`, `@mcp.tool()`, `
### Where a client sees them
Icons travel with whatever they decorate. The server's arrive when the client connects, on `client.server_info`:
Icons travel with whatever they decorate. The server's arrive when the client connects, on `client.server_info` (optional on 2026-era connections, so narrow it first):
```python
assert client.server_info is not None # python-sdk servers identify themselves by default
client.server_info.icons # [Icon(src="https://example.com/brand-kit.png", mime_type="image/png", sizes=["48x48"])]
```

View file

@ -44,7 +44,7 @@ v1 handed you three nested layers: a transport context manager yielding raw stre
--8<-- "docs_src/client/tutorial001.py"
```
`Client` takes a server object (in memory, no transport: the testing story), a URL (Streamable HTTP), or any transport context manager such as `stdio_client(...)`. Entering `async with` connects and negotiates the protocol version, whichever era the server speaks; `client.server_info`, `client.server_capabilities`, and `client.protocol_version` are simply there afterwards. The sampling and elicitation callbacks you registered in v1 still work (their bodies see the same snake_case attribute rename as everything else on this page), they now also answer the 2026-style requests-inside-results (below), and they run concurrently instead of one at a time. `ClientSession` is still underneath for anyone who wants the low-level surface, and `client.session` hands it to you; it moved too (it runs on the new dispatcher engine, and some of its own signatures changed), so read the **[Migration Guide](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** before you drop down.
`Client` takes a server object (in memory, no transport: the testing story), a URL (Streamable HTTP), or any transport context manager such as `stdio_client(...)`. Entering `async with` connects and negotiates the protocol version, whichever era the server speaks; `client.server_capabilities` and `client.protocol_version` are simply there afterwards, and `client.server_info` is too when the server identifies itself (it is `Implementation | None` now, since 2026-era identity is optional). The sampling and elicitation callbacks you registered in v1 still work (their bodies see the same snake_case attribute rename as everything else on this page), they now also answer the 2026-style requests-inside-results (below), and they run concurrently instead of one at a time. `ClientSession` is still underneath for anyone who wants the low-level surface, and `client.session` hands it to you; it moved too (it runs on the new dispatcher engine, and some of its own signatures changed), so read the **[Migration Guide](migration.md#clientsession-now-runs-on-jsonrpcdispatcher-basesession-removed)** before you drop down.
**[The Client](client/index.md)** introduces it, **[Client transports](client/transports.md)** covers the three connection forms, **[Client callbacks](client/callbacks.md)** covers the callbacks themselves, and **[Testing](get-started/testing.md)** shows the in-memory pattern that replaces v1's `create_connected_server_and_client_session()` helper.
@ -197,6 +197,7 @@ At 2026-07-28 the standalone HTTP GET stream and `resources/subscribe` are repla
### The rest, quickly
* **Identity is optional, per-message metadata.** The request-side `clientInfo` `_meta` key is optional (the required pair is `protocolVersion` + `clientCapabilities`), and `serverInfo` moved out of the `server/discover` result body: servers stamp it into every 2026-era result's `_meta` instead (since `2.0.0b3`; [spec #3002](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/3002)). The SDK always stamps; `client.server_info` is `None` when a server does not identify itself (for example, a middleware stripped the key). **[The low-level Server](advanced/low-level-server.md)** shows the stamp on the wire.
* **Requests are routable without parsing bodies.** Modern HTTP requests carry `Mcp-Method` (and, for the three tool-ish calls, `Mcp-Name`); a tool input-schema property annotated with `x-mcp-header` is mirrored into an `Mcp-Param-*` header and cross-checked by the server ([SEP-2243](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2243)). Gateways and rate limiters can route on headers alone; the **[Migration Guide](migration.md#servers-validate-mcp-param-headers-against-the-request-body-sep-2243)** has the rules.
* **Results carry cache hints.** List and read results declare `ttlMs` and `cacheScope` ([SEP-2549](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2549)); you set them per method with `cache_hints=`, and `Client` honors them with a built-in response cache. A server that sends no hints (every pre-2026 server) sees identical, uncached traffic. **[Caching hints](client/caching.md)**.
* **Extensions are first class.** Servers and clients declare optional capability bundles under reverse-DNS identifiers ([SEP-2133](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2133)); the built-in `Apps` extension (MCP Apps) is the reference. **[Extensions](advanced/extensions.md)** and **[MCP Apps](advanced/apps.md)**.

View file

@ -38,4 +38,4 @@ async def call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) ->
)
server = Server("Bookshop", on_list_tools=list_tools, on_call_tool=call_tool)
server = Server("Bookshop", version="2.0.0", on_list_tools=list_tools, on_call_tool=call_tool)

View file

@ -16,4 +16,5 @@ async def main() -> None:
async with Client(mcp, mode="2026-07-28", prior_discover=saved) as client:
print(client.protocol_version)
print(client.server_info.name)
if client.server_info is not None:
print(client.server_info.name)

View file

@ -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."

View file

@ -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]

View file

@ -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.

View file

@ -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"})

View file

@ -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;

View file

@ -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

Some files were not shown because too many files have changed in this diff Show more