The xfail register
Roughly fortyxfail markers across the test tree name the SDK gaps and removed protocol surfaces they wait on. Re-running the suite against a new SDK beta surfaces which have closed (a strict xfail that starts passing fails the suite, prompting removal of the marker). They cluster in three areas — but the largest cluster is no longer a set of gaps to close.
Task suite (tests/server/tasks/, tests/client/tasks/) — SEP-1686 wire layer being removed; engine rebuilt on SEP-2663. The large majority. These cover the 2025 task protocol (SEP-1686), which left the core MCP spec and was reworked into the io.modelcontextprotocol/tasks extension (SEP-2663). FastMCP’s SEP-1686 wire machinery (capability advertisement, the tasks/get|result|list|cancel handlers, the push notification/elicitation relay) is slated for removal, so the wire-protocol xfails disappear with the code they cover — they are not waiting on an SDK fix. The Docket/Redis execution engine underneath is not discarded: it is extracted into the planned fastmcp-tasks package and re-adapted to the SEP-2663 polling shape (see Background Tasks (SEP-2663)). The two SDK gaps these were originally filed against — sdk-feedback #1 (SEP-1686 task result types omitted from the method registries) and sdk-feedback #3 (no task field on ReadResourceRequestParams / GetPromptRequestParams) — are moot: they patched the SEP-1686 wire shape, which SEP-2663 replaces with a CreateTaskResult claimed on tools/call. The gap that matters for the rebuild is sdk-feedback #2 (extensions capability stripped at pre-2026 negotiated versions) — it now gates a flagship feature and is escalated accordingly.
Protocol eras (tests/server/test_protocol_eras.py). One remaining strict xfail, and it too is task-related: the v2 SDK high-level client exposes no task= parameter on call_tool, so a SEP-1686 task-augmented tools/call cannot be submitted through it. It resolves with the SEP-1686 wire-layer removal above; the SEP-2663 rebuild submits tasks by advertising the extension capability and claiming a CreateTaskResult, not through a task= params field. The earlier strict xfail for the ctx.elicit / ctx.sample “Method not found” degradation (sdk-feedback #10) is gone — the era-gating shipped in #4448 flipped it to a passing test.
MCP Apps (tests/test_apps.py). Two xfails tied to sdk-feedback #2 — the extensions capability is stripped by the pre-2026 version sieve, so the UI extension can’t be advertised to legacy-era clients.
Shims and their removal triggers
Every shim in the migration is temporary and carries a documented removal trigger.
The
TaskNotificationHandler binding (sdk-feedback #8) is the client-side equivalent: it registers a NotificationBinding for the SEP-1686 notifications/tasks/status because the SDK no longer tees custom server notifications to the message handler. It goes away with the SEP-1686 wire machinery it serves; the fastmcp-tasks client half registers its own binding for the SEP-2663 notifications/tasks shape when it ships (push notifications are deferred to a later fastmcp-tasks version — v1 is polling-only).
Statelessness on 2026-07-28
The2026-07-28 era is stateless by protocol construction, and the recurring maintainer question is whether that statelessness has to be woven through FastMCP everywhere. It does not — but the honest accounting has three parts: features that are legacy-only because the protocol removed the mechanism, features that already work because they never relied on a session, and a short list of design holes where the current code doesn’t error but also doesn’t work. Everything below concerns 2026-07-28 connections only. Every client in the field today negotiates a handshake era, where all of this behaves exactly as it always has.
The SDK ground truth. On the modern paths the SDK’s Connection is strictly per-request: a fresh Connection is built from each POST’s envelope, its exit_stack unwinds when the request returns, connection.session_id is always None, and connection.state is a fresh dict per request. The manager’s stateless flag never enters the picture — modern routing short-circuits ahead of it. There is no standing server→client stream: notifications emitted during a request ride that POST’s own SSE sink, and anything emitted after the POST returns is dropped (_NO_CHANNEL); server→client requests raise NoBackChannelError. The only replacement is subscriptions/listen, which carries four list-changed / resource-updated event kinds and nothing else — no logging, progress, or task-status events, no resumability, and it is not yet wired into FastMCP. There is no EventStore or Last-Event-ID on modern paths at all; both belong to the legacy transport.
Legacy-only by construction — document, don’t build
These are not bugs. The protocol removed the mechanism they depend on, so they are simply out of scope on2026-07-28:
- Per-session log levels.
logging/setLevelis absent from the 2026 method registry, so the_client_log_levelshandler is unreachable. There is no per-session log-level state because there is no session. EventStore/ resumability.EventStore,SessionScopedEventStore, and Last-Event-ID resumption are never constructed on the modern paths. Resumability presupposes a durable stream, which the era does not have.- Ping keepalive. Server-initiated ping is a server→client request and is therefore structurally a no-op on modern connections; the SDK owns SSE-level pings on this transport.
Already stateless by construction — works on 2026
These work on2026-07-28 today because they never leaned on a protocol session:
tasks/getpolling. Task result retrieval is keyed bytask_idand backed by Docket/Redis, so a client polls across independent requests without any session affinity. This session-free polling is exactly why the execution engine survives the SEP-1686-to-SEP-2663 rework: the SEP-2663 wire shape (polltasks/get, resolve in-task input viatasks/update) maps onto the same durable store, and SEP-2663’sMcp-Name: <taskId>routing header is moot for a shared-Redis deployment where any replica can serve the poll. See the xfail register.- OAuth bearer validation. Auth is per-request bearer validation — every POST carries and re-validates its own credential.
- In-request progress and logging notifications. Notifications emitted while a request is still streaming ride that POST’s SSE sink and are delivered normally.
Design holes deferred to the multi-protocol workstream
The remaining items are real holes, deferred to the first-class 2026 client workstream because they all reduce to one unanswered question — what is a session when the protocol has none? The danger in each is that the code currently returns without erroring, which reads as “works” but is actually silent degradation. Again: these affect2026-07-28 connections only; on the handshake eras every one of them behaves correctly.
ctx.session_idandctx.set_state/ctx.get_state(broken even single-replica). On a modern requestctx.session_idmints a freshuuid4, cached on the per-requestconnection.statethat is discarded when the request returns. Soctx.set_stateandctx.get_statesilently never round-trip across requests — no error, just lost data. The open design decision is whethersession_idshould becomeNonewithset_statedocumented as session-era-only, or be re-based on an app-level key (the auth subject, or a client-supplied header).- Task push and in-task input — resolved by the SEP-2663 design, not a statelessness hole. This was previously framed as a hole because SEP-1686 leaned on a push back-channel (the notification/elicitation relay) that dies once the submitting request returns. SEP-2663 removes the dependency: in-task input is poll-based — the task enters
input_required, surfaces its outstanding elicit/sample/roots requests in aninputRequestsmap ontasks/get, and the client answers viatasks/update. That round-trips through the durable store with no session affinity, so it is stateless-safe by construction. The SEP-1686 push relay (server/tasks/elicitation.py,notifications.py) is removed; thefastmcp-tasksrebuild implements the poll-based channel instead. Foreground (non-task) elicitation on 2026 remains the guard-modeInputRequiredResult. - Stateful proxy affinity (degraded). The stateful proxy’s
_cachesare keyed by the per-requestConnection, so on modern connections the proxy collapses to stateless proxying: results stay correct, but the per-session affinity guarantee is lost. This is decided alongside thesession_idquestion — same root — or gated to the legacy/stdio transports.
SubscriptionBus) are deployment configuration rather than protocol gaps and are out of scope for this section.
Upstream advisory dossier
FastMCP acts as an advisor to the SDK team. The migration produced a dossier of ten findings (sdk-feedback.md) — verified bugs and hard edges to report upstream, plus questions to bundle into a feedback thread. The highest-priority items:
- #1 (bug) — SEP-1686 task result types ship but the method registries omit them. Moot: the SEP-1686 wire shape was removed from the spec; the SEP-2663 rebuild claims
CreateTaskResultontools/callthrough the extensions mechanism, which the registries already admit. - #2 (bug/question) —
capabilities.extensionsstripped at pre-2026 negotiated versions. Elevated: this now gates theio.modelcontextprotocol/tasksextension (and MCP Apps) on the modern era, so it blocks a flagship v4 feature rather than an edge case. Worth prioritizing in the upstream thread. - #4 (security) — DCR redirect-URI validation accepts
javascript:/data:schemes. - #5 (hard edge) —
streamable_http_clientdrops session-id access with no replacement. - #8 (hard edge) — custom server notifications are dropped, not tee’d to
message_handler. - #10 (hard edge) — 2026 push-feature degradation error quality is inconsistent. Resolved on the FastMCP side:
ctx.elicit/ctx.sampleare era-gated to raise a clear error on modern connections (#4448).
GA transition checklist
The beta-to-stable transition is a small set of tracked steps:- Swap the pins. When
mcp 2.0.0reaches GA, changemcp-types==2.0.0b1(core) and themcppin (the[mcp]extra) infastmcp_slim/pyproject.tomlfrom the beta to the stable release, and cut4.0.0instead of another pre-release. - Re-run the xfail suite against the GA SDK. Any strict xfail that starts passing means a gap closed — remove the marker and, where applicable, the corresponding shim.
- Confirm
release/3.xis cut from pre-mergemainand receiving upstream security patches for users who stay on the SDK v1 line.

