mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 07:09:11 +02:00
Expose era-neutral client server metadata (#4599)
* Expose era-neutral client metadata
🤖 Generated with Codex
* Clarify pinned modern client metadata
This commit is contained in:
parent
edb54bddf3
commit
06aa84943c
3 changed files with 57 additions and 12 deletions
|
|
@ -123,7 +123,7 @@ async with client:
|
|||
|
||||
## Connection Lifecycle
|
||||
|
||||
The client uses context managers for connection management. When you enter the context, the client establishes a connection and performs an MCP initialization handshake with the server. This handshake exchanges capabilities, server metadata, and instructions.
|
||||
The client uses context managers for connection management. When you enter the context, the client establishes a connection and negotiates the protocol era with the server. Metadata returned by either legacy initialization or modern discovery is exposed through the same client properties.
|
||||
|
||||
```python
|
||||
from fastmcp import Client, FastMCP
|
||||
|
|
@ -136,10 +136,12 @@ def greet(name: str) -> str:
|
|||
return f"Hello, {name}!"
|
||||
|
||||
async with Client(mcp) as client:
|
||||
# Initialization already happened automatically
|
||||
print(f"Server: {client.initialize_result.server_info.name}")
|
||||
print(f"Instructions: {client.initialize_result.instructions}")
|
||||
print(f"Capabilities: {client.initialize_result.capabilities.tools}")
|
||||
# Protocol negotiation already happened automatically
|
||||
assert client.server_info is not None
|
||||
assert client.server_capabilities is not None
|
||||
print(f"Server: {client.server_info.name}")
|
||||
print(f"Instructions: {client.instructions}")
|
||||
print(f"Capabilities: {client.server_capabilities.tools}")
|
||||
```
|
||||
|
||||
For advanced scenarios where you need precise control over when initialization happens, disable automatic initialization and call `initialize()` manually:
|
||||
|
|
@ -199,12 +201,16 @@ You can also pin a specific modern protocol version to adopt it directly, withou
|
|||
client = Client("https://example.com/mcp", mode="2026-07-28")
|
||||
```
|
||||
|
||||
Once connected, the negotiated version and the server's advertised capabilities are available as properties. Both are populated regardless of which era was negotiated, and both are `None` while the client is disconnected.
|
||||
Once connected, the negotiated version, server identity, capabilities, and instructions are available as properties. They are populated from either the legacy `InitializeResult` or modern `DiscoverResult`, and reset to `None` when the client disconnects. `instructions` is also `None` when the server does not provide any.
|
||||
|
||||
When you pin a modern version directly, the client skips discovery and adopts that version with minimal synthesized metadata. In that mode, `server_info` has an empty name and `instructions` is `None`.
|
||||
|
||||
```python
|
||||
async with Client("https://example.com/mcp", mode="auto") as client:
|
||||
print(client.protocol_version) # e.g. "2026-07-28"
|
||||
print(client.server_info) # Implementation | None
|
||||
print(client.server_capabilities) # ServerCapabilities | None
|
||||
print(client.instructions) # str | None
|
||||
```
|
||||
|
||||
<Note>
|
||||
|
|
|
|||
|
|
@ -643,8 +643,9 @@ class Client(
|
|||
"""Get the result of the initialization request.
|
||||
|
||||
`None` on a modern (`server/discover`) connection, which negotiates via a
|
||||
`DiscoverResult` rather than an `InitializeResult`. Use `protocol_version` /
|
||||
`server_capabilities` for era-neutral access to the negotiated identity.
|
||||
`DiscoverResult` rather than an `InitializeResult`. Use `protocol_version`,
|
||||
`server_info`, `server_capabilities`, and `instructions` for era-neutral
|
||||
access to the negotiated server metadata.
|
||||
"""
|
||||
return self._session_state.initialize_result
|
||||
|
||||
|
|
@ -668,6 +669,27 @@ class Client(
|
|||
session = self._session_state.session
|
||||
return session.server_capabilities if session is not None else None
|
||||
|
||||
@property
|
||||
def server_info(self) -> mcp_types.Implementation | None:
|
||||
"""The session's server identity, or `None` when disconnected.
|
||||
|
||||
Populated from whichever negotiation result the era produced (the
|
||||
`InitializeResult` on legacy, the `DiscoverResult` on modern). A directly
|
||||
pinned modern version uses a synthesized identity with an empty name.
|
||||
"""
|
||||
session = self._session_state.session
|
||||
return session.server_info if session is not None else None
|
||||
|
||||
@property
|
||||
def instructions(self) -> str | None:
|
||||
"""The server's instructions, or `None` when absent or disconnected.
|
||||
|
||||
Populated from whichever negotiation result the era produced (the
|
||||
`InitializeResult` on legacy, the `DiscoverResult` on modern).
|
||||
"""
|
||||
session = self._session_state.session
|
||||
return session.instructions if session is not None else None
|
||||
|
||||
def set_roots(self, roots: RootsList | RootsHandler) -> None:
|
||||
"""Set the roots for the client. This does not automatically call `send_roots_list_changed`."""
|
||||
self._session_kwargs["list_roots_callback"] = create_roots_callback(roots)
|
||||
|
|
@ -853,8 +875,9 @@ class Client(
|
|||
|
||||
With `mode="auto"` or a pinned modern version, connect-time negotiation may adopt
|
||||
the modern `server/discover` era, which has no `InitializeResult`; in that case
|
||||
this method raises. Read `protocol_version` / `server_capabilities` instead, or use
|
||||
`mode="legacy"` when you need the handshake result.
|
||||
this method raises. Read `protocol_version`, `server_info`,
|
||||
`server_capabilities`, and `instructions` instead, or use `mode="legacy"`
|
||||
when you need the handshake result.
|
||||
|
||||
Args:
|
||||
timeout: Optional timeout for the initialization request (seconds or timedelta).
|
||||
|
|
@ -887,8 +910,9 @@ class Client(
|
|||
if self.initialize_result is None:
|
||||
raise RuntimeError(
|
||||
"The client negotiated a modern protocol era (server/discover), which has "
|
||||
"no InitializeResult. Read client.protocol_version / client.server_capabilities "
|
||||
"instead, or construct the client with mode='legacy'."
|
||||
"no InitializeResult. Inspect client.protocol_version, client.server_info, "
|
||||
"client.server_capabilities, and client.instructions for the metadata "
|
||||
"available in this mode, or construct the client with mode='legacy'."
|
||||
)
|
||||
return self.initialize_result
|
||||
|
||||
|
|
|
|||
|
|
@ -248,6 +248,9 @@ class TestPinnedMode:
|
|||
async with Client(fastmcp_server, mode=LATEST_MODERN_VERSION) as client:
|
||||
assert client.protocol_version == LATEST_MODERN_VERSION
|
||||
assert client.initialize_result is None
|
||||
assert client.server_info is not None
|
||||
assert client.server_info.name == ""
|
||||
assert client.instructions is None
|
||||
|
||||
async def test_pinned_modern_call_tool(self, fastmcp_server):
|
||||
async with Client(fastmcp_server, mode=LATEST_MODERN_VERSION) as client:
|
||||
|
|
@ -256,10 +259,20 @@ class TestPinnedMode:
|
|||
|
||||
|
||||
class TestConnectionProperties:
|
||||
@pytest.mark.parametrize("mode", ["legacy", "auto"])
|
||||
async def test_server_metadata_available_across_eras(self, mode):
|
||||
server = FastMCP("MetadataServer", instructions="Use the metadata tools.")
|
||||
async with Client(server, mode=mode) as client:
|
||||
assert client.server_info is not None
|
||||
assert client.server_info.name == "MetadataServer"
|
||||
assert client.instructions == "Use the metadata tools."
|
||||
|
||||
async def test_properties_none_before_connect(self, fastmcp_server):
|
||||
client = Client(fastmcp_server, mode="auto")
|
||||
assert client.protocol_version is None
|
||||
assert client.server_capabilities is None
|
||||
assert client.server_info is None
|
||||
assert client.instructions is None
|
||||
|
||||
async def test_properties_none_after_disconnect(self, fastmcp_server):
|
||||
client = Client(fastmcp_server, mode="auto")
|
||||
|
|
@ -267,6 +280,8 @@ class TestConnectionProperties:
|
|||
assert client.protocol_version is not None
|
||||
assert client.protocol_version is None
|
||||
assert client.server_capabilities is None
|
||||
assert client.server_info is None
|
||||
assert client.instructions is None
|
||||
|
||||
|
||||
class TestManualNegotiation:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue