Commit graph

23 commits

Author SHA1 Message Date
Jeremiah Lowin
80f753c1cb
Enforce protocol-version pins by era, not exact handshake revision
The initialize handshake is negotiated by the SDK with no knowledge of the
server's protocol_versions allowlist, and FastMCP can only veto the handshake,
not steer the negotiated revision. So a server pinned to an older handshake
revision (e.g. ["2024-11-05"]) wrongly refused an ordinary client that
requested a newer handshake revision. Enforce era membership for handshake
versions (exact membership stays for modern per-request versions), refusing
only a genuine cross-era mismatch.
2026-07-20 18:06:32 -04:00
Jeremiah Lowin
6107ad184c
Merge remote-tracking branch 'origin/main' into HEAD 2026-07-20 17:56:59 -04:00
Jeremiah Lowin
57bbc9859e
Surface resource, prompt, and proxy errors on the modern protocol era (#4579)
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.
2026-07-20 17:52:12 -04:00
Jeremiah Lowin
eac1c9d006
Merge remote-tracking branch 'origin/main' into feature/server-protocols
# Conflicts:
#	docs/development/v4-notes/change-register.mdx
2026-07-20 16:31:16 -04:00
Jeremiah Lowin
0ba3db1a56
Mirror the frontend's protocol era on a proxy's backend connection (#4573)
* 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.
2026-07-20 12:31:02 -04:00
Jeremiah Lowin
f5fca97c81
Respell server protocol restriction as a version allowlist
Replace `min_protocol_version` with `protocol_versions`, an allowlist of MCP
protocol versions taking the SDK's own era tuples. Versions are an enumerated
set, not an ordered scalar, so enforcement is set membership rather than a
bound -- which is what makes handshake-only expressible.

Generalize the startup coherence check into a capability -> required-versions
map, and make it silent unless a version set was declared.
2026-07-20 12:20:56 -04:00
Jeremiah Lowin
aca5dc99b9
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.
2026-07-19 21:20:00 -04:00
Jeremiah Lowin
3213776b25
Run FastMCP middleware for every inbound message (#4553)
* Make the SDK seam the root of FastMCP middleware dispatch (D3)

Notifications, cancellations, and malformed/unroutable messages now reach
on_message/on_request/on_notification at the SDK seam. Component methods keep
their interior dispatch (typed hooks, tool-exception visibility) unchanged; the
seam covers only messages the interior never dispatches, so each hook fires once.

* Document the middleware seam coverage and suspend semantics (D3)

* Align seam docs and ask-visibility test with the result-cycle MRTR model

An InputRequiredResult is the full result of a complete request->response
cycle, not a suspension: component hooks observe an asking round's
InputRequiredToolResult as an ordinary return value.

* Replace 'seam' language with plain dispatch terminology

* Keep the raw middleware __call__ signature; forward middleware message edits

* Cover fires-once across an MRTR continuation round

* Align cherry-picked coverage test with renamed recorder

* Rewrite only the message, never the dispatch destination
2026-07-19 20:29:08 -04:00
Jeremiah Lowin
1e529ee27d
Stop proxies from validating backend results or mutating shared transports (#4552) 2026-07-19 19:24:11 -04:00
Jeremiah Lowin
717f3535f6
Add guard-mode multi-round-trip tools (SEP-2322) (#4544)
* Add guard-mode MRTR server support (SEP-2322)

* Add server-side MRTR guard tests

* Add MRTR guard docs, exports, and output-schema handling

* Apply formatting to MRTR guard changes

* Fix MRTR review round 1: middleware-safe suspend, Annotated strip, stable audience

- ToolInputRequired subclasses BaseException (CancelledError precedent) so
  error middleware's broad except Exception cannot swallow a suspension
- Strip InputRequiredResult arms inside Annotated return types
- Reject a custom RequestStateSecurity without a stable audience (random
  per-replica server names would break shared-key verification)

* Fix static analysis: rewrite tuple([...]) as tuple literal (C409)

* Recognize InputRequiredResult inside Annotated union arms

_is_input_required_type now peels Annotated first, so a metadata-carrying
guard arm (str | Annotated[InputRequiredResult, Field(...)]) is stripped
and the data arm's output schema survives.

* docs: frame multi-round tools as elicitation on the modern protocol

Fold multi-round-tools.mdx into elicitation.mdx as two eras of one
capability; drop pause/suspend framing for the stateless per-round model.

* Transport MRTR asks as InputRequiredToolResult, not a raised signal

An input-required result is the full result of a stateless MRTR leg, so it
flows through the middleware chain as an ordinary ToolResult subclass instead
of a raised ToolInputRequired(BaseException). Middleware observes it, caching
skips it, and response-limiting leaves it untouched.

* Document MRTR middleware interaction and the isinstance pattern

* Update MRTR change-register verify note to InputRequiredToolResult

* Align test module docstring with result-cycle framing

* Fix MRTR review: bypass cache on continuation legs; soften audience guard

- ResponseCachingMiddleware skips read AND write on continuation legs:
  the cache key is name+arguments only, so a continuation's final result
  would be served to later fresh calls, which would never be asked
- The stable-audience check is a warning, not an error: a policy object
  cannot reveal whether its keys are shared, and single-process
  customization (ephemeral ttl, custom codec) is legitimate unnamed

* Treat state-only rounds as continuations in the response cache

A round carrying request_state but no questions retries with
input_responses=None; request_state alone must bypass the cache or its
terminal result is stored under the fresh-call key.

* Fix MRTR review round: preserve asks through transforms, empty-name audience, docs predicate

- TransformedTool.run returns an InputRequiredToolResult intact instead of
  reshaping it into an empty ToolResult for non-object output schemas
- audience warning uses a falsy-name check (empty string also autogenerates
  a per-replica name)
- the elicitation docs continuation predicate checks request_state too

* Add create_proxy(mode=) opt-in for guard round-tripping through proxies

An auto-created proxy client stays handshake-era by default (a dual-era
backend serves both, and one proxy session is one era; handshake preserves
server-initiated push forwarding). Pass create_proxy(target, mode="auto")
to negotiate modern so an upstream guard's InputRequiredResult round-trips —
the two are mutually exclusive per session.

* Wrap raw InputRequiredResult returned by a transform_fn

A custom transform function may return the raw ask directly, like any tool
body — wrap it into InputRequiredToolResult so it survives output
normalization and reaches the wire, not only pre-wrapped forwarded guards.

* Reject input-required results from background tasks

* Unwrap type aliases before stripping guard arms

* Apply ruff format

* Recursively strip guard arms through nested and composed aliases

* Reflect MRTR continuation fields on the middleware message

* Suppress output schema for InputRequiredResult subclasses

* Forward progress on modern proxy tool calls

* Suppress output schema for bare aliased guard returns

* Suppress output schema for any surviving guard return wrapping
2026-07-19 16:42:06 -04:00
Viktor Szépe
81fada4922
Fix typos (#4498)
* Fix typos

* Document Cachable* -> Cacheable* rename as v4 breaking change

Adds the response-cache model rename to the change register, per
maintainer decision to skip compatibility aliases in favor of clear
documentation.

* Skip invalid Before import in doc test; use inline codespell ignore

The Cachable* -> Cacheable* breaking-change entry showed the old,
now-invalid import for contrast, which the doc-example test picked up
as a real import and flagged as a regression. Comment out the
deliberately-broken "Before" line (matching the McpError entry just
above it) so only the working "After" import is exercised.

Also swap the blanket codespell ignore-words-list entry for a
narrower inline `codespell:ignore` directive on the one line that
needs it, so codespell keeps flagging "cachable" everywhere else.

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-18 21:18:39 -04:00
Jeremiah Lowin
998b37f32b
Add server-side identity assertion (SEP-990 ID-JAG) (#4483)
* Add server-side SEP-990 identity assertion (ID-JAG)

* Test SEP-990 identity assertion token endpoint

* Format identity assertion test

* Document SEP-990 identity assertion

* Thread identity_assertion through OIDCProxy

* Harden ID-JAG: authoritative scopes and grant-type enforcement

Scopes for the issued token now derive only from the signed assertion (or server policy when it omits scopes); the client-supplied request scope may narrow but never widen them. Enforce the registered grant-type constraint the SDK check bypassed, and have proxy DCR add the jwt-bearer grant to registered clients when identity assertion is enabled.

* Harden ID-JAG: honor nbf, reject non-object payload, bound jti cache, preserve required_scopes

* Document per-process ID-JAG replay limitation and nbf check

* Harden ID-JAG round 3: resource indicator, non-object header, algorithm config

- Honor RFC 8707 resource on the jwt-bearer grant (invalid_target on
  mismatch), mirroring authorize()'s invariant incl. skip-when-unconfigured
- Reject JSON-array JOSE headers with invalid_grant instead of a 500
- Add IdentityAssertion.algorithm so ES256/PS256 issuers can be verified
  (JWTVerifier otherwise defaults to RS256)

* Bind ID-JAG exchange to the assertion's signed client_id and resource

SEP-990: the IdP signs which client and which resource the assertion was
minted for. With public proxy clients the presented client_id is
self-asserted, so the signed binding is what stops client B redeeming
client A's leaked assertion — and the signed resource claim stops an
assertion for server A being redeemed at server B behind the same IdP.

* Harden ID-JAG round 4: check bindings before jti consumption; validate temporal claims, algorithm, and discovery body

- Move the client_id/resource binding checks into the validator itself,
  before jti is recorded as consumed, so an assertion presented with the
  wrong binding is rejected without burning replay protection for whoever
  it actually belongs to
- Reject non-numeric exp/iat/nbf with invalid_grant instead of a 500
- Validate IdentityAssertion.algorithm at config time (must be an
  asymmetric JWS algorithm verifiable via JWKS)
- Reject a non-object OIDC discovery body with invalid_grant instead of a 500
- Centralize the resource-URL comparison helpers used by both the
  validator and OAuthProxy.authorize()

* Rebase onto httpx2/SDK b2 and harden ID-JAG round 5

- Migrate identity assertion + tests to httpx2 and the local httpx2_mock
  (legacy httpx is now banned; pytest-httpx no longer intercepts)
- Add is_optional to the shared httpx2_mock, mirroring pytest-httpx
- Tighten the algorithm allowlist to JWTVerifier's exact supported set
  (prefix check accepted typos like RS999 -> 500 on first exchange)
- Reject non-string jti before the cache lookup (unhashable -> 500)
- Track revocation for self-contained ID-JAG tokens: revoke_token records
  the jti and load_access_token rejects it until natural expiry
- Dedupe resource-URL helpers: proxy now imports the shared
  normalize_resource_url/server_url_has_query from identity_assertion

* Advertise 'none' token-endpoint auth method when ID-JAG is enabled without CIMD

DCR clients are public, so metadata consumers must see 'none' to use the
advertised jwt-bearer grant; previously only the CIMD path added it.

* Document 2026-07-28 protocol support as a distinct feature catalog

SEP-990 identity assertion leads: the SDK provides the wire contract and
provider hook; FastMCP provides the complete server-side implementation.
Inventories the full modern-era capability set for v4.

* Harden ID-JAG round 6: lazy re-export, dual-form audience, per-issuer algorithms, discovery backoff

- IdentityAssertion re-exported lazily from server.auth (the eager import
  bypassed the package's documented lazy-import boundary)
- Accept the ID-JAG aud both with and without base_url's trailing slash;
  metadata advertises the slashed form, so IdPs echoing it verbatim work
- algorithms={issuer: alg} per-issuer override, mirroring jwks_uris
- OIDC discovery serializes per-issuer and backs off 30s after a failure
  (discovery runs pre-signature, so garbage could amplify into HTTP floods)
2026-07-18 19:52:15 -04:00
Jeremiah Lowin
18b5ab5852
Migrate to MCP SDK v2.0.0b2 (httpx2) (#4503) 2026-07-18 15:12:47 -04:00
Jeremiah Lowin
d779414f8a
Screen templated resource parameters for path traversal by default (#4482)
* Add ResourceSecurity screening for templated resources (defaults on)

* Add tests for resource path-security screening

* Document resource path-security; fix ty in tests

* Carry child template security policy through provider mount

Preserve a mounted template's explicit ResourceSecurity (per-param
exemptions or a deliberate opt-out) through FastMCPProviderResourceTemplate.wrap
so the parent read chokepoint honours it instead of the parent default.

* Defer mcp SDK import so fastmcp.resources loads without the [mcp] extra

* Make resource path-security docs examples self-contained and runnable

* Match exempt_params under both hyphen and underscore spellings

Template placeholders like {git-ref} extract as git_ref, so an exemption
written with the natural URI-template spelling never matched.

* Docs: describe net-depth traversal rule accurately; make example runnable

The screening only rejects .. segments that escape the starting depth
(foo/../bar passes) — saying any standalone .. is rejected overstated
the guarantee. Also define DOCS_ROOT so the example runs.
2026-07-17 17:42:48 -04:00
Jeremiah Lowin
a04f6fd911
Add telemetry off-switch and mcp.protocol.version span attribute (#4481)
* Turn OpenTelemetry instrumentation on by default with explicit off-switch

Add FASTMCP_ENABLE_TELEMETRY setting (default true) and mcp.protocol.version
span attribute for SDK parity.

* Make disabled telemetry a transparent pass-through, not a NoOpTracer

The stock NoOpTracer.start_as_current_span attaches a NonRecordingSpan, hijacking the current OTel context from any enclosing application span. When telemetry is disabled, get_tracer() now returns a non-attaching pass-through tracer so trace.get_current_span() inside handlers still resolves to the caller's span.
2026-07-17 17:02:48 -04:00
Jeremiah Lowin
836ceac30e
Add KeyValueResponseCacheStore for distributed client response caching (#4479)
* Add KeyValueResponseCacheStore adapter for client response cache

* Test KeyValueResponseCacheStore round-trip, isolation, and distributed sharing

* Document distributed response cache store
2026-07-17 16:41:35 -04:00
Jeremiah Lowin
8ba5b89918
Add server-level cache hints (SEP-2549) (#4464) 2026-07-08 09:10:41 -04:00
Jeremiah Lowin
f0e350942f
Merge pull request #4448 from PrefectHQ/modernize/sampling-deprecation
Deprecate ctx.sample and add clear errors for push features on 2026 connections
2026-07-07 08:00:08 -04:00
Jeremiah Lowin
ad02710e9b
Docs: register 4.0 deprecation removals in change register 2026-07-07 07:53:10 -04:00
Jeremiah Lowin
6621024ce4
Drive the FastMCP lifespan through the SDK session manager (#4446) 2026-07-07 07:49:16 -04:00
Jeremiah Lowin
ac78e6f693
Emit one SERVER span per request and adopt spec-correct error codes (#4445) 2026-07-07 07:49:06 -04:00
Jeremiah Lowin
b023aae51a
Docs: mark sampling deprecated, update era matrix and change register 2026-07-06 21:32:17 -04:00
Jeremiah Lowin
3522a98766
Migrate to MCP Python SDK v2 (#4437) 2026-07-06 17:36:45 -04:00