* Remove 3.x-era compatibility shims
* Require response_type in ctx.elicit()
* Name the utilities path for the two non-re-exported auth helpers
* Point sampling handler migration at its submodule
* Add require_roles auth check
* Make role docs runnable standalone and fully annotated
* Treat a scalar role claim as one role; correct step-up docs
* Add v4 version badge to require_roles docs
* Use issuer_url for OAuth issuer identity, not base_url
* Apply ruff format to issuer identity tests
* Align ID-JAG audience docstring with issuer_url
* Make InMemoryOAuthProvider keyword-only like its parent
* Keep ID-JAG audience on base_url, out of scope for issuer identity
* Remove stray scratch script
* Make AuthorizationHandler keyword-only
* Bind ID-JAG audience to the issuer identifier
* Fix double slash in issuer_url well-known log hint
A proxy has no back-channel to the real user, so driving a backend ask inside
it failed outright. Surface it as a result for the parent, as ProxyTool does.
Partial fulfillment means two in-flight updates can carry different answers,
so acknowledging the one that loses the update lock stranded the task on a key
the client had already sent.
Prompt and resource asks carry no content, so caching one stored an empty
result and the client never saw the question. Bypass the cache on
continuation legs and return asks unwrapped, as tool calls already did.
Keep the final outstanding input marker until the next task leg is durable,
so a racing tasks/get cannot read a parked leg as complete. Let resources and
resource templates return InputRequiredResult like tools and prompts. Identify
parked requests by their question rather than sort order.
Deletes fastmcp/server/sampling/, Context.sample/sample_step/list_roots, and
FastMCP(sampling_handler=). The proxy's handshake-era relay now reaches the
front session through the SDK directly.
* Honor OAuth application_type in DCR (SEP-837)
* Simplify web redirect check per ruff SIM103
* Enforce application_type over HTTP, at auth time, and tighten native scheme rules
Recover the DCR application_type the SDK RegistrationHandler drops (P1), enforce the stored type on the authorization redirect path (P2), restrict native to loopback http + custom schemes (P2), and document the web/native rules (P2).
* Fix loopback range detection and use a positive scheme allowlist
Classify loopback hosts with ipaddress.is_loopback so all of 127.0.0.0/8 counts (a web client could bypass the non-loopback rule with 127.0.0.2). Replace the NON_REDIRECT_NETWORK_SCHEMES denylist with STANDARD_URI_SCHEMES: native now accepts only https, loopback http, and unregistered private-use schemes per RFC 8252, so smb/smtp/nfs and other unlisted standard schemes no longer pass.
* Vendor the IANA scheme registry and consolidate the loopback classifier
Replace the hand-picked STANDARD_URI_SCHEMES with a vendored snapshot of the IANA URI scheme registry (423 schemes), so registered transports nobody enumerated (coap, coaps, stun, turn, mqtt) fail closed instead of passing as private-use. Delete the stale duplicate _is_loopback_host in oauth_proxy/models.py and reuse the ipaddress-based classifier from redirect_validation, restoring loopback port flexibility across all of 127.0.0.0/8.
* Treat the reserved localhost namespace and absolute host forms as loopback
RFC 6761 6.3 reserves the whole localhost namespace for the local machine, so app.localhost and localhost. are loopback just as much as localhost. Previously a web client could register https://app.localhost/callback and bypass the non-loopback rule, while native clients were wrongly refused legitimate http://app.localhost:3000 dev callbacks. The suffix test is anchored on a leading dot so localhost.evil.com and notlocalhost stay non-loopback.
* Narrow scope: drop native scheme classification, keep the web rule
Registry membership cannot separate app-dispatch schemes from network transports (vscode is registered because it is an app scheme), so classifying a native client's scheme rejected callbacks that real MCP clients need. Remove the vendored registry and the private-use test; native now accepts any scheme outside the unsafe set, with cleartext http still limited to loopback. Also reject web registrations that omit redirect_uris rather than storing an unusable localhost placeholder.
* Signal component-level scope shortfalls as insufficient_scope (SEP-2350)
* Fix ty type narrowing in scope step-up test
* Respect check short-circuit when reporting scope shortfall (P2)
* Report union of unmet scopes and document step-up contract
* Aggregate scope shortfall across the AuthMiddleware chain
* Stop chain scope aggregation at the first unevaluated gate
* Design doc: stateless session state
* Add stateless session-state primitives: Scope, SessionCodec, scoped state
* Add SessionProvider and Session() annotation for stateless session state
* Rewrite session-state design to final shape (Session object, two patterns, no seal)
* Rework stateless session state to final Session/SessionId design
Remove Scope, SessionCodec/sealing, and scoped ctx.get_state. Add the
Session object (get/set/delete/clear over one dict per (principal,
session_id) key), injected session: Session (keyed by principal, requires
auth), session_id: SessionId argument with auto-filled description, and
SessionProvider contributing create_session/end_session.
* Rename injected marker to UserSession; auto-wire SessionProvider on SessionId
* Document stateless session state as a v4 feature
* Require SessionProvider and make sessions create-then-validate
Remove the implicit SessionProvider auto-wiring; a SessionProvider must now be
registered explicitly. create_session records an owned session and get_session
validates the id, rejecting uncreated or foreign ids.
* Add Session.id (public id for session_id sessions, None for UserSession)
* Fix ty: narrow Tool | None and ToolResult.structured_content in session tests
* Fix session-provider enforcement gap for non-local tools; stop embedding raw principal in UserSession key
* Fix disabled session_id tools blocking listing; reject local tools shadowing SessionProvider lifecycle names
* Decouple SessionId description from lifecycle tool name so it survives namespaced mounts
* Remove SessionProvider enforcement; get_session validation is the guarantee
* Fix stale enforcement/key-format docs; document store-owned session TTL
* Dedup SessionId contract description; tighten context.mdx session-state lead
* Make session store/description resolution work in Docket task workers and for partial tools
* Expose get_session as a standalone task-safe function; drop foreground-only Context.get_session
* Move get_session to dependencies alongside the other request accessors
* Reframe context state docs as Request State; cross-request persistence points to Session State
* Address UserSession injection edge cases from review
- inject a UserSession instance (not bare Session) so isinstance holds
- support session: UserSession | None = None (inject None when unauth)
- detect SessionId params past a partial's positional binding
A task tool that returns InputRequiredResult now ends its leg (freeing the
worker) and stores the ask as durable state; tasks/update enqueues a fresh
Docket execution (the next leg) with accumulated request_state/input_responses
injected via ctx. No worker ever blocks on input, so a parked task no longer
holds up shutdown. Imperative ctx.elicit() inside a task is removed and raises
with guard-pattern guidance.
Co-Authored-By: Claude <noreply@anthropic.com>
TasksExtension serves io.modelcontextprotocol/tasks on the extension API:
a decide-and-task tools/call interceptor (era-gated to modern connections),
tasks/get with inlined results and inputRequests, tasks/update delivering
poll-based in-task elicitation, tasks/cancel, durable creation, and
auth-scoped task isolation. Wire models validate against the vendored
ext-tasks schema. Worker-side Context hooks are refcounted so sibling
servers cannot strand each other's workers.
Co-Authored-By: Claude <noreply@anthropic.com>
Engine modules (keys, context snapshot, docket lifespan, worker CLI,
client handles) move intact; SEP-1686 wire modules park in _legacy_wire
for adaptation to SEP-2663. Core keeps task=True declaration on tools
only and raises at serve time until the tasks extension is registered.
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix percent-encoded skill file names unreadable in resources mode
Encode supporting-file paths explicitly (quote/unquote) when building
and resolving skill:// resource URIs, instead of relying on AnyUrl's
implicit encoding. This also closes the ambiguity where a file literally
named "setup%20guide.md" would collide with "setup guide.md" once both
were percent-encoded.
Fixes#4545
* Quote main_file_name when building its resource URI
Keeps the main-file URI on the same explicit quote/unquote round-trip
as supporting files, so a custom main_file_name containing a literal
'%' still resolves after the shared unquote() in _get_resource().
* Add server-side argument completion (@mcp.completion)
* Reference CompletionValues directly in cast so the import reads as used
* Import completion types from mcp_types, not the fastmcp.types mirror
* Fix test imports after dropping the fastmcp.types mirror
* Fix change-register example import after dropping the types mirror
* Enforce 100-value completion cap; make docs example runnable
* Document completion authorization contract
* Offload sync completion handlers to threadpool
* Exclude bare str from completion return type
* Pass Any-typed value in bare-string rejection test
* Point completion authoring types to mcp_types in v4 notes
Resource/prompt error detail and proxy instructions/connection-error
surfacing now work on the modern protocol era, so the tests pinned to
mode="legacy" with a TODO(defect)/TODO(mode="legacy" pin) marker run
on the default auto mode again.
Three defects hidden by tests pinned to the handshake era, where a raw
exception reaches the wire as str(exc). At 2026-07-28 the runner masks
anything that is not an MCPError/ValidationError as "Internal server error".
- _on_read_resource / _on_get_prompt now translate FastMCPError through
to_mcp_error, mirroring _on_call_tool. Masking is unchanged.
- FastMCPProxy registers a server/discover handler so upstream instructions
reach modern clients; on_initialize only fires for the handshake.
- ProxyProvider's list methods normalize transport failures into MCPError.
Removes pins added while making the auto-default suite pass that weren't
actually testing older-protocol-only behavior, and keeps (with a stated
reason) the ones that are. Along the way, fixes two real defects the audit
surfaced in the modern protocol path: PingMiddleware could leak a
_active_sessions entry when a connection's exit_stack closed before its
keepalive task got its first scheduler turn, and FastMCP(experimental_
capabilities=...) was silently dropped from server/discover responses
(it only ever reached the legacy initialize handshake).
Most pins in tests/server/providers/proxy/ were added only to keep tests
green while unpinning changed which protocol era the proxy's backend
connection used, not because the test's subject cared about the era. With
proxy era-mirroring (#4573) landed, a front client on auto correctly moves
the whole chain to the modern protocol, so plain tool/resource/prompt calls
through a proxy no longer need a pin.
Kept pins fall into three buckets, each commented at the call site: tests
whose subject is genuinely handshake-only (sampling, roots, elicitation
push-forwarding, ping, initialize handshake mechanics); tests whose backend
is a directly-constructed ProxyClient/StatefulProxyClient, which always
defaults to legacy independent of the front era; and two tests left pinned
with a TODO documenting a real defect this audit surfaced (upstream
instructions not forwarded to a modern-era client through a proxy, and
ProxyProvider.list_tools leaking an unwrapped connection error instead of
an MCPError).
* Mirror front protocol era onto proxy backend connection
A proxy created from a non-Client target now negotiates, on its backend,
whatever era its front client negotiated, instead of pinning one era.
Explicit create_proxy(mode=...) still overrides. Guards the eager backend
initialize() so an explicit modern pin behind a handshake front no longer
crashes.
* Carry the mirrored proxy era into multi-server config backends
A multi-server MCPConfig target mounts one proxy per configured server on a
composite router, so setting the era on the outer client stopped at the router
and every real backend stayed on its default era. TransportOptions.backend_mode
carries it down, resolved per request alongside the outer mirroring.
The router is also sealed under a policy held on the transport rather than a
fresh per-router ephemeral key, so a guard tool's request_state survives the
router being rebuilt between rounds.