* Add FASTMCP_SSRF_TRUST_PROXY to allow SSRF fetches through a corporate proxy
🤖 Generated with Claude Code
* Make SSRF fetch client trust_env explicit for proxy routing
🤖 Generated with Claude Code
* Warn when SSRF proxy trust is enabled without a configured proxy
🤖 Generated with Claude Code
* Warn when NO_PROXY would send an SSRF-trust-proxy fetch direct
🤖 Generated with Claude Code
* Refuse SSRF-trust-proxy fetches when no proxy would route the target
🤖 Generated with Claude Code
* Fix TestProxyMode mocks to patch httpx2.AsyncClient
main's httpx -> httpx2 migration (#4503) landed after these tests were
written; ssrf.py's fetch path already uses httpx2.AsyncClient, but
TestProxyMode still patched the old httpx module, so the mock silently
stopped intercepting and requests escaped to the real network.
* Fix port-qualified NO_PROXY bypass in SSRF proxy-trust guard
proxy_bypass(hostname) discarded the port, so a NO_PROXY entry like
127.0.0.1:8443 went undetected while httpx2 honored it and sent the
request direct with the blocklist already disabled. Pass host:port
instead, except for IPv6 literals, where httpx2 ignores port when
matching NO_PROXY and neither bracketed nor unbracketed host:port
reliably matches through proxy_bypass()'s own parser.
* Replace NO_PROXY prediction with explicit proxy control in SSRF trust-proxy mode
Predicting httpx2's proxy routing (via proxy_bypass(), then via httpx2's own
get_environment_proxies()/URLPattern internals) kept diverging from its real
NO_PROXY handling — three rounds, three different divergences, always in the
unsafe direction. Read HTTPS_PROXY/ALL_PROXY directly and pass it to httpx2
explicitly with trust_env=False, so the request provably goes through that
proxy instead of being predicted to. NO_PROXY is no longer evaluated in this
mode: a NO_PROXY'd host is now routed through the proxy rather than refused,
since that's strictly safer than the alternative (direct with the blocklist
already off).
---------
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
* 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>
* Add subject field to AccessToken initialization
Fixes#4266
Add `subject` property to the AccessToken.
```python
return AccessToken(
token=access_token_as_dict["token"],
client_id=access_token_as_dict["client_id"],
scopes=access_token_as_dict["scopes"],
subject=access_token_as_dict["subject"],
# Optional fields
expires_at=access_token_as_dict.get("expires_at"),
resource=access_token_as_dict.get("resource"),
claims=access_token_as_dict.get("claims") or {},
)
```
* Populate AccessToken.subject across all token verifiers
Closes#4266. get_access_token().subject was always None: the SDK's
AccessToken.subject wasn't carried into FastMCP's AccessToken by the
dependency-layer conversion, and none of the built-in TokenVerifiers
(JWT, introspection, and the OAuth-provider verifiers for Discord,
Clerk, Google, WorkOS, HuggingFace, GitHub, and Cognito) populated it
from the sub claim/field they already extract.
---------
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
* Add Auth0MCPProvider for Auth0 Auth for MCP
* Document Auth0 MCP provider integration
* Add Auth0MCPProvider scope and auth rejection tests
Cover permissions-based required_scopes enforcement and unauthenticated MCP 401 responses.
* fixed documentation
* Narrow Auth0 docs to integration guide only
* Fix Auth0 provider: use httpx2 instead of httpx
httpx is a dev-only transitive dependency in this repo; runtime installs
declare httpx2 exclusively. The module-level 'import httpx' in auth0.py
broke import on a clean install of fastmcp or fastmcp-slim[server].
---------
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
#4466 added examples/ to [tool.ty.src] include, but two filters still
excluded it: the ty prek hook's files: scope and the static-analysis
workflow's push path triggers. An examples-only commit or direct push
to main could skip the gate entirely.
* Document icon theme support and add round-trip tests
MCP SDK v2 added a `theme` field to `Icon` (light/dark), letting a
server ship complementary icon variants for clients that render in
different UI themes. Document the field on the icons page and cover
it with round-trip tests through the server/client protocol.
* docs: add 4.0.0 version badge to Theme Variants section
* 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)
* Add examples/ to ty static-analysis gate
* Fix example type errors and stale SDK idioms for ty
* Use typing_extensions.TypedDict for the quiz tool-param type
Question is a take_quiz parameter, so FastMCP builds a Pydantic schema
for it; typing.TypedDict raises PydanticUserError on Python 3.10/3.11
(only 3.12+ accepts it). ty and 3.12 runs miss this, so it slipped in.
* Guard get_access_token() None case in huggingface_oauth example
Caught by the ty gate this PR adds: the example, merged separately,
had never been type-checked against examples/. Matches the existing
aws_oauth/keycloak_oauth pattern.
* Print actual YAML text in custom serializer example
* Improve DescopeProvider scope discovery and well-known URL support
Co-authored-by: Cursor <cursoragent@cursor.com>
* Simplify DescopeProvider scope and URL handling
Co-authored-by: Cursor <cursoragent@cursor.com>
* Make DescopeProvider scope discovery async and lazy
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🤖 Generated with Claude Code
* Use generic scope in Descope tests
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🤖 Generated with Claude Code
* Address Descope discovery edge cases
* Deduplicate Descope metadata fallback
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
* 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.
* 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.
* Add regression test: HTTP lifespan fires once per process across sessions
* Drop redundant enter-count assertion at teardown (CodeQL)
* Assert session-manager lifespan entry directly, not user-lifespan count