mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-23 14:04:18 +02:00
Add server protocol-version floor (provisional min_protocol_version)
Declare a minimum MCP protocol version; refuse handshake clients below it at connect time and warn on floor/feature incoherence at startup.
This commit is contained in:
parent
2676864163
commit
aca5dc99b9
7 changed files with 613 additions and 0 deletions
|
|
@ -380,6 +380,14 @@ A tool can gather client input across rounds on a `2026-07-28` call by returning
|
|||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/context.py` (`input_responses`/`request_state` properties), `fastmcp_slim/fastmcp/server/low_level.py` (`RequestStateBoundary` install), `fastmcp_slim/fastmcp/server/server.py` (`request_state_security` param), `fastmcp_slim/fastmcp/server/mixins/mcp_operations.py` (`_on_call_tool` input-required passthrough + era gate), `fastmcp_slim/fastmcp/tools/base.py` (`InputRequiredToolResult`), `tests/server/test_mrtr_guards.py`.
|
||||
|
||||
### Server protocol floor — New (opt-in feature, provisional API)
|
||||
|
||||
A server can declare the minimum MCP protocol version it requires, so a client that cannot meet it is refused at connection time instead of failing mid tool-call. The motivating case is modern-only guard tools (a tool returning `InputRequiredResult`, SEP-2322): under `FastMCP(min_protocol_version="2026-07-28")` a legacy client is refused during the initialize handshake with a clear `-32602` error naming the required version and pointing at the modern protocol, rather than discovering the incompatibility inside a call. Enforcement runs through FastMCP's entry in the SDK's middleware layer at the two negotiation points the framework owns: the initialize handshake (the negotiated handshake version is compared against the floor before the handshake commits) and `server/discover` (the modern era is the newest era, so it satisfies any currently declarable floor — discovery is never refused by a floor today). At startup a conservative, warning-only coherence check flags declared/registered conflicts: guard tools under a non-modern floor (handshake clients would fail mid-call) and a modern floor paired with a `"fallback"` sampling handler (the modern era forbids the back-channel, so the fallback is dead). Runtime-only back-channel usage (`ctx.elicit`/`ctx.sample`/`ctx.list_roots`) has no reliable static signal and is not inferred. The default is no floor — every era is served, fully backward compatible.
|
||||
|
||||
The public spelling (`min_protocol_version=`) is **provisional** and expected to change (the min-without-max shape is under review); the enforcement hook and inference rules are stable regardless of the eventual keyword. No `settings.py` entry yet.
|
||||
|
||||
*Verify:* `fastmcp_slim/fastmcp/server/protocol_floor.py` (validation, negotiation mirror, enforcement, coherence check), `fastmcp_slim/fastmcp/server/low_level.py` (`enforce_handshake_floor` in the initialize path), `fastmcp_slim/fastmcp/server/mixins/lifespan.py` (startup coherence call), `tests/server/test_protocol_floor.py`.
|
||||
|
||||
### The xfail register — Known gap
|
||||
|
||||
Roughly forty `xfail` markers across the test tree (concentrated in `tests/server/tasks/`, `tests/client/tasks/`, and `test_protocol_eras.py`) are the built-in beta tracker: each names the SDK gap it waits on. They are enumerated and mapped to sdk-feedback findings on the [Known Gaps](/development/v4-notes/known-gaps) page.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
title: 2026-07-28 Protocol Support
|
||||
---
|
||||
|
||||
import { VersionBadge } from "/snippets/version-badge.mdx"
|
||||
|
||||
FastMCP v4 serves the sessionless `2026-07-28` protocol era and the session-based handshake eras from a single server, with per-connection auto-detection. This page catalogs what FastMCP provides for the modern era — both the protocol machinery it inherits from the MCP Python SDK and the capabilities FastMCP implements itself on top of that layer. It is the reference for what a v4 deployment can actually do on the modern protocol today.
|
||||
|
||||
## Identity assertion (SEP-990): a complete server-side implementation
|
||||
|
|
@ -48,6 +50,72 @@ The complete picture of what a FastMCP v4 server and client provide on the `2026
|
|||
| **Pagination** | Declarative `FastMCP(list_page_size=...)` paginates all list operations in the high-level server; the client auto-paginates with cycle detection. |
|
||||
| **Telemetry** | OpenTelemetry spans on by default (no-op without an exporter), SDK-aligned attributes (`mcp.method.name`, `mcp.protocol.version`, `gen_ai.*`), plus auth and provider-delegation spans; `FASTMCP_ENABLE_TELEMETRY=false` disables cleanly. |
|
||||
|
||||
## Server protocol floor (DRAFT — provisional API)
|
||||
|
||||
<Warning>
|
||||
This section is a **draft** for an in-progress feature. The public spelling shown
|
||||
here (`min_protocol_version=`) is **provisional** and expected to change before
|
||||
release — the min-without-max shape is under review. The underlying mechanics
|
||||
(connect-time enforcement and startup coherence checks) are stable regardless of
|
||||
the final spelling.
|
||||
</Warning>
|
||||
|
||||
<VersionBadge version="4.0.0" />
|
||||
|
||||
A server can depend on features that exist on only one protocol era. A tool that
|
||||
returns an `InputRequiredResult` (the modern [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol))
|
||||
works only on a `2026-07-28` connection. Today a legacy client that reaches such
|
||||
a tool over the initialize handshake gets a confusing era error *mid tool-call*,
|
||||
long after connecting — the failure surfaces far from its cause.
|
||||
|
||||
The protocol floor moves that failure to connect time. A server declares the
|
||||
minimum protocol version it requires, and FastMCP refuses any handshake below it
|
||||
with a clear error naming the required version:
|
||||
|
||||
```python
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.types import InputRequiredResult
|
||||
|
||||
mcp = FastMCP("guarded", min_protocol_version="2026-07-28")
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def confirm(action: str) -> str | InputRequiredResult:
|
||||
... # a modern-only guard tool
|
||||
```
|
||||
|
||||
A legacy client connecting to this server is refused during `initialize` with a
|
||||
message pointing it at the modern protocol, rather than discovering the
|
||||
incompatibility inside a tool call. Modern (`server/discover`) clients connect
|
||||
normally — the modern era satisfies any currently declarable floor, because it is
|
||||
the newest era.
|
||||
|
||||
**Enforcement points.** The floor is applied at the two negotiation points
|
||||
FastMCP owns, both through the framework's entry in the SDK's middleware layer:
|
||||
|
||||
- **Initialize handshake** — the connection's negotiated handshake version is
|
||||
compared against the floor before the handshake commits; a version below the
|
||||
floor is refused with `-32602` (invalid params).
|
||||
- **`server/discover` (modern)** — the modern era is the newest era, so every
|
||||
modern connection satisfies any floor a server can declare today; discovery is
|
||||
therefore never refused by a floor in the current version set.
|
||||
|
||||
**Startup coherence check.** FastMCP knows what is registered, so at startup it
|
||||
warns (never fails) when the declared floor and the registered features conflict:
|
||||
|
||||
- A guard tool (returns `InputRequiredResult`) under a non-modern floor will fail
|
||||
for handshake-era clients mid-call — the warning names the tool and recommends
|
||||
a modern floor.
|
||||
- A modern floor combined with a `sampling_handler` set to `"fallback"` is a dead
|
||||
preference: the modern era forbids the server-initiated back-channel, so the
|
||||
local handler always runs. The warning suggests `"always"` or a lower floor.
|
||||
|
||||
Runtime-only back-channel usage (`ctx.elicit`, `ctx.sample`, `ctx.list_roots`)
|
||||
has no reliable static signal and is deliberately not inferred.
|
||||
|
||||
The default is no floor: a server without `min_protocol_version` serves every
|
||||
protocol era, fully backward compatible.
|
||||
|
||||
## Still in the program
|
||||
|
||||
Elicitation on the modern protocol is now shipped in its **guard form** — a tool returns an `InputRequiredResult` and re-runs per round to gather user input via multi-round trips (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)). The declarative `Resolve(...)` layer over that primitive remains staged, tracked in the [Feature Program](/development/v4-notes/feature-program), along with the unified `subscriptions/listen` stream. The [Known Gaps](/development/v4-notes/known-gaps) page tracks the upstream dependencies that gate them.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue