From 2bee9aeb58c83f1ce7dc2f66240d1ef60b1b2c48 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Thu, 6 Aug 2026 09:34:11 -0400 Subject: [PATCH 1/3] Clarify review of closed contributor PRs (#4780) --- CLAUDE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 1a2cbddb0..79b040531 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,6 +56,8 @@ When modifying MCP functionality, changes typically need to be applied across al **Read `CONTRIBUTING.md` before opening issues or PRs.** It describes when PRs are appropriate, what we expect from enhancement proposals, and what we'll close without review. +**Review closed contributor PRs.** When reviewing an issue, inspect every associated non-maintainer PR, including closed PRs. External PRs may be closed as part of the issue-link and assignment workflow, so closure alone is not a negative signal. Read `CONTRIBUTING.md` and the PR timeline and comments to understand its status before evaluating it. + ### Git & CI - Prek hooks are required (run automatically on commits) From c8b88b3a3763c3ce76868b1e92fa80f417084f46 Mon Sep 17 00:00:00 2001 From: Yonatan Date: Thu, 6 Aug 2026 16:35:49 +0300 Subject: [PATCH 2/3] fix(context): move elicit overload docs inside the stubs so mypy sees the chain (#4774) --- fastmcp_slim/fastmcp/server/context.py | 39 ++++++++++---------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/fastmcp_slim/fastmcp/server/context.py b/fastmcp_slim/fastmcp/server/context.py index 3373e09da..0e3ccf9d5 100644 --- a/fastmcp_slim/fastmcp/server/context.py +++ b/fastmcp_slim/fastmcp/server/context.py @@ -962,9 +962,8 @@ class Context: *, response_title: str | None = None, response_description: str | None = None, - ) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation: ... - - """The accepted elicitation will contain the response data""" + ) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation: + """The accepted elicitation will contain the response data""" @overload async def elicit( @@ -974,10 +973,9 @@ class Context: *, response_title: str | None = None, response_description: str | None = None, - ) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: ... - - """When response_type is a list of strings, the accepted elicitation will - contain the selected string response""" + ) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: + """When response_type is a list of strings, the accepted elicitation will + contain the selected string response""" @overload async def elicit( @@ -987,10 +985,9 @@ class Context: *, response_title: str | None = None, response_description: str | None = None, - ) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: ... - - """When response_type is a dict mapping keys to title dicts, the accepted - elicitation will contain the selected key""" + ) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation: + """When response_type is a dict mapping keys to title dicts, the accepted + elicitation will contain the selected key""" @overload async def elicit( @@ -1000,12 +997,9 @@ class Context: *, response_title: str | None = None, response_description: str | None = None, - ) -> ( - AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation - ): ... - - """When response_type is a list containing a list of strings (multi-select), - the accepted elicitation will contain a list of selected strings""" + ) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation: + """When response_type is a list containing a list of strings (multi-select), + the accepted elicitation will contain a list of selected strings""" @overload async def elicit( @@ -1015,13 +1009,10 @@ class Context: *, response_title: str | None = None, response_description: str | None = None, - ) -> ( - AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation - ): ... - - """When response_type is a list containing a dict mapping keys to title dicts - (multi-select with titles), the accepted elicitation will contain a list of - selected keys""" + ) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation: + """When response_type is a list containing a dict mapping keys to title dicts + (multi-select with titles), the accepted elicitation will contain a list of + selected keys""" async def elicit( self, From 6fb34e9383cd6b44bef606c06b493503c8354b61 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Thu, 6 Aug 2026 11:15:38 -0400 Subject: [PATCH 3/3] Document MCP protocol support and conformance (#4781) --- docs/more/faq.mdx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/more/faq.mdx b/docs/more/faq.mdx index 1ac3c2c7d..5566e908f 100644 --- a/docs/more/faq.mdx +++ b/docs/more/faq.mdx @@ -22,16 +22,24 @@ The client probes `server/discover` and adopts the modern protocol when the serv ## What are the two protocol eras, and which one does my server speak? -Both. A FastMCP server serves every era from one deployment and one URL, and the SDK negotiates per connection — the client picks, not the server. +Both. A FastMCP 4 server supports the handshake revisions `2024-11-05`, `2025-03-26`, `2025-06-18`, and `2025-11-25`, plus the modern `2026-07-28` protocol. It serves all of them from one deployment and one URL, and the SDK negotiates per connection — the client picks, not the server. The *handshake* era (`2025-11-25` and earlier) opens each connection with `initialize` and holds a session, which gives the server a back-channel it can push requests down. The *modern* era (`2026-07-28`) is sessionless: the client learns what the server offers through `server/discover`, every request stands alone, and there is no back-channel. Inside a tool, `ctx.request_context.protocol_version` tells you which era the current call arrived on; on the client, `client.protocol_version` reports it after connecting. +A protocol version establishes the wire format, while capabilities describe which optional operations a particular server provides. The capabilities returned by `server/discover` or `initialize` are therefore the authoritative way for a client to determine what is available. + ## Can FastMCP 4 talk to older clients and servers? Yes, in both directions, with no configuration. A FastMCP 4 server answers a handshake-era client and a modern one from the same process: the old client sends `initialize` and gets a session id, the modern client discovers and stays stateless. A FastMCP 4 client is equally happy against an old server, because `mode="auto"` falls back to the handshake when discovery finds no modern peer. The client-side handlers for server-initiated capabilities are all still there too — passing `sampling_handler=` or `roots=` answers a legacy server's requests exactly as before, which is what a modern client needs in order to interoperate. See [client sampling](/clients/sampling) and [client roots](/clients/roots). +## How does FastMCP verify protocol conformance? + +FastMCP runs the [official MCP conformance suite](https://github.com/modelcontextprotocol/conformance) in CI against a pinned suite release. A failing scenario for a released capability that FastMCP advertises as supported is treated as a regression. + +The suite's `all` mode also exercises draft, pending, retired, and deliberately unsupported capabilities, so its raw pass count is broader than FastMCP's support contract. Known exceptions are recorded in [`expected-failures.yml`](https://github.com/PrefectHQ/fastmcp/blob/main/tests/conformance/expected-failures.yml) with their rationale, and new upstream scenarios arrive through deliberate suite-version updates rather than silently changing CI. + ## When should I pin `mode="legacy"`? Pin it when your code depends on the session the handshake creates: `client.ping()` and `transport.get_session_id()` have no modern equivalent, since a sessionless connection has neither a live back-channel to ping nor an id to hold. It is also the escape hatch when a server misbehaves under discovery or you need the classic `initialize` result object.