Compare commits

...

201 commits

Author SHA1 Message Date
marvin-context-protocol[bot]
8a1820f1c3
chore: Update SDK documentation (#4782)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-08-07 15:49:36 -05:00
Sai Mouli
06fee6d300
Serialize the event store's stream list read-modify-write (#4758)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-08-06 20:09:36 -04:00
Jamie Zieziula
706f7d2695
feat(renovate): migrate to Renovate, retire Dependabot (#4754)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-06 20:02:29 -04:00
Chris Guidry
1ac8fc6060
Encrypt task context snapshots at rest (#4772)
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-06 20:01:40 -04:00
Jeremiah Lowin
04f9971120
Delegate typed tool output serialization to Pydantic (#4771) 2026-08-06 19:59:19 -04:00
marvin-context-protocol[bot]
803da5319c
chore: Update SDK documentation (#4679) 2026-08-06 19:56:40 -04:00
Jake Kaplan
9feb1f378b
Forward proxy server metadata across protocol eras (#4776)
* Forward proxy negotiation metadata

🤖 Generated with OpenAI Codex

* Limit forwarded proxy metadata

🤖 Generated with OpenAI Codex

* Tighten negotiation metadata forwarding

🤖 Generated with OpenAI Codex

* Tighten proxy metadata docs

🤖 Generated with OpenAI Codex

* Keep proxy metadata middleware with provider

🤖 Generated with OpenAI Codex

* Simplify proxy negotiation middleware API

🤖 Generated with OpenAI Codex

* Name proxy metadata middleware directly

🤖 Generated with OpenAI Codex

* Preserve discovery middleware contracts

🤖 Generated with OpenAI Codex

* Clarify proxy metadata ownership

🤖 Generated with OpenAI Codex

* Align proxy metadata wording

🤖 Generated with OpenAI Codex

* Call forwarded values server metadata

🤖 Generated with OpenAI Codex

* Harden proxy metadata reads

🤖 Generated with OpenAI Codex

* Expose configured discovery result

🤖 Generated with OpenAI Codex

* Preserve proxy discovery compatibility

🤖 Generated with OpenAI Codex

* Preserve deprecated initialization middleware

🤖 Generated with OpenAI Codex

* Harden proxy metadata boundaries

🤖 Generated with OpenAI Codex

* Restore deprecated middleware location

🤖 Generated with OpenAI Codex

* Simplify proxy metadata client lifecycle

🤖 Generated with OpenAI Codex

* Clarify proxy metadata lifecycle

🤖 Generated with OpenAI Codex

* Preserve proxy factory errors

🤖 Generated with OpenAI Codex

* Detach forwarded proxy metadata

🤖 Generated with OpenAI Codex

* Simplify proxy metadata implementation

🤖 Generated with OpenAI Codex

* Distinguish proxy metadata failures

🤖 Generated with OpenAI Codex

* Narrow proxy metadata validation fallback

🤖 Generated with OpenAI Codex

* Retrigger CI

🤖 Generated with OpenAI Codex
2026-08-06 19:09:05 -04:00
nate nowack
75fb116e36
Support EdDSA verification in JWTVerifier (#4752) 2026-08-06 14:09:37 -04:00
Jeremiah Lowin
6fb34e9383
Document MCP protocol support and conformance (#4781) 2026-08-06 11:15:38 -04:00
Yonatan
c8b88b3a37
fix(context): move elicit overload docs inside the stubs so mypy sees the chain (#4774) 2026-08-06 09:35:49 -04:00
Jeremiah Lowin
2bee9aeb58
Clarify review of closed contributor PRs (#4780) 2026-08-06 09:34:11 -04:00
Jeremiah Lowin
875e8e18bd
Preserve legacy httpx compatibility without importing it (#4766) 2026-08-05 18:09:16 -04:00
Jake Kaplan
959daf2321
Sanitize forwarded request metadata where the proxy copies it (#4770)
* Separate proxy protocol policy from client construction

🤖 Generated with OpenAI Codex

* Strip connection-owned request metadata at the proxy backend boundary

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Sanitize forwarded request metadata where the proxy copies it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Forward hop-safe request metadata for proxied resources, templates, and prompts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 15:46:10 -04:00
nate nowack
8661193411
Quote run-claude allowed tools argument (#4741)
Generated with Codex
2026-08-05 11:15:51 -04:00
Jeremiah Lowin
e4d8ca648a
Avoid loading MCP and CLI stacks during lightweight imports (#4763) 2026-08-05 10:56:13 -04:00
Jeremiah Lowin
2c2f98691f
Docs: mirror v3.4.6 release notes (#4764) 2026-08-05 10:53:15 -04:00
nate nowack
b9b7ea6914
Declare run-claude extra allowed tools input (#4740)
Generated with Codex
2026-08-05 10:39:35 -04:00
Sai Mouli
4f28dceac8
Don't cache error results in ResponseCachingMiddleware (#4705) 2026-08-04 17:15:01 -04:00
LHMQ878
db92d44ef5
Serve empty list results from the response cache (#4738) 2026-08-04 17:11:42 -04:00
Jeremiah Lowin
886776f5fc
Canonicalize response cache arguments (#4753)
Co-authored-by: LHMQ878 <LHMQ878@users.noreply.github.com>
2026-08-04 15:25:46 -04:00
Jeremiah Lowin
d267792653
docs: clarify external OAuth consent mode (#4746) 2026-08-03 18:02:09 -04:00
nate nowack
a7e9b70919
Fix static analysis with latest ty (#4739)
* Fix upgrade static analysis

Generated with Codex

* Preserve concrete transport return types

Generated with Codex

* Avoid widening transport return types

Generated with Codex

* Model transforming transport return types

Generated with Codex

* Exclude standalone screenshot examples from ty

Generated with Codex
2026-08-03 10:53:23 -05:00
Jeremiah Lowin
022547ad8c
Preserve string-compatible prompt arguments (#4730) 2026-08-02 14:56:05 -04:00
Jeremiah Lowin
34bdd480c9
Improve HTTP server startup performance (#4729) 2026-08-02 14:11:26 -04:00
Shuying
c428a08fea
fix: preserve valid servers during CLI discovery (#4714)
Co-authored-by: Shuying <zsy@u.northwestern.edu>
2026-08-02 09:42:26 -04:00
Martin Styk
10b158baf3
Forward enable_cimd to Auth0, AWS Cognito and OCI providers (#4719)
Signed-off-by: Martin Styk <mart.styk@gmail.com>
2026-08-02 09:42:06 -04:00
YAO_001
9034a2eb4b
Fix CodeMode tool error propagation (#4704)
Co-authored-by: nate nowack <thrast36@gmail.com>
2026-08-02 09:41:36 -04:00
nate nowack
40c3e122e8
Write downloaded skill text as UTF-8 (#4715) 2026-08-02 09:33:32 -04:00
Jake Kaplan
bc07264529
Fix self-referential connection error causes (#4720)
🤖 Generated with OpenAI Codex
2026-07-30 16:23:33 -04:00
Jeremiah Lowin
bcef61d806
Route published docs through pull requests (#4713) 2026-07-30 10:18:03 -04:00
Nicholas Brown
0f18a258d4
add language dropdown (#4716) 2026-07-30 09:32:58 -04:00
nate nowack
44c0907dda
Give parallel Windows CI more timeout headroom (#4680)
pytest-timeout falls back to its thread method on Windows, which os._exit()s
the process instead of failing the test. A single slow test therefore kills an
xdist worker and fails whichever unrelated test it was running.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 10:22:17 -05:00
Jeremiah Lowin
07f6eafd99
Add comprehensive Codex code review rules (#4710) 2026-07-29 09:51:36 -04:00
Jeremiah Lowin
a22f778dbf
Rewrite the FastMCP docs welcome page (#4709) 2026-07-29 09:51:22 -04:00
Jeremiah Lowin
0792ac812c
Improve the v4 docs (#4707) 2026-07-29 09:51:13 -04:00
Jeremiah Lowin
7339936980
Rewrite the v4 What's New page and document server extensions (#4698)
* Rewrite the v4 What's New page

Teach the headline features with code instead of asserting them, drop the
major-version throat-clearing and SEP list, and correct the elicitation
claim: ctx.elicit() is unchanged and handshake-only, while sampling and
roots are removed outright.

* Fix broken doc links and stale version references

Repoint five dead links and anchors, refresh v3-era version examples on the
v4 docs, and add the missing FastMCP 3 entry to the installation page's
upgrade section.

* Document server extensions

add_extension() shipped in v4 with no documentation page. Covers the
extension interface, request methods, tool-call interception, lifespan
ownership, and the client half.

* Link the FastMCP TypeScript library

* Address Codex review feedback

Gate the extension interceptor on the client's per-request opt-in rather
than claiming negotiation does it; show the v4 beta pin on the install
page instead of a version a reader cannot get; note that UserSession
requires authentication.
2026-07-28 20:05:45 -04:00
Jeremiah Lowin
7a77805159
Use mcp.types directly instead of aliasing to mcp_types in docs (#4696) 2026-07-28 19:08:44 -04:00
Jeremiah Lowin
baced6281c
Always emit a tool title, derived from name when unset (#4694)
* Always emit a tool title, derived from name when unset

Some MCP clients (e.g. ChatGPT) drop tools with no `title` instead of
falling back to `name` for display as the spec allows. Deriving a
default title in Tool.to_mcp_tool() fixes this for every tool built on
top of it, including the search-transform, code-mode, and session
proxy tools that never set one explicitly.

Fixes #4414

* Derive fallback title from the overridden name, document it

Addresses Codex review on #4694.

* Resolve title precedence from effective overrides

* Normalize mapping annotations before deriving the title
2026-07-28 17:30:20 -04:00
Jeremiah Lowin
5a98ceb5ca
Add v4.0.0b1 changelog and updates entries (#4681)
* Add v4.0.0b1 changelog and updates entries

* Drop meta note from b1 intro; add #4682 under enhancements

* File #4682 under fixes

* Correct the camelCase rename claim: Python model fields, not the wire

* Baseline the b1 changelog on v3.4.5

* Simplify the beta banner
2026-07-28 17:15:48 -04:00
Jeremiah Lowin
8b76710e66
Move to the stable MCP Python SDK 2.0.0 (#4655) 2026-07-28 16:31:49 -04:00
Jeremiah Lowin
81b1e818e5
Apply app visibility where no host can (#4692) 2026-07-28 16:12:38 -04:00
Jeremiah Lowin
a8b5da9770
Late-bind app tool names so UIs survive composition (#4682) 2026-07-28 10:54:19 -04:00
Jeremiah Lowin
0175bc9235
Split the SDK upgrade guides by SDK version (#4684) 2026-07-28 10:47:39 -04:00
Jeremiah Lowin
d6b9daecb1
Read CLI-scanned MCP config files as UTF-8 explicitly (#4690) 2026-07-28 08:12:17 -04:00
Jeremiah Lowin
78c61415b5
Resolve review threads on fix, reply on decline (#4685)
* Resolve review threads on fix, reply on decline

* Use a placeholder PR number in the resolve example
2026-07-27 21:41:02 -04:00
Jeremiah Lowin
90ea26f337
Soften the review-comment reply guidance (#4683) 2026-07-27 21:06:12 -04:00
Jeremiah Lowin
f4ae8bb0af
Let maintenance releases publish without fastmcp-tasks (#4676) 2026-07-27 16:07:54 -04:00
Jeremiah Lowin
d382943012
Note that review comment threads should get an acknowledgement (#4678) 2026-07-27 16:07:39 -04:00
Jeremiah Lowin
7674645761
Flatten OpenAPI discriminator subtypes into request bodies (#4677)
* Flatten OpenAPI discriminator subtypes into request bodies

* Resolve schema-name discriminator mappings and union conflicting variant fields

* Advertise discriminator values for propertyless variants and document the behavior
2026-07-27 16:06:54 -04:00
Bill Easton
75b9f92504
feat: Add telemetry interop mode for FastMCP (#4046)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-27 16:05:29 -04:00
苏紫辰
c3cbe8b9a3
Fix OpenAPI allOf reference fields (#4653)
* Fix OpenAPI allOf reference fields

🤖 Generated with Codex

* Add allOf reference crash regression

🤖 Generated with Codex

* Handle OpenAPI component refs in allOf

🤖 Generated with Codex

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-27 15:25:22 -04:00
Jeremiah Lowin
ffea4d6a3e
Docs: add v3.4.5 changelog entries to main (#4674)
* Docs: add v3.4.5 changelog entries

* Condense the 3.4.5 entries to patch-release length
2026-07-27 15:13:03 -04:00
Eduardo Cruz Guedes
794bfe9567
Add valid_scopes parameter to OIDC proxy valid scopes (#4660)
* Accept valid_scopes on OIDCProxy

* Keep valid_scopes when verify_id_token restores scopes

* Document valid_scopes on the OIDC proxy

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-27 15:12:15 -04:00
marvin-context-protocol[bot]
11ee46bf3b
chore: Update SDK documentation (#4647)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-07-27 15:03:41 -04:00
Jeremiah Lowin
cc02df94c5
Audit v4 docs: fix missing version badges, fill whats-new gaps (#4668)
* Fix missing/wrong 4.0.0 version badges

* Fill v4 badge and whats-new gaps found in docs audit
2026-07-27 15:03:18 -04:00
Jeremiah Lowin
ea7fb8cb2e
Remove 3.x-era compatibility shims (#4661)
* 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
2026-07-27 14:59:43 -04:00
Jeremiah Lowin
1550eea886
Copy schemas iteratively so deep nesting still compresses (#4671)
* Copy schemas iteratively so deep nesting still compresses

* Keep $defs when the reference scan hits its depth limit
2026-07-27 14:58:13 -04:00
Jeremiah Lowin
e4ccf06baf
Fix FAQ: sampling/roots/elicitation legacy-mode advice, SessionProvider registration (#4672)
* Fix FAQ: narrow the legacy-mode recommendation, note SessionProvider registration

* Correct sampling's modern-protocol claim: guard pattern works, just isn't the recommended path
2026-07-27 14:57:35 -04:00
Jeremiah Lowin
886c85e5f5
Cover require_roles in the v4 highlights (#4666)
* Cover require_roles in whats-new

* Decouple require_roles from the SEP-990 example; require a claim-validating provider
2026-07-27 14:57:23 -04:00
Jeremiah Lowin
95edc1d2f3
Record effective scopes on the OAuth transaction (#4670) 2026-07-27 14:33:57 -04:00
Jeremiah Lowin
4e136e60d6
Document the issuer_url identity change for upgraders (#4658)
* Document the issuer_url identity change for upgraders

Adds an upgrade note covering the one-time re-authorization, fixes the MultiAuth examples that pointed issuer_url at the upstream IdP, and corrects the OCI docstring.

* Address review: valid docstring example, narrower reauth scope

* Scope the reauth checklist item to token-minting providers
2026-07-27 12:45:24 -04:00
Jeremiah Lowin
27a5921bff
Expand the FAQ for the v4 transition (#4649) 2026-07-27 12:41:32 -04:00
Jeremiah Lowin
920cb47778
Add require_roles auth check (#4656)
* 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
2026-07-27 12:36:58 -04:00
Jeremiah Lowin
0c1c42f151
Bind CIMD assertion audience to the advertised token endpoint (#4659)
A bare-authority base_url renders with a trailing slash, so the expected aud was https://host//token while the metadata advertised https://host/token.
2026-07-27 11:46:03 -04:00
Jeremiah Lowin
6c4ba6b420
Use the snake_case field name in the task return-type test (#4657) 2026-07-27 10:56:03 -04:00
Jeremiah Lowin
a42faab783
Use issuer_url for OAuth issuer identity (#4652)
* 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
2026-07-27 10:43:12 -04:00
Jeremiah Lowin
b1e0586d4e
Remove server-initiated sampling and roots from the server API (#4648)
* Remove server-initiated sampling and roots from the server API

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.

* Update tests for the removed sampling and roots server API

* Era-gate client.set_logging_level on modern connections

* Document that server-initiated sampling and roots are not in FastMCP 4

* Silence ty deprecation diagnostics and drop stale sampling doc mentions

* Baseline tools-call-sampling; fix removal leftovers flagged by ruff

* Document sampling handlers on both protocol routes; qualify log-level override

* Docs: sampling and roots work on modern via the guard pattern

The imperative ctx.sample()/ctx.list_roots() stay removed, but both
capabilities survive as input-required requests, as tests/conformance
exercises on 2026-07-28. Direct LLM calls remain the recommendation for
generation; roots has no round-trip-budget objection.

* Change register: record the guard route for sampling and roots

* Editorial pass on the sampling and roots docs

* Flag the sampling removal at the top of the page

* Restore the version badge and point sampling users at 3.x

* Keep the sampling conformance scenario live; fix roots example URIs

* Upgrade guide: staying on 3.x is an option for sampling servers

* Elicitation: state the era split once, not twice
2026-07-27 10:32:31 -04:00
Jeremiah Lowin
9ea5a40728
Drop stale label-noting instructions from CLAUDE.md and review-pr skill (#4654)
Labels are bot-assigned from title/body/code; noting a "suggested" label
in the PR body was a leftover from an unrelated PR (#4392) and doesn't
match how this repo actually labels things.
2026-07-27 10:31:58 -04:00
Jeremiah Lowin
b2b2b0f918
Elicitation: state the era split once, not twice 2026-07-27 09:43:52 -04:00
Jeremiah Lowin
a2bec08e76
Fix double slash in issuer_url well-known log hint 2026-07-27 09:43:29 -04:00
Jeremiah Lowin
62afdca775
Bind ID-JAG audience to the issuer identifier 2026-07-27 09:37:38 -04:00
Jeremiah Lowin
c4dcf833ca
Upgrade guide: staying on 3.x is an option for sampling servers 2026-07-27 09:37:15 -04:00
Jeremiah Lowin
0172e4c4d4
Keep the sampling conformance scenario live; fix roots example URIs 2026-07-27 09:35:30 -04:00
Jeremiah Lowin
aaaca09a91
Make AuthorizationHandler keyword-only 2026-07-26 21:11:55 -04:00
Jeremiah Lowin
0e9cab86fd
Restore the version badge and point sampling users at 3.x 2026-07-26 21:10:41 -04:00
Jeremiah Lowin
4a616d6e39
Flag the sampling removal at the top of the page 2026-07-26 20:42:12 -04:00
Jeremiah Lowin
7fe3c1e8bd Editorial pass on the sampling and roots docs 2026-07-26 20:23:23 -04:00
Jeremiah Lowin
d5ff831602
Change register: record the guard route for sampling and roots 2026-07-26 19:33:40 -04:00
Jeremiah Lowin
cf7edc895c
Docs: sampling and roots work on modern via the guard pattern
The imperative ctx.sample()/ctx.list_roots() stay removed, but both
capabilities survive as input-required requests, as tests/conformance
exercises on 2026-07-28. Direct LLM calls remain the recommendation for
generation; roots has no round-trip-budget objection.
2026-07-26 19:22:37 -04:00
Jeremiah Lowin
46c3b74346
Remove stray scratch script 2026-07-26 17:48:11 -04:00
Jeremiah Lowin
98ac0402df
Keep ID-JAG audience on base_url, out of scope for issuer identity 2026-07-26 17:48:01 -04:00
Jeremiah Lowin
dec25ba6be
Merge remote-tracking branch 'origin/main' into r4648
# Conflicts:
#	tests/conformance/expected-failures.yml
2026-07-26 17:40:14 -04:00
Jeremiah Lowin
fecced2b5c
Merge pull request #4650 from PrefectHQ/claude/mcp-conformance-tests-7ec13f
Pass the MCP conformance suite's draft and pending scenarios
2026-07-26 17:38:13 -04:00
Jeremiah Lowin
5745323ecd
Cover trace propagation through the modern proxy relay 2026-07-26 17:15:30 -04:00
Jeremiah Lowin
46cd0c7933
Format the split guard-components test module 2026-07-26 17:03:04 -04:00
Jeremiah Lowin
f7eed91aa8
Forward guard asks through prompt, resource, and template proxies
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.
2026-07-26 16:58:58 -04:00
Jeremiah Lowin
8768921fd9
Split guard caching tests into their own module
Keeps test_caching.py under its size limit by topic rather than by
compressing the new tests.
2026-07-26 16:46:21 -04:00
Jeremiah Lowin
2d3ad9ca0d
Make a contended tasks/update wait instead of dropping its answer
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.
2026-07-26 16:40:33 -04:00
Jeremiah Lowin
4ef7d16419
Keep multi-round-trip asks out of the response cache
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.
2026-07-26 16:06:17 -04:00
Jeremiah Lowin
7ca58583fd
Document the guard pattern for prompts and resources 2026-07-26 15:49:45 -04:00
Jeremiah Lowin
96e12569b1
Address review: durable re-entry, resource guards, deterministic test
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.
2026-07-26 15:36:40 -04:00
Jeremiah Lowin
a7248480e2
Make InMemoryOAuthProvider keyword-only like its parent 2026-07-26 15:34:06 -04:00
Jeremiah Lowin
18aa6a09d6
Document sampling handlers on both protocol routes; qualify log-level override 2026-07-26 15:19:11 -04:00
Jeremiah Lowin
66d842d6e9
Align ID-JAG audience docstring with issuer_url 2026-07-26 15:13:58 -04:00
Jeremiah Lowin
681d5a7120
Apply ruff format to issuer identity tests 2026-07-26 15:12:50 -04:00
Jeremiah Lowin
c4c72ac240
Use issuer_url for OAuth issuer identity, not base_url 2026-07-26 15:10:15 -04:00
Jeremiah Lowin
b9fcef1889
Baseline tools-call-sampling; fix removal leftovers flagged by ruff 2026-07-26 15:07:24 -04:00
Jeremiah Lowin
7699deb99c
Pass the MCP conformance suite's draft and pending scenarios
Pin the suite, build out the fixture, and fix the protocol gaps it found.
2026-07-26 15:00:08 -04:00
Jeremiah Lowin
1593257f2a
Merge remote-tracking branch 'origin/main' into modern-merge
# Conflicts:
#	tests/client/test_roots.py
2026-07-26 14:53:31 -04:00
Jeremiah Lowin
90f2e190d0
Silence ty deprecation diagnostics and drop stale sampling doc mentions 2026-07-26 14:45:55 -04:00
Shuying
2a93404e8c
fix: accept callable roots handlers (#4639)
* Accept callable roots handlers

🤖 Generated with OpenAI Codex

* fix: cover callable object roots handlers

🤖 Generated with OpenAI Codex

---------

Co-authored-by: Shuying <zsy@u.northwestern.edu>
2026-07-26 14:43:41 -04:00
Jeremiah Lowin
1a43a3b8e9
Document that server-initiated sampling and roots are not in FastMCP 4 2026-07-26 14:43:18 -04:00
Jeremiah Lowin
fca339084b
Era-gate client.set_logging_level on modern connections 2026-07-26 14:39:31 -04:00
Jeremiah Lowin
704b74b3ab
Update tests for the removed sampling and roots server API 2026-07-26 14:38:26 -04:00
Jeremiah Lowin
e056a3946e
Remove server-initiated sampling and roots from the server API
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.
2026-07-26 14:34:21 -04:00
Jeremiah Lowin
b07f9ce9ae
Honor OAuth application_type in DCR (SEP-837) (#4621)
* 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.
2026-07-26 14:24:50 -04:00
Aman Gupta
e4a87f2afe
fix : canonical mime type mapping from formats to remove inconsistency #4627 (#4628)
* fix : canonical mime type mapping from formats to remove inconsistency

* Apply ruff format to _get_mime_type

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-26 14:15:08 -04:00
Jeremiah Lowin
4ebb3fd5e6
Emit scope step-up challenges for incremental authorization (SEP-2350) (#4623)
* 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
2026-07-26 14:13:26 -04:00
dependabot[bot]
c4cb1910a3
chore(deps): bump mcp from 1.27.2 to 1.28.1 in /examples/testing_demo in the uv group across 1 directory (#4614)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-26 14:10:21 -04:00
dependabot[bot]
bc770c6fc0
Bump actions/setup-python from 6 to 7 (#4641)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-26 14:10:03 -04:00
dependabot[bot]
ac83711ff8
Bump actions/upload-artifact from 4 to 7 (#4640)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-26 14:09:51 -04:00
Jeremiah Lowin
4b09a040be
Unpublish v4 development notes; prep docs for beta 1 (#4644)
* Unpublish v4 development notes; prep docs for beta 1

* Nest development notes under dev-docs/

* Rewrite site-root links in dev notes as absolute URLs for GitHub rendering
2026-07-26 14:08:34 -04:00
Jeremiah Lowin
387a063aec
Pin Claude Code to 2.1.215 in the sandboxed Marvin workflows (#4645)
2.1.216 regressed the bubblewrap sandbox the action wraps Bash in when
allowed_non_write_users is set, so every command failed and triage applied
zero labels while reporting success. Also fail the triage job on sandbox
errors, which the existing denial guard could not see.
2026-07-26 13:57:14 -04:00
andrew-stelmach-fleet
caadfe6413
docs: fix ProxyProvider docstring example using nonexistent with_namespace() (#4633)
The example called proxy.with_namespace("remote"), which is not defined
anywhere in the codebase and raises AttributeError. Namespacing a provider
is done via the add_provider() keyword argument.
2026-07-26 13:50:13 -04:00
Jeremiah Lowin
2f992f71ea
Support routable transport headers for gateways (SEP-2243) (#4622) 2026-07-26 13:40:09 -04:00
marvin-context-protocol[bot]
37fb0ad803
chore: Update SDK documentation (#4615) 2026-07-23 20:52:17 -04:00
Jeremiah Lowin
cb5f6abdd0
Pin burner-redis below the Windows-crashing 0.1.7 release (#4618)
* Pin burner-redis below the Windows-crashing 0.1.7 release

burner-redis 0.1.7 crashes the interpreter (native fault, no Python traceback)
running the memory:// task backend under pytest-xdist on Windows — reproduced on
GitHub Actions windows-latest via the 'Upgrade checks' workflow, confirmed
absent on macOS/Linux with the identical dependency versions.

pydocket only floors burner-redis at >=0.1.6, so capping pydocket's own version
is not enough: a resolver remains free to pick the newest burner-redis
satisfying that floor. fastmcp-tasks previously pinned pydocket>=0.20.0 with no
upper bound, so a fresh 'pip install fastmcp[tasks]' today can resolve straight
into the broken combination for a real Windows user on the default backend.

Pin burner-redis<0.1.7 directly, which in turn caps pydocket to <0.20.2 (the
last release that doesn't itself require burner-redis>=0.1.7). Verified the pin
holds under both locked and --upgrade (highest) resolution.

* Scope the burner-redis pin to Windows only

burner-redis 0.1.7 is confirmed fine on macOS/Linux (full suite green there with
the identical upgraded dependencies) - only Windows crashes. The previous
unconditional pin blocked every platform from newer pydocket/burner-redis
releases unnecessarily. Add sys_platform == 'win32' to the burner-redis
constraint so only Windows installs are capped.

Verified via uv pip compile --python-platform: macOS/Linux resolve to
burner-redis 0.1.7 / pydocket 0.23.0 (unblocked); Windows resolves to
burner-redis 0.1.6 / pydocket 0.20.1 (still capped).
2026-07-23 20:51:43 -04:00
Jeremiah Lowin
cf021d1a70
whats-new: add the argument completion capability (#4620)
Server-side completions (@mcp.completion) shipped in #4582 but the
What's New page didn't mention it. Adds it to the authoring-capabilities
cluster with a runnable example and a link to the servers/completions page.
2026-07-23 20:44:16 -04:00
Jeremiah Lowin
f896f5acb5
v4 docs quality pass: stale task/era claims, broken links, polish (#4619)
* v4 docs quality pass: fix stale task/era claims, broken links, writing polish

* whats-new: add the client-side protocol negotiation story

The page told the server half of the era story (serves every era) but
never the client half — that a default Client(url) now negotiates the
modern era, where earlier versions pinned the handshake. Completes the
mental model and links to the client negotiation docs.

* Address review: drop 'complete' over-claim; link mounted-state to Session State
2026-07-23 20:42:13 -04:00
Jeremiah Lowin
078c44d835
Fix stale Mac/Windows-vs-Linux OAuth key/storage docs (#4617)
* Fix stale Mac/Windows-vs-Linux OAuth key/storage docs

#2223 replaced platform-aware keyring/MemoryStore defaults with
deterministic key derivation and an always-on-disk encrypted store,
but the docs update in that PR missed several spots.

* Fix OIDCProxy doc referring to internal upstream_client_secret name

Codex review: the public OIDCProxy constructor takes client_secret;
upstream_client_secret is only OAuthProxy's internal parameter name.
2026-07-23 20:30:05 -04:00
Jeremiah Lowin
39148870af
Merge pull request #4603 from PrefectHQ/feat/tasks-sep2663
Add background tasks via the io.modelcontextprotocol/tasks extension (SEP-2663)
2026-07-23 20:16:11 -04:00
Jeremiah Lowin
601903436b
Pass task-creation results through ToolResult-only middleware
A task-augmented tools/call returns a CreateTaskResult up through the middleware
chain. Response caching and response limiting assumed a ToolResult and accessed
.content/.wrap(), crashing after the task was already enqueued (a client retry
could duplicate side effects). Both now pass any non-ToolResult through
untouched, alongside the existing InputRequiredToolResult bypass.
2026-07-23 20:09:47 -04:00
Jeremiah Lowin
79ba8f180d
Merge remote-tracking branch 'origin/main' into claude/mcp-background-tasks-v2-0f883f
# Conflicts:
#	uv.lock
2026-07-23 20:07:13 -04:00
Jeremiah Lowin
76c6f1a64e
Session-in-task Context API, task telemetry, settings dotenv, guard fail-loud
Five review fixes. ctx.session_id / get_state / set_state now work in a Docket
worker by falling back to the snapshotted session id. Task management wire calls
(submission, tasks/get/update/cancel) create client spans and propagate trace
context. TasksClientSettings loads .env like DocketSettings, and the docs use
its real env var name. A state-only guard round (request_state, no input
requests) fails with a clear error instead of silently completing wrong.
2026-07-23 20:05:54 -04:00
nate nowack
856844cae7
Document targeted v4 prerelease installation (#4598)
🤖 Generated with Codex

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-23 20:00:28 -04:00
nate nowack
cc3d1c18a3
Allow FastMCP prereleases through the repository cooldown (#4596)
* Allow FastMCP prerelease resolution

🤖 Generated with Codex

* Include fastmcp-remote in prerelease resolution

🤖 Generated with Codex

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-23 20:00:23 -04:00
nate nowack
d8ac6cbfde
Fix OAuth proxy override typing (#4612) 2026-07-23 19:52:42 -04:00
Jeremiah Lowin
c556f07a66
Archive v3 docs and publish v4 as the primary version (#4613)
* Archive v3 docs under /v3 and publish v4 as the primary version

* Label primary docs version v4.0.0 (alpha 1)

* Add What's New in v4 page; fix upgrade-guide phrasing; point banner at What's New

* Rewrite What's New around v4's new capabilities, not the sampling deprecation

* Lead What's New with the SDK v2 engine swap and the SEPs it brings

* State ships now (link Session State); tasks arrive next alpha

* Exclude docs/v3 frozen snapshots from doc-example import validation
2026-07-23 19:47:57 -04:00
Jeremiah Lowin
1c57079b9b
Verify UserSession state works inside background tasks
Lock in the tasks x stateless-session-state (#4604) integration: a
session: UserSession parameter resolves in a Docket worker via the task-aware
get_server() and the principal restored from the task snapshot, sharing state
across a principal's tasked calls and staying isolated between principals.
2026-07-23 19:38:19 -04:00
Jeremiah Lowin
3f746b91fc
Merge remote-tracking branch 'origin/main' into claude/mcp-background-tasks-v2-0f883f
# Conflicts:
#	fastmcp_slim/fastmcp/server/dependencies.py
2026-07-23 19:32:52 -04:00
Jeremiah Lowin
cb4419ed02
Fix broken Docket links in task docs
The example README pointed at github.com/PrefectHQ/docket (404); the canonical
repo is chrisguidry/docket. Point the docs' Docket-docs link at the canonical
docket.lol.
2026-07-23 19:24:45 -04:00
Jeremiah Lowin
8363ec4d26
Add stateless session state (UserSession / SessionId) (#4604)
* 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
2026-07-23 19:21:00 -04:00
Jeremiah Lowin
53741dc9c7
Keep task routing keys alive via sliding TTL; version-aware worker server resolution
A resumed leg that runs longer than its pointer's wall-clock TTL stranded
_lookup_task on the base leg (false completion / not found). Each poll now
refreshes the routing keys' TTL (sliding expiration), so an actively-polled
task keeps them alive regardless of execution duration, and the resumed-leg
write uses the same buffered TTL as creation. Separately, remote-worker server
resolution now respects the requested tool version, so two versions of the same
mounted tool name resolve to their own child server.
2026-07-23 19:18:51 -04:00
Jeremiah Lowin
95f766cb74
Normalize asyncio.TimeoutError to builtin in task input timeout (py3.10)
asyncio.wait_for raises asyncio.TimeoutError, a distinct type from the builtin
before Python 3.11, so an elicitation-callback timeout leaked an uncaught type
on 3.10. Convert it to the builtin TimeoutError the rest of the drive raises.
2026-07-23 18:35:57 -04:00
Jeremiah Lowin
f81d6c07d8
Load task settings from .env; gate root publish on fastmcp-tasks; fix worker command
DocketSettings now loads the same dotenv source as core settings, so a
FASTMCP_DOCKET_* value in .env configures the backend instead of silently
using memory://. The root fastmcp publish waits for the matching fastmcp-tasks
to appear on PyPI before uploading, so the [tasks] extra is never installable
but unresolvable. And the example README uses the real worker entry point
(python -m fastmcp_tasks.worker_cli worker).
2026-07-23 18:30:52 -04:00
Jeremiah Lowin
c3ad5e9ecb
Clear stale auth in reused workers; bound elicitation; version explicit tasks
Three review fixes. A Docket worker may reuse an asyncio context across tasks,
so snapshot restore now always resets auth and headers to the current task's
state — an anonymous task following an authenticated one no longer inherits the
prior caller's identity. A stalled in-task elicitation handler is now bounded by
the call's remaining timeout, like polling and sleeps. And call_tool_task takes
a version= to task a specific component version rather than the highest.
2026-07-23 18:25:48 -04:00
Jeremiah Lowin
a194acdc5f
Resolve mounted server and headers correctly in remote task workers
Two remote-worker fixes. A separate worker process cannot reach the submitting
process's server map, so a mounted task's ctx.fastmcp/CurrentFastMCP() fell back
to the root; the worker now re-resolves the owning child from the root using the
snapshotted tool name. And restoring headers no longer fabricates a live Request
— get_http_headers() reads a dedicated task-headers context var while
get_http_request()/CurrentRequest() correctly keep raising inside a task.
2026-07-23 18:19:35 -04:00
Jeremiah Lowin
fbee629ed9
Advertise only supported token endpoint auth methods in OAuthProxy metadata (#4608)
* Advertise only supported token endpoint auth methods in OAuthProxy metadata

* Normalize DCR registration response to the stored public client
2026-07-23 18:11:18 -04:00
Jeremiah Lowin
9019a7af70
Add PyPI publish workflow for fastmcp-tasks
The fastmcp[tasks] extra pins fastmcp-tasks=={version}, but no workflow
published it — pip install "fastmcp[tasks]" would fail to resolve. Mirror the
fastmcp-remote workflow: build on release, wait for the matching fastmcp-slim to
appear on PyPI, then publish.
2026-07-23 16:54:31 -04:00
Jeremiah Lowin
f627170088
Bound task drive by one deadline; version-aware tasking; serialize cancel
Three review fixes: transparent call_tool(timeout=N) now enforces one deadline
across the whole poll loop (not per-request), matching the sync timeout; the
tools/call interceptor resolves the client-requested component version instead
of the highest; tasks/cancel runs under the per-task update lock and re-resolves
the live leg, so it can't cancel a stale leg while an update enqueues the next.
2026-07-23 16:53:08 -04:00
Jeremiah Lowin
733801ed6c
Rework tasks example into a runnable HTTP client/server pair
Server runs over HTTP on the default memory:// backend (no Redis needed); the
client drives it transparently, via an explicit handle, and with a parallel
command that fires several tasks at once to show them overlap. A 1s poll
interval keeps the demo snappy.
2026-07-23 16:44:53 -04:00
Liang Hu
99327084d2
Preserve raw CallToolResult returns (#4587)
Co-authored-by: nate nowack <thrast36@gmail.com>
2026-07-23 15:10:42 -05:00
nate nowack
06aa84943c
Expose era-neutral client server metadata (#4599)
* Expose era-neutral client metadata

🤖 Generated with Codex

* Clarify pinned modern client metadata
2026-07-23 15:06:26 -05:00
Jeremiah Lowin
1d442ffa36
Make tasks/cancel actually cancel input_required tasks
A guard task parked on input has an already-COMPLETED Docket execution, so
docket.cancel on it was a no-op: tasks/get reported input_required forever and
tasks/update could still resume it. Record a durable logical-cancellation
marker that tasks/get reports as cancelled and tasks/update refuses to resume,
and clear the parked leg's outstanding requests on cancel.
2026-07-23 08:15:21 -04:00
Jeremiah Lowin
110943fc61
Skip expired snapshot tokens; bound task wait polls by deadline
A queued task can outlive its submitter's token expiry: install the snapshot
token only if still valid, matching the SDK bearer check, so a delayed task
never runs under credentials a live request would reject. ToolTask.wait now
bounds each tasks/get by the remaining deadline so a stalled poll cannot block
past the caller's timeout.
2026-07-23 08:12:42 -04:00
Jeremiah Lowin
1c7ade215b
Merge remote-tracking branch 'origin/main' into claude/mcp-background-tasks-v2-0f883f 2026-07-23 08:00:29 -04:00
Jeremiah Lowin
edb54bddf3
Merge pull request #4602 from PrefectHQ/feat/server-extension-api
Add FastMCP-native server extension API (SEP-2133)
2026-07-23 07:59:59 -04:00
Jeremiah Lowin
b75dde3b5c
Mask raised task errors regardless of ctx param
Resolve the error-masking policy via the worker-server resolver instead of
the active Context: a task tool that raises without requesting a ctx param
has no active context, so the old lookup leaked unmasked error text past
mask_error_details=True. Also route custom Tool subclasses through the same
error-conversion wrapper as FunctionTool.
2026-07-23 07:59:54 -04:00
Jeremiah Lowin
19c5c507cc
Address review feedback on SEP-2663 tasks
- Client task support is opt-in via importing fastmcp_tasks (drop the core
  auto-load of companion packages); a plain Client never advertises tasks.
- A worker restores the submitting caller's auth token and headers from the
  task snapshot into the standard ambient context, so get_access_token() /
  get_http_headers() work in a distributed worker with no new core hooks.
- worker_cli validates the loaded extension's resolved backend, not env defaults,
  so a constructor-configured Redis worker starts.
- Thread the per-call read timeout through task polling; bound ToolTask.wait by
  its deadline; set_elicitation_callback rebuilds internal extensions so a
  later-set handler answers in-task input.
- README imports TaskConfig from fastmcp.utilities.tasks.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 19:14:16 -04:00
Jeremiah Lowin
e5ca0269cb
docs: rewrite background tasks pages for SEP-2663
Server (servers/tasks.mdx) and client (clients/tasks.mdx) docs rewritten for
the extension model: add_extension(TasksExtension()), the guard pattern for
in-task input (no imperative ctx.elicit()), tools-only, and the modern-protocol
requirement (the inverse of the old SEP-1686 legacy-only note). Mechanical
fixes elsewhere for the same reason: telemetry.mdx's tasks/{operation} method
list (get/update/cancel, not result/list), client.mdx's legacy-only feature
list (tasks moved to modern-only) and extension-composition paragraph
(describes the tasks ClientExtension, not the removed notification binding),
and stale SEP-1686 references in the FastMCP 2 upgrade guide. v4-notes status
lines updated to Shipped (#4602, #4603).
2026-07-22 17:24:56 -04:00
Jeremiah Lowin
bb3ef39a89
Close SEP-2663 compliance gaps: -32003 on task methods, raised-error semantics, update race
- tasks/get|update|cancel now return -32003 when the client did not declare the
  tasks extension for the request (SEP-2663 MUST).
- A task tool that raises is a completed task with an is_error result, not a
  failed task; failed is reserved for protocol faults, matching a live tools/call.
- A per-task lock serializes concurrent tasks/update so two racing answers cannot
  each enqueue a next leg (double execution).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 16:59:13 -04:00
Jeremiah Lowin
74e01d5e08
Add SEP-2663 client half: transparent call_tool, ResultClaim, Task handle
A FastMCP client now transparently completes tasked tools/call: the tasks
ClientExtension advertises the capability and claims the CreateTaskResult, and
the resolver drives the tasks/get poll loop to completion, answering in-task
input through the client's elicitation handler and returning the tool's real
result. call_tool is transparent, call_tool_mcp exposes the raw result, and
call_tool_task yields a Task handle. The client half moves to fastmcp-tasks;
the [tasks] client extension auto-wires into Client (ProxyClient opts out).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 15:21:10 -04:00
nate nowack
8efa405833
Bridge camelCase ToolAnnotations reads (#4597)
🤖 Generated with Codex
2026-07-22 13:55:41 -05:00
Jeremiah Lowin
d41ff5bcd8
Rebuild guard tasks as end-and-reenter; remove imperative in-task elicit
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>
2026-07-22 12:38:46 -04:00
Jeremiah Lowin
ef29b731ea
Add server-side claim production for tasks; emit resultType discriminator
Widen the tools/call result serialization (via a refcounted, modern-gated wrap
installed by TasksExtension) so a CreateTaskResult reaches the client instead of
being stripped by the CallToolResult|InputRequiredResult surface — the SDK ships
claim consumption but no production. Emit the resultType discriminator the
protocol requires (task on CreateTaskResult, complete on the tasks/* results);
the draft schema forbids it (additionalProperties:false), a contradiction
reported upstream. Closes compliance gaps G1/G4/G5.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-22 08:03:32 -04:00
Jeremiah Lowin
b0d3e653b9
Fix flaky stdio crash-recovery tests: assert eventual recovery (#4594) 2026-07-22 08:01:37 -04:00
Jeremiah Lowin
bc22e517fd
Fix ty diagnostics in task tests, scope ty exclusion to client-task files 2026-07-21 23:10:00 -04:00
Jeremiah Lowin
5fa2883670
Implement SEP-2663 tasks extension: TasksExtension, poll-based task lifecycle
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>
2026-07-21 23:00:38 -04:00
Jeremiah Lowin
6fce4e538f
Move task subsystem to fastmcp-tasks package, disconnect SEP-1686 wire from core
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>
2026-07-21 21:51:45 -04:00
Jeremiah Lowin
d756b99bf6
Sort imports in extensions.py 2026-07-21 20:44:54 -04:00
Jeremiah Lowin
242850c0f3
Scaffold fastmcp-tasks workspace package
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 20:44:54 -04:00
Jeremiah Lowin
094738f68a
Add server extension API: add_extension with capability, methods, tool-call interception, and lifespan
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-21 20:36:38 -04:00
Jeremiah Lowin
4402b48954
test: vendor ext-tasks draft schema as fixture 2026-07-21 20:19:28 -04:00
Jeremiah Lowin
44d6d739ea
docs: v4 background-tasks design — rebuild on SEP-2663 as fastmcp-tasks 2026-07-21 20:18:58 -04:00
Jeremiah Lowin
36caaa6f56
Document v3->v4 removals and add upgrade-reality tests (#4585)
* Document v3->v4 removals and add upgrade-reality tests

* Check canonical imports in a clean subprocess to avoid suite import pollution

* Address review: import_server semantics note, pin traversal error, drop redundant import

* Address review round 2: real screening test, Depends factory, remove_tool/create_proxy notes

* Validate canonical imports in-process; fix lifespan/timeout/error-code/starlette doc notes

* Reconcile with fastmcp.types trim: import protocol types from mcp_types

* Record v4 release codename arc in dev notes
2026-07-21 17:12:59 -04:00
Jeremiah Lowin
30044c7864
Fix percent-encoded skill file names unreadable in resources mode (#4590)
* 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().
2026-07-21 15:34:09 -04:00
marvin-context-protocol[bot]
d0f1468fce
chore: Update SDK documentation (#4589)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-07-21 13:10:24 -05:00
VectorPeak
74b8f1bc1c
Fix File helper extension handling (#4531)
Preserve explicit suffixes when building data-backed File resource URIs, while keeping the inferred-extension fallback for names without a suffix.

Closes #4530

Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
2026-07-21 13:04:15 -05:00
苏紫辰
16a09f0151
Fix skill frontmatter with UTF-8 BOM (#4533)
🤖 Generated with Codex
2026-07-21 12:48:40 -05:00
Jeremiah Lowin
f038cf3be7
Add machine-to-machine client authentication (#4583)
* Add M2M client credentials auth providers

Wrap the SDK's client_credentials and private_key_jwt OAuth providers as
FastMCP-idiomatic ClientCredentialsOAuthProvider and PrivateKeyJWTOAuthProvider,
enabling browser-free client authentication via Client(auth=...).

* Fix M2M token cache collision and explicit-scope drop

Namespace the token cache by client_id so distinct clients sharing one store don't overwrite each other's tokens; pin caller-supplied scopes so the token request keeps them; fix CodeQL URL-substring check in tests; drop unused logger.

* Preserve step-up scope union, scope-aware token cache, restore token expiry

Only pin the caller's explicit scopes on initial authorization, leaving the SDK's step-up scope union intact; namespace the token cache by requested scopes as well as client_id; restore persisted absolute expiry on init so an expired stored token is re-fetched.

* Skip expiry restore for non-expiring reloaded tokens

* Distinguish expires_in=0 from omitted when restoring expiry

* Scope step-up flag to the flow via ContextVar; runnable JWT signing example
2026-07-21 12:27:08 -04:00
Jeremiah Lowin
7417e974f4
Let a server answer argument-completion requests (#4582)
* 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
2026-07-21 12:26:24 -04:00
Jeremiah Lowin
611a35861d
Trim fastmcp.types to FastMCP-unique types (#4584)
* Trim fastmcp.types to FastMCP-unique types only

fastmcp.types re-exported 29 mcp_types symbols verbatim, which was
pointless indirection users had to discover. It now holds only Textarea,
the one type FastMCP actually defines; everything else imports from
mcp_types directly. These mirrors were added during unreleased SDK v2
migration work and never shipped, so this is not a breaking change.

* Keep historical mcp.types import in v2/v3 migration examples
2026-07-21 09:45:41 -04:00
Jeremiah Lowin
10af989563
Keep historical mcp.types import in v2/v3 migration examples 2026-07-20 21:04:01 -04:00
Jeremiah Lowin
7814d95990
Merge pull request #4581 from PrefectHQ/docs/v4-notes-refresh
Bring the v4 developer notes up to date with what shipped
2026-07-20 20:58:26 -04:00
Jeremiah Lowin
e32a2098f9
Trim fastmcp.types to FastMCP-unique types only
fastmcp.types re-exported 29 mcp_types symbols verbatim, which was
pointless indirection users had to discover. It now holds only Textarea,
the one type FastMCP actually defines; everything else imports from
mcp_types directly. These mirrors were added during unreleased SDK v2
migration work and never shipped, so this is not a breaking change.
2026-07-20 20:53:11 -04:00
Jeremiah Lowin
07fa270652
Address review: drop tasks from modern capability table; note the reentrancy blocker too 2026-07-20 20:28:23 -04:00
Jeremiah Lowin
2db431f5e5
Refresh v4 notes index overview to match shipped work 2026-07-20 20:02:49 -04:00
Jeremiah Lowin
18b898e42a
Refresh v4 developer notes to match shipped work 2026-07-20 20:00:37 -04:00
nate nowack
9d9bf2b717
Restore upgraded dependency checks (#4576)
* Restore upgraded dependency checks

Generated with Codex

* Clarify settings loading and teardown logging

Generated with Codex

* Align ty checks on the upgraded version

Generated with Codex

* Preserve simultaneous caller cancellation

Generated with Codex
2026-07-20 18:46:30 -04:00
marvin-context-protocol[bot]
4ec2757b6d
chore: Update SDK documentation (#4569)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-07-20 18:44:54 -04:00
Jeremiah Lowin
d927762003
Drop forked client protocol helpers in favor of the SDK's (#4574)
* Delegate forked client protocol helpers to the SDK

Replace FastMCP's copies of _fold_extensions, _evicting_message_handler,
and _synthesize_discover with imports from mcp.client.client. The fork had
drifted: it was missing validate_extension_identifier, so non-reverse-DNS
extension identifiers were silently accepted.

Full lifecycle composition over mcp.Client stays blocked upstream —
mcp.Client hardcodes ClientSession construction (no session_class hook)
and forbids reentry.

* Drop duplicate local helpers reintroduced by the merge; use SDK versions
2026-07-20 18:21:56 -04:00
Jeremiah Lowin
2bcfae3412
Merge pull request #4572 from PrefectHQ/feature/client-auto-default
Negotiate the best mutual protocol era by default
2026-07-20 18:16:40 -04:00
Jeremiah Lowin
afbe342587
Document on_initialize and session-state loss under the auto default 2026-07-20 18:04:29 -04:00
Jeremiah Lowin
e01c5932bf
Unpin tests whose defects #4579 fixed
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.
2026-07-20 18:02:21 -04:00
Jeremiah Lowin
5f428aaced
Merge remote-tracking branch 'origin/main' into feature/client-auto-default 2026-07-20 17:53:27 -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
52b37b9a0c
Mark transforming single-server config wrapper legacy-only 2026-07-20 16:34:42 -04:00
Jeremiah Lowin
d048c7e690
Merge branch 'audit/pins-server' into feature/client-auto-default 2026-07-20 16:27:59 -04:00
Jeremiah Lowin
de14ed1b7c
Audit mode="legacy" pins in server tests and top-level stragglers
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).
2026-07-20 16:00:27 -04:00
Jeremiah Lowin
f2ccb6a32c
Merge branch 'audit/pins-proxy' into feature/client-auto-default 2026-07-20 16:00:18 -04:00
Jeremiah Lowin
bcf90d5ae7
Remove unnecessary mode="legacy" pins from proxy tests
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).
2026-07-20 15:50:35 -04:00
Jeremiah Lowin
ae43039d8e
Audit tests/client/ mode="legacy" pins
Remove 4 unjustified pins (proxy header passthrough, connect timeout,
two response_title validation tests that fail before any request is
dispatched). Keep 81 pins that genuinely exercise older-protocol-only
behavior (ctx.elicit back-channel, sampling, roots, ping, session IDs,
initialize handshake, client.set_logging_level).

Flags a real defect: _on_read_resource/_on_get_prompt only catch
(DisabledError, NotFoundError), unlike _on_call_tool which catches
FastMCPError broadly. A ResourceError/PromptError escapes as a raw
exception and the modern protocol's generic exception ladder masks it
as "Internal server error", losing the detailed message tool errors
still get. Left pinned with a TODO in test_client.py and
test_error_handling.py rather than hidden.
2026-07-20 15:43:04 -04:00
Jeremiah Lowin
bf352d8a4a
Make protocol-version notes scannable and drop era jargon from client docs 2026-07-20 15:00:58 -04:00
Jeremiah Lowin
1e1882851d
Keep the CLI URL proxy era-agnostic; monitor claimed-result resolution 2026-07-20 12:46:34 -04:00
Jeremiah Lowin
35c2b52652
Merge remote-tracking branch 'origin/main' into feature/client-auto-default
# Conflicts:
#	fastmcp_slim/fastmcp/server/providers/proxy.py
2026-07-20 12:33:38 -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
1a788cf349
Merge remote-tracking branch 'origin/main' into feature/client-auto-default
# Conflicts:
#	tests/client/test_streamable_http.py
#	tests/server/middleware/test_initialization_middleware.py
#	tests/server/tasks/test_task_status_notifications.py
2026-07-20 12:02:38 -04:00
Jeremiah Lowin
effbc568ff
Document Windows CI parallelism and the subprocess_heavy marker (#4575)
* Document Windows CI parallelism and the subprocess_heavy marker

* Exclude subprocess_heavy from the process-free test command
2026-07-20 11:52:42 -04:00
Rach Granville
dd803a0d7f
docs: quote pip extras install examples (#4568)
* docs: quote pip extras install examples

* docs: quote remaining unquoted pip install extras

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-20 11:09:50 -04:00
Jeremiah Lowin
c8b8911226
Stop gather() from creating coroutines it may never schedule (#4559)
* Fix: gather() eagerly creates coroutines before scheduling them

AggregateProvider fans out Provider.get_tool() (and sibling calls) across
child providers via gather(*[p.get_tool(x) for p in providers]). The list
comprehension builds every coroutine up front, then gather()'s scheduling
loop hands them to an anyio task group one at a time. If that loop is
interrupted partway through - e.g. by pytest-timeout's SIGALRM-based
per-test timeout, which can fire between any two bytecode instructions,
unlike normal async cancellation - any coroutine not yet scheduled is
abandoned and silently garbage collected later, producing a "coroutine
'Provider.get_tool' was never awaited" warning attributed to whatever
unrelated test happens to be running when the GC gets to it.

Change gather() to take a single iterable consumed lazily, one awaitable
at a time, right before each is scheduled, and close any awaitable that
was just retrieved if scheduling it raises. Update call sites to pass
generator expressions instead of eagerly-built lists so coroutine
creation and scheduling stay tightly coupled.

* Close unscheduled awaitables from eager callers; make get_tasks lazy
2026-07-20 11:01:30 -04:00
Jeremiah Lowin
d2ac7ed3d2
Default fastmcp.Client to mode="auto"; surface extensions=/result_claims=
Client negotiates the newest mutual protocol era by default (probe
server/discover, fall back to the initialize handshake). ProxyClient and the
inspect utility explicitly pin the handshake era so proxy forwarding and
server_info reads are unchanged. SSE and multi-server config transports are
legacy-only. extensions= and result_claims= (SEP-2133) are thin passthroughs
to the SDK session.
2026-07-19 21:22:49 -04:00
715 changed files with 82480 additions and 24596 deletions

View file

@ -96,12 +96,9 @@ Codex sometimes re-posts old comments that reference code you've already fixed (
## Labels — never apply or invent them
**Do not apply labels to PRs or issues programmatically, and never create new ones.** Labeling is the maintainer's call (and is often automated). Two hard rules:
**Do not apply labels to PRs or issues programmatically, and never create new ones.** Issues and PRs in this repo are auto-labeled by a bot based on title, body, and code changes — there's no fixed canonical list to match against, and GitHub's "add labels" API auto-creates any label name that doesn't already exist, so a typo or guessed name silently pollutes the repo's label list with a stray, uncolored duplicate. There is no MCP tool to delete a label, so a mistaken creation can only be cleaned up by hand in repo settings.
- **Never invent a label.** GitHub's "add labels" API *auto-creates* any label name that doesn't already exist — so a typo or a guessed name silently pollutes the repo's label list with a stray, uncolored duplicate. Adding `breaking` (which does not exist) creates it alongside the real `breaking change` label.
- **Use only labels that already exist.** If you genuinely need to confirm a label, look it up first (`get_label` / the repo's label list) and match the exact name. The canonical names here are specific — e.g. the breaking-change label is **`breaking change`**, not `breaking`; enhancements is **`enhancements`**, features is **`features`**, bugs is **`bugs`**.
When a change warrants a label (e.g. it's breaking), **say so in the PR body and let the maintainer apply the label** rather than applying it yourself. There is no MCP tool to delete a label, so a mistaken creation can only be cleaned up by hand in repo settings — the cost of guessing is high and one-directional.
Don't call out a "suggested" or "appropriate" label in the PR body either — the bot doesn't read it, and it just adds noise.
## When a PR is ready

View file

@ -37,6 +37,11 @@ inputs:
required: false
default: ""
extra-allowed-tools:
description: "Additional comma-separated tools to append to allowed-tools"
required: false
default: ""
model:
description: "Model to use for Claude"
required: false
@ -88,7 +93,7 @@ runs:
track_progress: ${{ inputs.track-progress }}
prompt: ${{ inputs.prompt }}
claude_args: |
${{ (inputs.allowed-tools != '' || inputs.extra-allowed-tools != '') && format('--allowedTools {0}{1}', inputs.allowed-tools, inputs.extra-allowed-tools != '' && format(',{0}', inputs.extra-allowed-tools) || '') || '' }}
${{ (inputs.allowed-tools != '' || inputs.extra-allowed-tools != '') && format('--allowedTools ''{0}{1}''', inputs.allowed-tools, inputs.extra-allowed-tools != '' && format(',{0}', inputs.extra-allowed-tools) || '') || '' }}
${{ inputs.mcp-servers != '' && format('--mcp-config ''{0}''', inputs.mcp-servers) || '' }}
--model ${{ inputs.model }}
settings: |

View file

@ -46,6 +46,16 @@ runs:
PARALLEL_FLAGS="--numprocesses auto --maxprocesses $MAX_PROCS --dist worksteal"
fi
# pytest-timeout has no signal-based method on Windows, so it falls back
# to the thread method, which dumps stacks and os._exit()s the process.
# Under a contended runner that turns a single slow test into a dead
# xdist worker, failing whichever unrelated test that worker happened to
# be running. Give parallel Windows runs more headroom so ordinary
# scheduling jitter does not take a worker down.
if [ "$RUNNER_OS" == "Windows" ] && [ "$MAX_PROCS" != "0" ]; then
TIMEOUT=$((TIMEOUT * 4))
fi
uv run --no-sync pytest \
--inline-snapshot=disable \
--timeout=$TIMEOUT \

View file

@ -1,14 +0,0 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
labels:
- "dependencies"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
labels:
- "dependencies"

View file

@ -19,6 +19,13 @@ jobs:
issues: write
id-token: write
# TEMPORARY PIN — see the matching note in marvin-label-triage.yml.
# Claude Code 2.1.216 broke every Bash call under the action's subprocess
# isolation, which this workflow needs for all of its `gh` searching.
# https://github.com/anthropics/claude-code/issues/79997
env:
PINNED_CLAUDE_CODE_VERSION: "2.1.215"
steps:
- name: Checkout repository
uses: actions/checkout@v7
@ -91,9 +98,17 @@ jobs:
- name: Clean up stale Claude locks
run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true
- name: Install pinned Claude Code
id: pin-claude
run: |
curl -fsSL https://claude.ai/install.sh | bash -s -- "$PINNED_CLAUDE_CODE_VERSION"
echo "path=$HOME/.local/bin/claude" >> "$GITHUB_OUTPUT"
"$HOME/.local/bin/claude" --version
- name: Run Marvin dedupe command
uses: anthropics/claude-code-action@v1
with:
path_to_claude_code_executable: ${{ steps.pin-claude.outputs.path }}
github_token: ${{ steps.marvin-token.outputs.token }}
bot_name: "Marvin Context Protocol"
prompt: ${{ steps.dedupe-prompt.outputs.PROMPT }}

View file

@ -27,6 +27,22 @@ jobs:
issues: write
pull-requests: write
# TEMPORARY PIN — remove once upstream ships a fix.
#
# Claude Code 2.1.216 regressed the sandbox that claude-code-action wraps
# every Bash call in when `allowed_non_write_users` is set: the mountpoint
# walk fails closed, so every command — down to `true` — dies with
# `bwrap: Can't create file at /home/.mcp.json: Permission denied`.
# Marvin still reads the issue and picks correct labels, then cannot run
# the helper that applies them, so triage silently applied zero labels
# from 2026-07-20 onward while every run reported success.
#
# 2.1.215 is the last release without the regression.
# https://github.com/anthropics/claude-code/issues/79997
# https://github.com/anthropics/claude-code-action/issues/1547
env:
PINNED_CLAUDE_CODE_VERSION: "2.1.215"
steps:
- name: Checkout base repository
uses: actions/checkout@v7
@ -142,10 +158,21 @@ jobs:
- name: Clean up stale Claude locks
run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true
# Mirrors how the action installs Claude Code itself, minus the version
# it hardcodes. Passing path_to_claude_code_executable makes the action
# skip its own install and use this build.
- name: Install pinned Claude Code
id: pin-claude
run: |
curl -fsSL https://claude.ai/install.sh | bash -s -- "$PINNED_CLAUDE_CODE_VERSION"
echo "path=$HOME/.local/bin/claude" >> "$GITHUB_OUTPUT"
"$HOME/.local/bin/claude" --version
- name: Run Marvin for Issue Triage
id: marvin
uses: anthropics/claude-code-action@v1
with:
path_to_claude_code_executable: ${{ steps.pin-claude.outputs.path }}
github_token: ${{ steps.marvin-token.outputs.token }}
bot_name: "Marvin Context Protocol"
prompt: ${{ steps.triage-prompt.outputs.PROMPT }}
@ -173,7 +200,7 @@ jobs:
# agent reaching for something never on the allowlist (falling back to
# `gh issue view` when the API is down, say) is behaving normally, and
# failing on that would cry wolf during every GitHub incident.
- name: Fail if an allowlisted tool was denied
- name: Fail if Marvin could not run its tools
if: always() && steps.marvin.conclusion != 'skipped'
env:
EXECUTION_FILE: ${{ steps.marvin.outputs.execution_file }}
@ -222,9 +249,41 @@ jobs:
echo "::notice::Marvin was denied $total call(s), none of them to tools this workflow grants. That is expected when it probes for a tool we deliberately withhold; the allowlist is intact."
fi
# A granted tool can also fail *after* the permission check, which the
# denial count above cannot see. Claude Code 2.1.216 did exactly that:
# the sandbox refused to build and every Bash call — including the
# labeling helper — exited 1 with `bwrap: ...`, while the run stayed
# green. Correlate results back to their Bash tool_use rather than
# grepping the whole log, so an issue body quoting a sandbox error
# cannot fail an otherwise healthy run.
if ! sandbox=$(jq -sr '
[ .[] | if type == "array" then .[] else . end ]
| map(select(type == "object" and (.type == "assistant" or .type == "user")))
| map(.message.content // []) | flatten
| map(select(type == "object"))
| . as $blocks
| ( $blocks
| map(select(.type == "tool_use" and .name == "Bash"))
| map(.id) ) as $bash
| $blocks
| map(select(.type == "tool_result" and (.tool_use_id as $i | $bash | index($i))))
| map(.content | tostring)
| map(select(test("bwrap:|Failed to (start|create) sandbox")))
| "\(length)\t\(.[0] // "" | gsub("[\t\n]"; " ") | .[0:200])"
' "$file"); then
echo "::error::Could not scan Marvin execution log for sandbox failures ($file)."
exit 1
fi
IFS=$'\t' read -r sandbox_failures sandbox_sample <<<"$sandbox"
if [[ "$sandbox_failures" -gt 0 ]]; then
echo "::error::Marvin's Bash tool failed $sandbox_failures time(s) inside the action's subprocess sandbox, so it could not apply labels: ${sandbox_sample}. This is an environment failure, not a prompt or allowlist problem — check whether the pinned Claude Code version (${PINNED_CLAUDE_CODE_VERSION}) still avoids the upstream sandbox regression."
exit 1
fi
- name: Upload Marvin execution log
if: always() && steps.marvin.conclusion != 'skipped'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: marvin-triage-execution-log
path: |

View file

@ -35,7 +35,7 @@ jobs:
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
- name: Set up Python 3.10
uses: actions/setup-python@v6
uses: actions/setup-python@v7
with:
python-version: "3.10"

View file

@ -0,0 +1,104 @@
name: Publish fastmcp-tasks to PyPI
on:
workflow_run:
workflows: ["Publish fastmcp-slim to PyPI"]
types: [completed]
workflow_dispatch:
permissions:
contents: read
id-token: write
jobs:
pypi-publish:
name: Upload fastmcp-tasks to PyPI
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'release')
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
# Maintenance branches predate the standalone fastmcp-tasks package and
# resolve the `tasks` extra through fastmcp-slim instead. This workflow
# runs from the default branch for every fastmcp-slim release, including
# those tags, so detect the package rather than assume it is there.
- name: Check whether this ref builds fastmcp-tasks
id: package_present
run: |
if [ -d fastmcp_tasks ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "This ref has no fastmcp_tasks package; nothing to publish."
fi
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Build fastmcp-tasks
if: steps.package_present.outputs.present == 'true'
run: uv build --package fastmcp-tasks
- name: Verify matching fastmcp-slim is published
if: steps.package_present.outputs.present == 'true'
run: |
SLIM_VERSION=$(python - <<'PY'
import email.parser
import re
import zipfile
from pathlib import Path
wheel = next(Path("dist").glob("fastmcp_tasks-*.whl"))
metadata_name = next(
name for name in zipfile.ZipFile(wheel).namelist()
if name.endswith(".dist-info/METADATA")
)
metadata = email.parser.Parser().parsestr(
zipfile.ZipFile(wheel).read(metadata_name).decode()
)
for value in metadata.get_all("Requires-Dist", []):
requirement, _, marker = value.partition(";")
if marker.strip():
continue
match = re.fullmatch(
r"fastmcp-slim(?:\[[^\]]+\])?==([^;\s]+)",
requirement.strip(),
)
if match:
print(match.group(1))
break
else:
raise RuntimeError("Could not find the base fastmcp-slim dependency")
PY
)
for attempt in {1..12}; do
if python - "$SLIM_VERSION" <<'PY'
import json
import sys
import urllib.request
version = sys.argv[1]
url = f"https://pypi.org/pypi/fastmcp-slim/{version}/json"
with urllib.request.urlopen(url, timeout=30) as response:
json.load(response)
PY
then
exit 0
fi
echo "fastmcp-slim ${SLIM_VERSION} is not available on PyPI yet; retrying (${attempt}/12)."
sleep 10
done
echo "fastmcp-slim ${SLIM_VERSION} is not available on PyPI; refusing to publish fastmcp-tasks." >&2
exit 1
- name: Publish fastmcp-tasks to PyPI
if: steps.package_present.outputs.present == 'true'
run: uv publish -v dist/fastmcp_tasks-*.tar.gz dist/fastmcp_tasks-*.whl

View file

@ -115,23 +115,90 @@ jobs:
echo "fastmcp-slim ${SLIM_VERSION} is not available on PyPI; refusing to publish fastmcp." >&2
exit 1
- name: Verify matching fastmcp-tasks is published
run: |
TASKS_VERSION=$(python - <<'PY'
import email.parser
import re
import zipfile
from pathlib import Path
wheel = next(Path("dist").glob("fastmcp-*.whl"))
metadata_name = next(
name for name in zipfile.ZipFile(wheel).namelist()
if name.endswith(".dist-info/METADATA")
)
metadata = email.parser.Parser().parsestr(
zipfile.ZipFile(wheel).read(metadata_name).decode()
)
# fastmcp-tasks is pinned via the optional `tasks` extra, so its
# Requires-Dist entry carries an `extra == "tasks"` marker — unlike the
# base slim dependency, do not skip marked entries here.
#
# Print nothing when there is no such pin. Release lines that resolve
# the `tasks` extra through fastmcp-slim instead of a standalone
# fastmcp-tasks package have nothing here to verify.
for value in metadata.get_all("Requires-Dist", []):
requirement, _, _marker = value.partition(";")
match = re.fullmatch(r"fastmcp-tasks==([^;\s]+)", requirement.strip())
if match:
print(match.group(1))
break
PY
)
if [ -z "$TASKS_VERSION" ]; then
echo "This build does not pin fastmcp-tasks; the [tasks] extra cannot be uninstallable, so there is nothing to verify."
exit 0
fi
for attempt in {1..12}; do
if python - "$TASKS_VERSION" <<'PY'
import json
import sys
import urllib.request
version = sys.argv[1]
url = f"https://pypi.org/pypi/fastmcp-tasks/{version}/json"
with urllib.request.urlopen(url, timeout=30) as response:
json.load(response)
PY
then
exit 0
fi
echo "fastmcp-tasks ${TASKS_VERSION} is not available on PyPI yet; retrying (${attempt}/12)."
sleep 10
done
echo "fastmcp-tasks ${TASKS_VERSION} is not available on PyPI; refusing to publish fastmcp (the [tasks] extra would be uninstallable)." >&2
exit 1
- name: Publish fastmcp to PyPI
run: uv publish -v dist/fastmcp-*.tar.gz dist/fastmcp-*.whl
update-published-docs:
name: Update published-docs branch
name: Open published-docs PR
runs-on: ubuntu-latest
needs: pypi-publish
if: github.event_name == 'workflow_run' && github.event.workflow_run.event == 'release' && needs['pypi-publish'].outputs.is_prerelease != 'true'
timeout-minutes: 2
timeout-minutes: 5
permissions:
contents: write
contents: read
steps:
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
- uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ github.event.workflow_run.head_sha }}
token: ${{ steps.marvin-token.outputs.token }}
- name: Check release line
id: release_line
@ -146,6 +213,26 @@ jobs:
echo "Release commit is not on ${DEFAULT_BRANCH}; skipping published-docs update."
fi
- name: Point published-docs at published release
- name: Prepare published docs tree
if: steps.release_line.outputs.update_published_docs == 'true'
run: git push --force origin "HEAD:published-docs"
env:
RELEASE_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
git fetch origin published-docs
git switch --force-create published-docs-sync origin/published-docs
git read-tree --reset -u "$RELEASE_SHA"
test "$(git write-tree)" = "$(git rev-parse "${RELEASE_SHA}^{tree}")"
- name: Open published docs PR
if: steps.release_line.outputs.update_published_docs == 'true'
uses: peter-evans/create-pull-request@v8
with:
token: ${{ steps.marvin-token.outputs.token }}
base: published-docs
branch: marvin/publish-docs-v${{ needs.pypi-publish.outputs.version }}
commit-message: "Publish FastMCP v${{ needs.pypi-publish.outputs.version }} docs"
title: "Publish FastMCP v${{ needs.pypi-publish.outputs.version }} docs"
body: "Updates `published-docs` to the exact release tree. Merging publishes the documentation to production."
delete-branch: true
author: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"
committer: "marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>"

View file

@ -56,11 +56,13 @@ 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)
- Never amend commits to fix prek failures
- Never apply labels manually or invent new ones — the GitHub API auto-creates any unknown label name, polluting the repo's label list. Note the appropriate label in the PR body and let the maintainer/automation apply it. Canonical names: `bugs`, `breaking change`, `enhancements`, `features` (it's `breaking change`, not `breaking`). See the review-pr skill.
- Never apply labels manually or invent new ones — issues and PRs are auto-labeled by a bot based on title/body/code changes. Don't note a "suggested" or "appropriate" label anywhere in the PR body either. See the review-pr skill.
- Improvements = enhancements (not features) unless specified
- **NEVER** force-push on collaborative repos
- **ALWAYS** run prek before PRs
@ -68,6 +70,12 @@ When modifying MCP functionality, changes typically need to be applied across al
- **NEVER** merge a PR marked as do-not-merge or draft. Check title, body, AND labels for `[DNM]`, `DNM`, `DO NOT MERGE`, `DON'T MERGE`, `DONT MERGE`, `do-not-merge`, `dont-merge`, `[DRAFT]`, or `DRAFT` (case-insensitive, any variation — some authors use `[DRAFT]` in the title even when `isDraft` is false). Authors use these as hard stops — respect them even if CI is green and review looks clean. When triaging a batch of PRs, filter these out up front AND re-check each one's labels immediately before merging, since labels can change mid-session.
- **ALWAYS** read review-bot comments before approving a PR. CodeRabbit and chatgpt-codex-connector (Codex) leave substantive review comments on most PRs in this repo — these bots have read the diff and often flag real issues that aren't in the PR description. Use `gh pr view <num> --comments` and read the bot feedback as part of review. Unlike proposed solutions from issue reporters, review-bot feedback should be evaluated on its merits, not discounted.
- **Be constructively skeptical of bot review comments on your own PRs.** CodeRabbit, Codex, and claude[bot] run a fresh review pass on every push, which means a PR with active churn can accumulate bot comments in a stream that never really ends — each fix surfaces a new edge case the next pass can flag. Most of the early feedback is real and worth acting on; diminishing returns set in fast. Evaluate each comment on its merits, the same way you would a human reviewer: is this a real bug users will hit, or a hypothetical that requires an adversarial setup? Does the fix introduce more complexity than the problem? Has the bot missed context that's obvious to a human reader (a `*,` keyword-only marker, a design decision documented elsewhere, something already resolved on a later commit)? When a comment is pedantic, a false positive, or flagging something already fixed, reply on the thread explaining the reasoning and move on — don't keep iterating just because more comments arrive. If you find yourself three rounds deep and the feedback is shifting toward "what if someone does X" hypotheticals, you're past the point where each fix is improving the PR. Stop, document the contract as-is, and ship.
- **Resolve a review thread when you fix it; reply when you're declining it.** A fix explains itself through the commit, so resolving is enough — and it leaves unresolved threads meaning unfinished business, which is the signal worth having. A decline needs a one-line reason in a reply, because resolving collapses the thread and a hidden objection is worse than a visible one. Doing both is noise. Get thread ids from the GraphQL `reviewThreads` field, then resolve:
```bash
gh api graphql -f query='query($n:Int!){repository(owner:"PrefectHQ",name:"fastmcp"){pullRequest(number:$n){reviewThreads(first:50){nodes{id isResolved path}}}}}' -F n=<pr-number>
gh api graphql -f query='mutation($id:ID!){resolveReviewThread(input:{threadId:$id}){thread{isResolved}}}' -F id=PRRT_...
```
### Outbound Comments and Shell Interpolation
@ -111,7 +119,9 @@ Set `target_commitish` to the same branch that will receive the release tag. For
**Patch releases** (3.1.1, 3.0.2) get 1-2 sentences explaining what broke and what the fix does. Keep it minimal — the auto-generated changelog has the details.
**Merge the docs changelog PR *before* cutting the release, not after.** The post-publish `update-published-docs` job force-pushes the `published-docs` branch (which gofastmcp.com serves) to the released commit for stable releases on the default branch, so the changelog entry only reaches the live site if it's already in the commit being tagged. Land the docs PR on the release target branch first, then cut the release from that branch. If you tag first and merge docs after, this release's changelog won't appear on the live site until the next default-branch stable release force-pushes `published-docs` forward. Maintenance releases from `release/3.x` or `release/2.x` publish packages and GitHub notes without repointing `published-docs`; add their changelog entries on the maintenance branch, slotted into the matching major-version section. Two hand-maintained files mirror the GitHub release and must get a new entry for every version, newest at the top (these are `.mdx` and are not covered by the prek Prettier hook, which only runs on `yaml`/`json5` — match the existing entries' style by hand):
**Publish docs through a PR.** The `published-docs` branch serves gofastmcp.com, and repository rules reject direct pushes and force-pushes to it. Stable releases from `main` automatically open a publication PR after PyPI succeeds. For prereleases and later docs follow-ups, create the same PR manually: start a temporary branch from the current `published-docs`, make a single commit whose tree exactly matches the desired commit on `main`, and use `published-docs` as the PR base. Merging publishes to production. Never push directly to `published-docs`.
**Merge the docs changelog PR *before* cutting the release, not after.** The post-publish `update-published-docs` job opens a PR that syncs `published-docs` to the released commit for stable releases on the default branch, so the changelog entry only reaches the live site if it's already in the commit being tagged. Land the docs PR on the release target branch first, then cut the release from that branch. If you tag first and merge docs after, this release's publication PR will not include the changelog; publish `main` manually through the PR flow above or wait for the next default-branch stable release. Maintenance releases from `release/3.x` or `release/2.x` publish packages and GitHub notes without repointing `published-docs`; add their changelog entries on the maintenance branch, slotted into the matching major-version section. Two hand-maintained files mirror the GitHub release and must get a new entry for every version, newest at the top (these are `.mdx` and are not covered by the prek Prettier hook, which only runs on `yaml`/`json5` — match the existing entries' style by hand):
- `docs/changelog.mdx` is the full mirror. Add an `<Update label="v<version>" description="YYYY-MM-DD">` block with: a bold linked title (`**[v<version>: <pun>](<release-url>)**`), a condensed 1-paragraph intro (one sentence for patches), the full categorized PR list reformatted from the `--generate-notes` output (`* <title> by [@user](https://github.com/user) in [#NNNN](<pull-url>)`), a `## New Contributors` list (plain `@user`, linked PR), and a `**Full Changelog**: [vA...vB](<compare-url>)` line.
- `docs/updates.mdx` is the skimmable card feed. Add an `<Update label="FastMCP <version>" description="Month DD, YYYY" tags={["Releases"]}>` wrapping a `<Card>` that links to the GitHub release, with a 1-2 sentence summary and (for point releases) a handful of emoji-bulleted highlights.
@ -178,6 +188,20 @@ Because the docs land *before* the tag exists, derive the entry from the maintai
- **Style:** Prose over code comments for important information
- **Docstrings:** FastMCP docstrings are automatically compiled into MDX documents. Use markdown (single backticks, fenced code blocks), not RST (no double backticks). Bare `{}` in examples will be interpreted as JSX — wrap in backticks instead.
## Code Review Rules
### Framework regressions and root causes
- Review changes carefully for regressions in supported framework behavior, including interactions beyond the immediate diff. Trace relevant callers, shared abstractions, protocol and public API contracts, and all affected MCP component types. Determine whether a change fixes the causal code path or merely compensates for the symptom; side channels and special cases that leave the root cause intact should be treated as suspect.
### Comprehensive first pass
- Review the entire pull request diff against the merge base, not only the latest commits. Inspect every changed file and the relevant surrounding code, collect all independent, substantiated consequential findings before submitting the review, and report the complete set in one review whenever possible. Do not stop after finding the first few issues or defer other already-visible findings to later review cycles.
### Prior discussion and proportionality
- When prior review threads and author or maintainer replies are available, read them before commenting. Evaluate responses on their merits and do not repeat a resolved or convincingly rebutted finding without new evidence. Avoid fixating on speculative edge cases: report an edge case only when it is reachable under supported usage or a credible threat model and has meaningful impact; otherwise omit it or clearly treat it as non-blocking.
## Critical Patterns
- Never use bare `except` - be specific with exception types

View file

@ -17,6 +17,7 @@
[![Docs](https://img.shields.io/badge/docs-gofastmcp.com-blue)](https://gofastmcp.com)
[![Discord](https://img.shields.io/badge/community-discord-5865F2?logo=discord&logoColor=white)](https://discord.gg/uu8dJCgttd)
[![PyPI - Version](https://img.shields.io/pypi/v/fastmcp.svg)](https://pypi.org/project/fastmcp)
[![TypeScript](https://img.shields.io/npm/v/%40prefecthq%2Ffastmcp-ts?label=typescript&color=3178c6)](https://github.com/PrefectHQ/fastmcp-ts)
[![Tests](https://github.com/PrefectHQ/fastmcp/actions/workflows/run-tests.yml/badge.svg)](https://github.com/PrefectHQ/fastmcp/actions/workflows/run-tests.yml)
[![License](https://img.shields.io/github/license/PrefectHQ/fastmcp.svg)](https://github.com/PrefectHQ/fastmcp/blob/main/LICENSE)
@ -25,7 +26,7 @@
---
The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) connects LLMs to tools and data. FastMCP gives you everything you need to go from prototype to production:
The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) connects LLMs to tools and data. FastMCP is a full MCP application framework for servers, clients, and interactive apps. A server starts with ordinary Python:
```python
from fastmcp import FastMCP
@ -77,13 +78,15 @@ FastMCP has three pillars:
**[Servers](https://gofastmcp.com/servers/server)** wrap your Python functions into MCP-compliant tools, resources, and prompts. **[Clients](https://gofastmcp.com/clients/client)** connect to any server with full protocol support. And **[Apps](https://gofastmcp.com/apps/overview)** give your tools interactive UIs rendered directly in the conversation.
**Building in TypeScript?** [FastMCP for TypeScript](https://github.com/PrefectHQ/fastmcp-ts) is the official counterpart, built and maintained by the same team. Same pillars, same ideas, `npm install @prefecthq/fastmcp-ts`.
Ready to build? Start with the [installation guide](https://gofastmcp.com/getting-started/installation) or jump straight to the [quickstart](https://gofastmcp.com/getting-started/quickstart).
## Run FastMCP in production with Horizon
## Scale MCP with Horizon
FastMCP is the standard way to build MCP servers. **[Prefect Horizon](https://www.prefect.io/horizon?utm_source=github&utm_medium=readme&utm_campaign=readme_horizon&utm_content=readme_body)** is the enterprise MCP gateway for running them safely.
FastMCP handles the MCP application layer. **[Prefect Horizon](https://www.prefect.io/horizon?utm_source=github&utm_medium=readme&utm_campaign=readme_horizon&utm_content=readme_body)** is the enterprise MCP gateway for scaling servers and tools across teams, with centralized governance over how they are deployed, discovered, secured, and used.
Built by the FastMCP team, Horizon packages the best practices we've learned shipping the world's most popular MCP framework.
FastMCP and Horizon are built by the same team at [Prefect](https://www.prefect.io/).
Deploy FastMCP servers from GitHub with branch previews and instant rollback. Create a private registry of every MCP your company uses. Secure access with SSO and tool-level RBAC. Get audit logs, observability, and governance across your MCP stack. Remix approved tools into purpose-built endpoints for teams and agents.
@ -91,21 +94,19 @@ Start with FastMCP. [Scale with Horizon →](https://www.prefect.io/horizon?utm_
## Installation
We recommend installing FastMCP with [uv](https://docs.astral.sh/uv/):
We recommend adding FastMCP to your project with [uv](https://docs.astral.sh/uv/):
```bash
uv pip install fastmcp
uv add fastmcp
```
For full installation instructions, including verification and upgrading, see the [**Installation Guide**](https://gofastmcp.com/getting-started/installation).
**Upgrading?** We have guides for:
- [Upgrading from FastMCP v2](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2)
- [Upgrading from the MCP Python SDK](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk)
- [Upgrading from the low-level SDK](https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk)
> [!NOTE]
> If `import fastmcp` fails right after a `pip` upgrade from FastMCP 3.2 or earlier, run `pip install --force-reinstall fastmcp`. See [Troubleshooting](https://gofastmcp.com/getting-started/installation#troubleshooting) for why this happens (`uv` is unaffected).
- [Upgrading from FastMCP 3](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-3)
- [Upgrading from FastMCP 2](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2)
- [Upgrading from MCP SDK v1](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v1) or [v2](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v2)
- [Upgrading from the low-level SDK v1](https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v1) or [v2](https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v2)
## 📚 Documentation

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,153 @@
---
title: Background Tasks (SEP-2663)
---
**Status: Shipped (#4602, #4603).** This page is the approved design for rebuilding FastMCP's background-task support on the `io.modelcontextprotocol/tasks` extension. It supersedes the earlier "delete the task machinery" direction recorded during the SDK v2 migration. The [Feature Program](feature-program.md#background-tasks-sep-2663) carries the one-line status; user-facing usage is documented at [Background Tasks](https://gofastmcp.com/servers/tasks) and [Background Tasks (client)](https://gofastmcp.com/clients/tasks).
## TL;DR
Background tasks live on. The MCP spec moved them out of core and into a **Final, merged** extension — `io.modelcontextprotocol/tasks` (SEP-2663) — that keeps the polling model FastMCP already implements. **No SDK, in any language, ships a runtime for it yet.** FastMCP owns the only production-shaped execution engine (Docket/Redis) built for a near-identical protocol.
The plan: **rebuild task support on SEP-2663 as `fastmcp-tasks`, an in-repo optional package**, gated by `task=True` exactly as MCP Apps is gated by `app=True`. Remove the SEP-1686 *wire layer*; keep and re-home the *execution engine*. Along the way, introduce a **FastMCP-native server extension API** so tasks (and later Apps) plug in through one documented mechanism instead of bespoke surgery on core.
Net effect: a server that already uses `@mcp.tool(task=True)` needs **no code change**, and FastMCP plausibly becomes the first runtime implementation of the tasks extension anywhere.
## Background: where tasks stand today
FastMCP 3 shipped background tasks against **SEP-1686**, the task protocol that briefly lived in the core MCP spec. The implementation is ~4,000 lines across server, client, CLI, and an SDK shim, split into two very different halves:
- **A wire layer** — capability advertisement, the `tasks/get|result|list|cancel` handlers, a `CreateTaskResult` on augmented `tools/call`, and a Redis-backed *push* relay that lets a worker reach a client to deliver notifications and elicitation requests.
- **An execution engine** — [Docket](https://github.com/chrisguidry/docket) (queue, worker, result store, TTL, `memory://` or `redis://` backends) plus FastMCP-built durability: auth-scoped compound keys that isolate task access by caller, request-context snapshot/restore across worker processes, argument-coercion parity with the sync path, and the `fastmcp tasks worker` CLI.
The SDK v2 migration removed SEP-1686 from the core spec. The v4 design notes, until now, recorded the consequence as "delete the task machinery; users who need tasks stay on FastMCP 3." That was the right call **given the information at the time** — the assumption was that the successor protocol either didn't exist or wasn't implementable. Both halves of that assumption turned out to be wrong.
## What changed upstream: SEP-2663
Tasks were reworked, not removed. **SEP-2663 ("Tasks Extension") is Final and was merged upstream on 2026-05-15**, superseding SEP-1686. It defines the `io.modelcontextprotocol/tasks` extension, a capability-negotiated feature layered on the SEP-2133 extensions mechanism. It keeps SEP-1686's polling core and tightens it.
**The wire shape:**
1. Client advertises the tasks capability (per-request, in `_meta`). This is *consent* — "I can handle a task result" — not a request to run one.
2. Client issues a normal `tools/call`. **The server decides** whether to run it as a task.
3. If tasked, the server returns a `CreateTaskResult` (a claimed result shape carrying `resultType: "task"`) with a **server-generated** `taskId`.
4. Client polls `tasks/get` until the status is terminal; the result is **inlined** into that response.
5. In-task input (elicit/sample/roots requested *during* execution) is **poll-based**: status flips to `input_required`, outstanding requests appear in an `inputRequests` map, and the client answers via `tasks/update`.
6. `tasks/cancel` is cooperative. Optional push exists (`notifications/tasks` over `subscriptions/listen`) but servers need not send it.
**Delta from SEP-1686** — and the striking thing is that most of it is *deletion*, because the spec moved toward what FastMCP already built:
| Dimension | SEP-1686 (old) | SEP-2663 (new) | FastMCP today |
| --- | --- | --- | --- |
| Task-id generation | Client-generated | **Server**-generated | Already server-generated |
| `tasks/list` | Present | **Removed** (enumeration risk) | Already a stub returning `[]` |
| Result retrieval | Separate `tasks/result` | **Inlined** into `tasks/get` | Merge two handlers into one |
| `tasks/delete` | Present | **Removed** (rely on TTL) | TTL is Docket-native |
| Creation race | `notifications/tasks/created` | **Durable-creation MUST** | One read-your-writes check away |
| In-task input | Push relay + `_meta` tagging | **Poll**: `input_required` + `tasks/update` | Replaces the hairiest module |
| Statuses | 7 (incl. `submitted`, `unknown`) | 5 | Shrinks a mapping table |
| Augmentable requests | Any | **`tools/call` only** | Tools-only surface (see scope) |
| LB routing | Unspecified | `Mcp-Name: <taskId>` header | Moot with shared Redis |
**Critically: no runtime exists.** The `ext-tasks` repo is schema + prose only. The TypeScript and Python SDKs carry the wire types and conformance fixtures — no client/server implementation. The field is open.
## The decision
**Build it.** Two facts flip the earlier "delete and wait" call:
1. **The spec is what FastMCP already implements**, minus a push relay it can now shed. The rebuild is dominated by deletion and a thin new wire adapter, not a from-scratch effort.
2. **FastMCP is uniquely positioned.** SEP-2663 *assumes* a durable server-side store, server-minted high-entropy ids, eventual-consistency-aware creation, and multi-node routing — precisely what Docket/Redis provides. No other framework has this built.
Maintaining the SEP-1686 machinery through the migration is dead weight (it's the sole reason for the `_sdk_patches.py` shim, the `TaskNotificationHandler`, and a cluster of protocol-era xfails). Rebuilding on SEP-2663 clears that debt *and* produces a flagship v4 capability with a zero-code-change migration story.
## Architecture
### Engine and wire split
The existing code already separates cleanly along this line; the rebuild makes the boundary a package boundary.
- **Removed:** the SEP-1686 wire layer — capability advertisement, the four CRUD handlers, and (the big win) the entire Redis push relay (`server/tasks/elicitation.py`, `notifications.py`), which existed only because SEP-1686 had no poll-based in-task input channel. SEP-2663's `input_required`/`tasks/update` replaces it; the request/response store survives, the push envelope does not.
- **Kept and re-homed:** the Docket execution engine, the auth-scoped key encoding (this is our *authorization* layer for `tasks/get`/`update`/`cancel` — stronger than the spec's "taskIds may be bearer tokens"), context snapshot/restore, argument coercion, and the worker CLI. All of it is wire-agnostic.
- **New:** a thin SEP-2663 wire adapter — capability, the `tasks/get`/`update`/`cancel` methods, and a `tools/call` interceptor that decides-and-tasks.
### Packaging
`fastmcp-tasks` becomes an in-repo `uv` workspace member on the `fastmcp_remote` template (own `pyproject.toml`, lockstep-versioned, re-exported through the `fastmcp` metapackage). The DX parallel with MCP Apps is exact:
| Concern | MCP Apps | Background tasks |
| --- | --- | --- |
| Authoring flag (core) | `@mcp.tool(app=True)` | `@mcp.tool(task=True)` |
| Optional package | `prefab-ui` | `fastmcp-tasks` |
| Extra | `fastmcp[apps]` | `fastmcp[tasks]` |
| Missing-package behavior | Loud install hint | Loud install hint at server build |
**Core keeps only the declaration:** `task=True` / `TaskConfig` is metadata on a component, with no engine import. Everything else — engine and wire adapter — lives in the `fastmcp-tasks` package. The existing `[tasks]` extra re-points from the SEP-1686 machinery to `fastmcp-tasks`, so `pip install fastmcp[tasks]` and `task=True` keep working with modern wire underneath.
Activation stays **implicit-but-loud** (the existing `require_docket()` pattern, not silent degradation): `task=True` anywhere triggers a lazy import of `fastmcp-tasks` at build time; a missing install raises immediately. A tool the author marked as a task silently running inline would be a correctness bug, not a graceful fallback.
### The extension API
MCP extensions (SEP-2133) are a **genuinely new abstraction in SDK v2** — they did not exist in v1. So MCP Apps hand-rolling its integration wasn't a wrong choice; it predates the tool. Today FastMCP's **server** bypasses the SDK's `Extension` class entirely (it hand-splices the `ui` capability onto the low-level server and walks tool metadata directly), while the **client** forwards `ClientExtension` natively. Every new protocol extension currently means bespoke core surgery.
Tasks is the forcing function to fix that. The design adds a single registration point:
```python test="skip"
from fastmcp import FastMCP
from fastmcp_tasks import TasksExtension
mcp = FastMCP("Server")
mcp.add_extension(TasksExtension(url="redis://...")) # required to enable tasks
@mcp.tool(task=True) # intent: this tool CAN run as a task
async def crunch(dataset: str) -> str:
...
```
`add_extension` is **required** for `task=True` to work — it is not autodetected from the presence of `task=True` flags. This is deliberate. The extension needs configuration that has to live somewhere (backend URL, worker concurrency, TTL defaults), and `add_extension(TasksExtension(...))` is its natural home; autodetection would only scatter that config into settings/env and hide the moment of enablement. Requiring it also keeps capability advertisement honest — the server advertises the `tasks` capability iff the extension is registered — and removes the worst footgun, a tool silently running on an in-memory backend in production because nobody configured Redis. The two concerns stay cleanly separated: `task=True` is per-component intent ("this tool *can* be a task"); `add_extension` is server-wide enablement and config ("this server *runs* tasks, here's how"). Using `task=True` with no extension registered is a loud build-time error.
The extension API contributes a negotiated capability, additive request methods, and a `tools/call` interceptor — with access to FastMCP-level constructs the SDK's `Extension` withholds (the component registry, `Context`, auth scope). It is **designed against tasks** because tasks exercises the full surface (capability + methods + interception + client claims + notifications), where Apps exercises only a subset. Apps migrates onto the extension API as a fast-follow, deleting the hand-rolled splices and confirming the design generalizes.
**Extension vs. middleware** — the discriminator, so we do not over-apply this: an extension is a *negotiated contract change the client must understand*; middleware is *unilateral server behavior the client never sees*. PII detection, auth, rate limiting → [middleware](https://gofastmcp.com/servers/middleware). Tasks, Apps → extensions. Litmus test: delete the capability advertisement — if nothing about the client's behavior changes, it was middleware.
### Client experience
SEP-2663 removed the client-side "make this a task" flag — the server decides. That maps onto FastMCP's existing two-tier client surface, the **friendly** `call_tool` vs the **low-level** `call_tool_mcp`, so there is almost no new API:
- **`call_tool(name, args)` (friendly)** — advertises the capability and, if the server tasks the call, **transparently drives the poll loop** and returns the finished result. Whether the server tasked it is invisible. The machinery already exists: the migration wired claim-resolution through `call_tool_mcp`'s `allow_claimed` path, so a returned `CreateTaskResult` is finished into an ordinary `CallToolResult`. In-task `input_required` routes through the client's **existing elicitation handler**, answered via `tasks/update` — so background elicitation looks identical to foreground elicitation, with zero new client API.
- **`call_tool_mcp(...)` (low-level)** — hands back the raw `CreateTaskResult` claimed shape for callers managing the task themselves.
- **A "return quickly" flag on the friendly interface** yields the `Task` handle (`.status()`, `.wait()`, `.cancel()`, awaitable) without blocking — the escape hatch for progress and cancellation.
Server-side, `TaskConfig` modes translate directly: `required` → always task (`-32003` for non-declaring clients), `optional` → task iff the client declared, `forbidden` → never.
## Sequencing
1. **Design + unit-test the extension API** against tasks' full surface (capability, methods, interception, client claims/notifications) — as its own testable layer, proven in isolation with a trivial in-test extension before any tasks logic lands on it.
2. **Build `fastmcp-tasks`** — extract the engine from the removed SEP-1686 layer, write the SEP-2663 adapter, port the client half.
3. **Migrate MCP Apps onto the extension API** — fast-follow, off the critical path, with Apps' existing green tests as the regression net.
Tasks leads because only it exercises the full API surface; leading with the Apps subset would design us into a corner. Apps becomes the second consumer that confirms generality.
## Scope for v1 (non-goals)
- **Polling only.** The optional `notifications/tasks` push and `subscriptions/listen` integration are deferred to a later `fastmcp-tasks` version. This lets the second Redis notification queue die rather than be ported.
- **`tools/call` only — do not lead the spec.** SEP-2663 augments `tools/call` only. FastMCP 3 offered `task=True` on prompts and resources *ahead* of the SDK under SEP-1686, and that was a mistake: it produced wire-inexpressible capability, a permanent xfail cluster, and the sdk-feedback #3 gap. The rebuild does **not** repeat it — `task=` is a tools-only surface, and the generic prompt/resource task spine is dropped rather than carried. If the spec extends augmentation later, the surface grows with it.
- **Ship experimental.** The `ext-tasks` schema is labeled experimental with no releases; `fastmcp-tasks` ships labeled experimental initially and revs on its own cadence when the schema moves.
## Risks
| Risk | Mitigation |
| --- | --- |
| **Spec churn** (extension is experimental) | Thin wire adapter over a wire-agnostic engine; ship experimental; SEP itself is Final, so the polling model is stable even if field names move. |
| **Era gating** — SDK strips `capabilities.extensions` at pre-2026 negotiated versions (sdk-feedback #2) | Advertisement effectively requires the 2026-07-28 era. FastMCP 3 covers legacy tasks. **#2 now gates a flagship feature → escalate upstream.** |
| **Co-developing a new abstraction + greenfield feature** | Build and unit-test the extension API in isolation first (step 1) before tasks logic lands on it. |
| **Naming confusion**`[tasks]` extra re-points under the same name | Deliberate changelog note; user code and the extra name are unchanged, only the wire modernizes. |
## Design decisions (resolved)
These were the open forks; the maintainer has settled them. Recorded here so the direction is unambiguous going into implementation.
1. **Wire adapter location — in the `fastmcp-tasks` package.** The engine *and* the SEP-2663 wire adapter live in the package; core carries only the `task=True` declaration. This isolates the experimental schema's churn from core, at the cost of diverging from the Apps precedent (where the `ui` wire glue lives in core today — Apps will converge onto this model when it migrates to the extension API).
2. **Extension API shape — a FastMCP-native `mcp.add_extension()`, required to enable tasks.** Chosen over a thin pass-through to the SDK's `MCPServer(extensions=...)` because the FastMCP-native API can hand extensions the `Context`, component registry, and auth scope the SDK's `Extension` withholds. `add_extension` is **required** for `task=True` (not autodetected) — it is the single home for backend config and the honest source of capability advertisement. See [The extension API](#the-extension-api).
3. **Client default — transparent completion on the friendly interface.** `call_tool` drives the poll loop and returns the finished result; `call_tool_mcp` exposes the raw `CreateTaskResult`; a "return quickly" flag yields the `Task` handle. See [Client experience](#client-experience).
4. **Experimental labeling — yes.** `fastmcp-tasks` ships labeled experimental for at least one minor cycle, tracking the experimental `ext-tasks` schema.
5. **Resource/prompt spine — dropped; tools-only.** The rebuild does not lead the SDK on augmentable request types, correcting the SEP-1686-era mistake. See [Scope for v1](#scope-for-v1-non-goals).

View file

@ -4,33 +4,54 @@ title: Change Register
This is the complete register of user-facing changes from the MCP Python SDK v2 migration ([PR #4437](https://github.com/PrefectHQ/fastmcp/pull/4437)), organized by subsystem. It doubles as a review lens: take one subsystem, read its claimed changes, and verify each against the diff.
Each entry is tagged **Absorbed** (public surface unchanged), **Bridged** (shim keeps old code working, usually warning), **Breaking** (user code must change), or **Deprecated** (works, warns, slated for removal). See the [overview](/development/v4-notes/index) for what each disposition means.
Each entry is tagged **Absorbed** (public surface unchanged), **Bridged** (shim keeps old code working, usually warning), **Breaking** (user code must change), or **Deprecated** (works, warns, slated for removal). See the [overview](index.md) for what each disposition means.
**Empirical validation (WS2 upgrade reality-check).** The register's compatibility claims are verified, not predicted. Running unchanged 3.x-era code against this branch, all 11 upgrade scenarios pass or warn — the only failures are the two predicted breaks, user `mcp.types` imports and positional `McpError(ErrorData(...))` construction. Cross-version wire interop between a 3.4.3 peer and this branch is bidirectionally clean across 9 operations (3.4.3 client ↔ v4 server and v4 client ↔ 3.4.3 server over HTTP). All 25 `_ALIASES` bridge entries warn correctly with actionable messages.
**Empirical validation (WS2 upgrade reality-check).** The register's compatibility claims are verified, not predicted. Running unchanged 3.x-era code against this branch, all 11 upgrade scenarios pass or warn — the only failures were the two predicted breaks, user `mcp.types` imports and positional `McpError(ErrorData(...))` construction — and the first of those went away when the stable SDK restored `mcp.types` (below). Cross-version wire interop between a 3.4.3 peer and this branch is bidirectionally clean across 9 operations (3.4.3 client ↔ v4 server and v4 client ↔ 3.4.3 server over HTTP). All 29 `_ALIASES` bridge entries warn correctly with actionable messages.
## Environment
### Dependency floors: pydantic >= 2.12, Starlette >= 1.0 — Breaking (environment)
The SDK v2 raises FastMCP's dependency floors. Projects pinning an older pydantic (e.g. `2.11.*`) hit an unsatisfiable-resolution error at install time and must bump their pin; unpinned projects get pydantic upgraded silently. The server extra floors Starlette at `>=1.0.1` — modern FastAPI (0.11x+) already runs Starlette 1.x, so coexistence is clean (verified with FastAPI 0.138.2); only very old FastAPI pinned below Starlette 1.0 conflicts. Both are documented in the [upgrade guide's Environment requirements](/getting-started/upgrading/from-fastmcp-3#environment-requirements).
The SDK v2 raises FastMCP's dependency floors. Projects pinning an older pydantic (e.g. `2.11.*`) hit an unsatisfiable-resolution error at install time and must bump their pin; unpinned projects get pydantic upgraded silently. The server extra floors Starlette at `>=1.0.1` — modern FastAPI (0.11x+) already runs Starlette 1.x, so coexistence is clean (verified with FastAPI 0.138.2); only very old FastAPI pinned below Starlette 1.0 conflicts. Both are documented in the [upgrade guide's Environment requirements](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-3#environment-requirements).
*Verify:* `fastmcp_slim/pyproject.toml` (`pydantic[email]>=2.12.0` core, `starlette>=1.0.1` server extra); WS2 environment-upgrade scenario.
## Types and imports
The SDK v2 split protocol types into a standalone `mcp_types` package and renamed every field from camelCase to snake_case. This is the single largest source of user-facing change, and FastMCP absorbs nearly all of it.
The SDK v2 moved protocol types into a standalone `mcp_types` package — still importable as `mcp.types` and renamed every model field from camelCase to snake_case in Python. The wire format is unchanged: the models keep their camelCase aliases and the SDK serializes with `by_alias=True`, so this renames the attributes code reads, not the JSON on the connection. This is the single largest source of user-facing change, and FastMCP absorbs nearly all of it.
### `mcp.types` split into `mcp_types` — Breaking (by omission)
<Note>
Superseded by the stable SDK — see "`mcp.types` restored as a permanent alias" below. The betas this section was written against had no `mcp.types`; `2.0.0` brought it back, so the break never reached a release.
</Note>
The `mcp.types` module no longer exists. Any `from mcp.types import X` or `import mcp.types` in user code raises `ImportError`. This is the one import change users cannot avoid.
*Verify:* `fastmcp_slim/fastmcp/types.py`, and grep the diff for the doc migration `from mcp.types import``from fastmcp.types import` (30 sites).
### `mcp.types` restored as a permanent alias — Absorbed (stable-SDK change)
The SDK betas removed `mcp.types` outright, which made user imports the one unavoidable break in the migration. SDK `2.0.0` reintroduced it as a permanent alias for `mcp_types`: a wildcard mirror where every name is the *same object* (`mcp.types.Tool is mcp_types.Tool`), with matching `__all__` and the same snake_case fields. It is not a v1 restoration — only the import path came back. So `from mcp.types import X` keeps working, and the break is gone.
This leaves the two spellings pointing at one package, and FastMCP uses each in a different place on purpose:
- **User-facing docs and examples use `mcp.types`.** Anyone installing `fastmcp` gets the full SDK (`fastmcp``fastmcp-slim[client,server]``[mcp]``mcp`), so the aliased path always resolves and is the spelling the SDK prefers. It also means a user's own dependency list needs only `mcp`, without naming `mcp-types` to satisfy a linter.
- **FastMCP's own source uses `mcp_types`.** `mcp.types` is a submodule of `mcp`, so importing it requires the whole SDK. `mcp-types` is a *core* `fastmcp-slim` dependency while `mcp` sits behind the `[mcp]` extra, and a bare `fastmcp-slim` install must import without the SDK present — a guarantee `test_bare_slim_import_needs_only_mcp_types` pins. Reaching for `mcp.types` in core modules (`exceptions.py`, `_compat.py`, `tools/`, `resources/`) would pull the full SDK into the slim floor and break it.
The rule of thumb: import `mcp_types` in library code, write `mcp.types` in anything a user copies. Both resolve to the same objects, so neither choice constrains the other.
*Verify:* `.venv/.../mcp/types/__init__.py` (the wildcard mirror), `fastmcp_slim/pyproject.toml` (`mcp-types` core vs `mcp` in the `[mcp]` extra), `tests/client/test_slim_package_boundaries.py::test_bare_slim_import_needs_only_mcp_types`, and `tests/test_upgrade_from_v3.py::TestRemovedSurfacesFailLoudly::test_mcp_types_import_path_restored_by_stable_sdk`.
### `fastmcp.types` is the stable home — Bridged
<Note>
Superseded before release — see "`fastmcp.types` trimmed to FastMCP-unique types only" below. This section documents the re-export set as it existed mid-migration; none of it ever shipped.
</Note>
FastMCP re-exports the protocol types users are most likely to touch from `fastmcp.types`, sourced from `mcp_types` (the `mcp` root package lacks most of them):
```python
```python test="skip"
from fastmcp.types import TextContent, Tool, ToolAnnotations, ErrorData
```
@ -38,6 +59,18 @@ The re-export set is deliberately limited to names that trace to a documented us
*Verify:* `fastmcp_slim/fastmcp/types.py` `__all__`.
### `fastmcp.types` trimmed to FastMCP-unique types only — Absorbed (post-review cleanup)
The re-export set above never shipped in a release, so it was cut before 4.0 rather than deprecated. `fastmcp.types` now holds only types FastMCP defines itself — `Textarea` — and every bare `mcp_types` mirror (`TextContent`, `Tool`, `ToolAnnotations`, `ErrorData`, and the rest of the 29-name list) is gone. Code that imported those from `fastmcp.types` now imports them from `mcp_types` directly:
```python
from mcp_types import TextContent, Tool, ToolAnnotations, ErrorData
```
Because `fastmcp.types.__all__` was `["Textarea"]` as of the last stable release (v3.4.4) and the mirrors were added only in this unreleased migration work, removing them breaks no released user — there is no bridge or deprecation warning to write.
*Verify:* `fastmcp_slim/fastmcp/types.py` `__all__` (back down to `["Textarea"]`).
### camelCase field reads are bridged — Bridged (deprecated)
Objects FastMCP hands back — results of `client.list_tools()`, `client.call_tool_mcp()`, `client.read_resource()`, and the parameter objects passed to sampling and elicitation handlers — are SDK v2 objects with snake_case fields. A compatibility bridge installed at import time routes the old camelCase names to their snake_case fields, warning once per read:
@ -52,7 +85,7 @@ async def read_schema():
return tools[0].inputSchema # works, warns; prefer .input_schema
```
The bridged fields are exactly those users read, data-driven from an `_ALIASES` table: `inputSchema`/`outputSchema` (Tool); `mimeType` (Resource, ResourceTemplate, TextResourceContents, BlobResourceContents, ImageContent, AudioContent) and `uriTemplate` (ResourceTemplate); `isError`/`structuredContent` (CallToolResult); `hasMore` (Completion); `serverInfo`/`protocolVersion` (InitializeResult); `nextCursor`/`resourceTemplates` (List\*Result); `systemPrompt`/`maxTokens`/`stopSequences`/`modelPreferences`/`toolChoice` (CreateMessageRequestParams); `requestedSchema` (ElicitRequestFormParams). WS2 verified all 25 alias entries warn correctly with actionable messages.
The bridged fields are exactly those users read, data-driven from an `_ALIASES` table: `inputSchema`/`outputSchema` (Tool); `readOnlyHint`/`destructiveHint`/`idempotentHint`/`openWorldHint` (ToolAnnotations); `mimeType` (Resource, ResourceTemplate, TextResourceContents, BlobResourceContents, ImageContent, AudioContent) and `uriTemplate` (ResourceTemplate); `isError`/`structuredContent` (CallToolResult); `hasMore` (Completion); `serverInfo`/`protocolVersion` (InitializeResult); `nextCursor`/`resourceTemplates` (List\*Result); `systemPrompt`/`maxTokens`/`stopSequences`/`modelPreferences`/`toolChoice` (CreateMessageRequestParams); `requestedSchema` (ElicitRequestFormParams). WS2 verified all 29 alias entries warn correctly with actionable messages.
*Verify:* `fastmcp_slim/fastmcp/_compat.py` (the `_ALIASES` table and `install()`).
@ -66,7 +99,7 @@ import fastmcp
fastmcp.settings.mcp_camelcase_compat = False # now takes effect immediately
```
The setting is documented in [Settings](/more/settings) as `FASTMCP_MCP_CAMELCASE_COMPAT`.
The setting is documented in [Settings](https://gofastmcp.com/more/settings) as `FASTMCP_MCP_CAMELCASE_COMPAT`.
*Verify:* `fastmcp_slim/fastmcp/settings.py` (setting), `fastmcp_slim/fastmcp/_compat.py` (per-read gate), commit `d9659453`.
@ -120,13 +153,13 @@ The SDK's `request_ctx` ContextVar is gone; the SDK passes context to handlers a
### `ServerMiddleware` bridge for `initialize` — Absorbed
Server-side middleware is a new first-class SDK concept: `Server.middleware` is a list of `ServerMiddleware` composed around every request and notification, including `initialize`. FastMCP no longer subclasses `ServerSession` (the runner constructs it), so the old `MiddlewareServerSession._received_request` override is gone. A `FastMCPServerMiddleware` is appended to the SDK's middleware list (preserving the SDK's own OpenTelemetry middleware) and intercepts `initialize` to run FastMCP's middleware chain. The v2 seam is cleaner — `call_next(ctx)` returns the serialized result directly, so the old `capturing_respond` machinery is deleted.
Server-side middleware is a new first-class SDK concept: `Server.middleware` is a list of `ServerMiddleware` composed around every request and notification, including `initialize`. FastMCP no longer subclasses `ServerSession` (the runner constructs it), so the old `MiddlewareServerSession._received_request` override is gone. A `FastMCPServerMiddleware` is appended to the SDK's middleware list (preserving the SDK's own OpenTelemetry middleware) and intercepts `initialize` to run FastMCP's middleware chain. The v2 interface is cleaner — `call_next(ctx)` returns the serialized result directly, so the old `capturing_respond` machinery is deleted.
*Verify:* `fastmcp_slim/fastmcp/server/low_level.py` (`FastMCPServerMiddleware`).
### Middleware observes every inbound message — New (coverage)
FastMCP's `Middleware` chain used to begin *inside* the per-method handlers, so `on_message`/`on_request`/`on_notification` only fired for messages that reached a tool/resource/prompt handler. Notifications, cancellations, and malformed or unroutable requests were invisible to middleware. `FastMCPServerMiddleware` — FastMCP's entry in the SDK's own middleware list — is now the dispatch root: it runs the `on_message`/`on_request`/`on_notification` pass for every message the interior handlers do not dispatch (all notifications including `notifications/cancelled`, `ping`, `logging/setLevel`, unknown methods, and component requests that fail validation before the handler runs). The component methods keep their interior dispatch unchanged, so `on_call_tool` and friends still receive the typed component result and a tool exception still propagates through `on_message`/`on_request` exactly where the built-in error/logging/timing middleware expect it — each hook fires exactly once per message. Multi-round (SEP-2322) calls compose cleanly with this: each round is a complete request→response cycle through the full chain, and an asking round's `call_next` returns the ask as an ordinary `InputRequiredToolResult` value (see the MRTR entry below). All thirteen built-in middleware pass their suites unmodified. See [What middleware sees](/servers/middleware#what-middleware-sees).
FastMCP's `Middleware` chain used to begin *inside* the per-method handlers, so `on_message`/`on_request`/`on_notification` only fired for messages that reached a tool/resource/prompt handler. Notifications, cancellations, and malformed or unroutable requests were invisible to middleware. `FastMCPServerMiddleware` — FastMCP's entry in the SDK's own middleware list — is now the dispatch root: it runs the `on_message`/`on_request`/`on_notification` pass for every message the interior handlers do not dispatch (all notifications including `notifications/cancelled`, `ping`, `logging/setLevel`, unknown methods, and component requests that fail validation before the handler runs). The component methods keep their interior dispatch unchanged, so `on_call_tool` and friends still receive the typed component result and a tool exception still propagates through `on_message`/`on_request` exactly where the built-in error/logging/timing middleware expect it — each hook fires exactly once per message. Multi-round (SEP-2322) calls compose cleanly with this: each round is a complete request→response cycle through the full chain, and an asking round's `call_next` returns the ask as an ordinary `InputRequiredToolResult` value (see the MRTR entry below). All thirteen built-in middleware pass their suites unmodified. See [What middleware sees](https://gofastmcp.com/servers/middleware#what-middleware-sees).
*Verify:* `fastmcp_slim/fastmcp/server/low_level.py` (`FastMCPServerMiddleware` root dispatch, `_INTERIOR_METHODS`), `fastmcp_slim/fastmcp/server/middleware/middleware.py` (`MiddlewarePhase`, `mark_interior_dispatched`), `fastmcp_slim/fastmcp/server/server.py` (`_dispatch_component_middleware`), `tests/server/middleware/test_message_visibility.py`.
@ -146,10 +179,12 @@ SDK v2 declares `extensions` as a real field on `ClientCapabilities`, so a clien
The SEP-1686 task CRUD protocol (`tasks/get`, `tasks/result`, `tasks/list`, `tasks/cancel`) is entirely FastMCP-owned — the SDK ships no task store. Task detection moves to a params field: `params.task is not None` on `CallToolRequestParams`, with `ttl` from `params.task.ttl`. The four task handlers port to `add_request_handler`.
The SDK has a real gap here (see [Known Gaps](/development/v4-notes/known-gaps) and sdk-feedback #1): it ships the task result types but omits them from the method registries, so a background-task `tools/call` returning a `CreateTaskResult` fails validation. FastMCP installs a registry-widening shim in `_sdk_patches.py` that adds `CreateTaskResult` to the `tools/call` result union and registers the `tasks/*` rows. It is a temporary patch with a self-documented removal trigger.
The SDK has a real gap here (see [Known Gaps](known-gaps.md) and sdk-feedback #1): it ships the task result types but omits them from the method registries, so a background-task `tools/call` returning a `CreateTaskResult` fails validation. FastMCP installs a registry-widening shim in `_sdk_patches.py` that adds `CreateTaskResult` to the `tools/call` result union and registers the `tasks/*` rows. It is a temporary patch with a self-documented removal trigger.
Resources and prompts have **no `task` field** on their params in b1, so task-augmented resource reads and prompt gets are not wire-expressible — a documented capability regression, tracked by xfails, not a bug FastMCP fixes.
This section records the migration's *handling* of the SEP-1686 wire layer as it stood at merge. That layer is not the end state: it is slated for removal and rebuild on the `io.modelcontextprotocol/tasks` extension (SEP-2663) as the `fastmcp-tasks` package. See [Background Tasks (SEP-2663)](background-tasks.md) for the forward plan; the `_sdk_patches.py` shim and the `server/tasks/*` wire handlers described here go away with it, while the Docket execution engine moves into `fastmcp-tasks`.
*Verify:* `fastmcp_slim/fastmcp/_sdk_patches.py`, `server/tasks/*`.
### Single SERVER span per request — Absorbed (post-migration fix)
@ -158,11 +193,13 @@ SDK v2 seeds an `OpenTelemetryMiddleware` into every lowlevel `Server`, so each
*Verify:* `fastmcp_slim/fastmcp/server/low_level.py` (the `OpenTelemetryMiddleware` filter); `tests/server/telemetry/test_server_tracing.py::TestSingleServerSpan`.
### Telemetry on by default, with an explicit off-switch — Absorbed
### Telemetry on by default, with a three-way mode setting — Absorbed
FastMCP's OpenTelemetry instrumentation is on by default. Because FastMCP uses only the OpenTelemetry API, span creation is a no-op with negligible overhead (the API's `NonRecordingSpan`) unless the user configures an SDK and exporter — so being always-on costs nothing until you opt into collection. The new `FASTMCP_ENABLE_TELEMETRY` setting (`fastmcp.settings.enable_telemetry`, default `true`) is the explicit off-switch: set it to `false` and `get_tracer()` returns a genuine no-op tracer, so no FastMCP spans are created even when an SDK is configured. The off-switch governs FastMCP's own spans (all SERVER spans, plus FastMCP's high-level CLIENT span); the SDK's low-level `mcp-python-sdk` `MCP send <method>` CLIENT spans are governed by the user's OpenTelemetry SDK, not this setting. FastMCP's SERVER span now also carries `mcp.protocol.version` — the attribute the dropped SDK `OpenTelemetryMiddleware` set — restoring parity with the SDK's semantic conventions.
FastMCP's OpenTelemetry instrumentation is on by default. Because FastMCP uses only the OpenTelemetry API, span creation is a no-op with negligible overhead (the API's `NonRecordingSpan`) unless the user configures an SDK and exporter — so being always-on costs nothing until you opt into collection. `FASTMCP_TELEMETRY_MODE` (`fastmcp.settings.telemetry_mode`, default `native`) controls how much is active: `native` emits spans and propagates trace context; `propagation_only` emits no FastMCP spans but still extracts the incoming `_meta` context and attaches it, so downstream spans are parented to the calling trace; `off` is a full pass-through that touches neither spans nor context. The setting governs FastMCP's own spans (all SERVER spans, plus FastMCP's high-level CLIENT span); the SDK's low-level `mcp-python-sdk` `MCP send <method>` CLIENT spans are governed by the user's OpenTelemetry SDK, not this setting. `suppress_fastmcp_telemetry()` applies `propagation_only` semantics to a single block for library authors who own the MCP hierarchy for one operation rather than process-wide; it cannot override `off`. FastMCP's SERVER span now also carries `mcp.protocol.version` — the attribute the dropped SDK `OpenTelemetryMiddleware` set — restoring parity with the SDK's semantic conventions.
*Verify:* `fastmcp_slim/fastmcp/settings.py` (`enable_telemetry`); `fastmcp_slim/fastmcp/telemetry.py` (`get_tracer` off-switch); `fastmcp_slim/fastmcp/server/telemetry.py` (`get_protocol_span_attributes`); `tests/server/telemetry/test_server_tracing.py::TestTelemetryEnabledByDefault`, `::TestProtocolVersionAttribute`.
`propagation_only` is applied at the seam span, which is where the incoming `_meta` parent context is established for the whole request; suppressing only the deeper `server_span` would leave the per-request SERVER span intact and defeat the mode.
*Verify:* `fastmcp_slim/fastmcp/settings.py` (`telemetry_mode`); `fastmcp_slim/fastmcp/telemetry.py` (`telemetry_mode`, `get_tracer`, `suppress_fastmcp_telemetry`); `fastmcp_slim/fastmcp/server/telemetry.py` (`_propagation_only_span`, `seam_span`, `get_protocol_span_attributes`); `tests/server/telemetry/test_server_tracing.py::TestTelemetryEnabledByDefault`, `::TestProtocolVersionAttribute`; `tests/telemetry/test_interop.py`.
### Spec-correct error codes via a central translator — Breaking (wire error code)
@ -186,9 +223,66 @@ There is deliberately no compatibility alias for the old spelling.
*Verify:* `fastmcp_slim/fastmcp/server/middleware/caching.py`.
### Server-side argument completion — New (opt-in feature)
A FastMCP server can now answer `completion/complete` requests, suggesting values for prompt arguments and resource-template parameters as a user types. Previously a FastMCP *client* could call `complete()` but a FastMCP *server* had no way to respond — the method was unregistered, so it returned `-32601` (method-not-found) on both eras. The new `@mcp.completion` decorator registers a single server-level handler that receives the reference (a `PromptReference` or `ResourceTemplateReference`), the `CompletionArgument` being completed, and the optional `CompletionContext` of already-supplied argument values, and returns candidates — a list of strings, a `Completion` (to carry the `total`/`has_more` pagination hints), or `None`/empty for a reference it does not recognize (which yields an empty completion, not an error).
```python
from fastmcp import FastMCP
from mcp_types import PromptReference
mcp = FastMCP("Completion Server")
@mcp.prompt
def write_poem(theme: str) -> str:
return f"Write a poem about {theme}"
@mcp.completion
def complete(ref, argument, context):
if isinstance(ref, PromptReference) and argument.name == "theme":
options = ["nature", "love", "adventure"]
return [o for o in options if o.startswith(argument.value)]
return None
```
The completions capability is declared exactly when a handler exists: `add_completion_handler` registers the low-level `completion/complete` handler, and the SDK derives the capability from that handler's presence — a server with no completion handler does not advertise it. FastMCP does not hand-set the capability. The single-handler shape mirrors the SDK's own `completion/complete` surface and FastMCP's existing client-side `Client.complete()`, and it slots into the `@mcp.tool`/`@mcp.prompt`/`@mcp.resource` decorator lineup as another server-level `@mcp.<verb>` registration rather than inventing a per-argument sub-decorator idiom. It works identically on the handshake and modern (`2026-07-28`) eras, since `completion/complete` is a request/response method that flows on every era. The authoring types — `PromptReference`, `ResourceTemplateReference`, `CompletionArgument`, `CompletionContext`, and `Completion` — are imported from `mcp_types`, not `fastmcp.types`.
*Verify:* `fastmcp_slim/fastmcp/server/completions.py` (handler type + `normalize_completion`), `fastmcp_slim/fastmcp/server/server.py` (`completion` decorator, `add_completion_handler`), `fastmcp_slim/fastmcp/server/mixins/mcp_operations.py` (`_on_complete`), `tests/server/test_completions.py`, `docs/servers/completions.mdx`.
## Client
The `fastmcp.Client` public API is preserved exactly. The client stays a wrapper around `mcp.ClientSession` in legacy/handshake mode; the first-class `mcp.client.Client` is deliberately not adopted in this PR.
The `fastmcp.Client` public API is largely preserved. The client stays a wrapper around `mcp.ClientSession`; the first-class `mcp.client.Client` is deliberately not adopted. Two client-surface changes are called out below: the connection `mode` default flips to `"auto"`, and `extensions=` / `result_claims=` are newly surfaced.
### Connection `mode` defaults to `"auto"` — Breaking (behavior)
`Client(mode=...)` now defaults to `"auto"` instead of `"legacy"`. The client probes `server/discover` and adopts the modern (`2026-07-28`) era when the server responds, denylist-falling-back to the initialize handshake for any server that is not positive evidence of a modern peer. Against a FastMCP server (which serves both eras), an ordinary `Client(url)` now negotiates the modern era by default, where the legacy-only Context push features are unavailable per the per-feature era matrix (see the *Protocol eras* section below) — server-initiated sampling/elicitation/roots, `ping`, session ids, and FastMCP task submission all require the legacy era. The one-line revert is `Client(..., mode="legacy")`, which restores byte-identical pre-v4 negotiation.
The SSE transport is legacy-only (it cannot carry the sessionless modern era), so a client connecting over SSE negotiates the legacy handshake even under `mode="auto"` — expressed by a `ClientTransport.legacy_only` flag set on `SSETransport`. `MCPConfigTransport` reports `legacy_only` as a property: a multi-server config is legacy-only (each backend is mounted behind a legacy-era proxy), while a single-server config mirrors its one backend transport's era so a modern Streamable HTTP backend stays modern-capable. Two internal library clients that are inherently handshake-based are pinned to legacy so the flip does not break them: the `ProxyClient` backend (which forwards the initialize handshake and server-initiated features) defaults to `mode="legacy"`, and the `inspect` utility (which reads the full `server_info` only the handshake carries) connects legacy.
```python
from fastmcp import Client
client = Client("https://example.com/mcp") # now negotiates "auto"
client = Client("https://example.com/mcp", mode="legacy") # opt back into the handshake
```
*Verify:* `fastmcp_slim/fastmcp/client/client.py` (`mode` default, `_negotiate` `legacy_only` shortcut), `fastmcp_slim/fastmcp/client/transports/{base,sse,config}.py` (`legacy_only`), `fastmcp_slim/fastmcp/server/providers/proxy.py` (`ProxyClient` legacy default), `fastmcp_slim/fastmcp/mcp_config.py` and `fastmcp_slim/fastmcp/utilities/inspect.py` (legacy inner clients), `tests/client/client/test_mode_negotiation.py` (default, clean discover-rejection fallback, legacy-only transport), `tests/test_mcp_config.py` (single- vs multi-server `legacy_only`), `docs/clients/client.mdx`.
### `extensions=` / `result_claims=` surfaced — New (opt-in feature)
`fastmcp.Client` now accepts `extensions=` (a sequence of SEP-2133 `ClientExtension` instances) and `result_claims=` (extra `ResultClaim`s keyed by an advertised extension's identifier). Each extension's capability advertisement, result claims, and notification bindings are folded into the underlying `ClientSession` on every transport. User-supplied notification bindings **compose** with FastMCP's internal task-status binding rather than clobbering it: the task binding always leads, and a user extension that binds the same method surfaces a clear duplicate-method error at connect time rather than silently winning. Result claims are wired end-to-end: `call_tool()` / `call_tool_mcp()` pass `allow_claimed=True` and resolve a claimed result through the owning claim's resolver (`ClaimContext`), so a server-emitted claimed shape is finished into an ordinary `CallToolResult` instead of raising `UnexpectedClaimedResult`. Claimed shapes are modern-only, so they are inert on a legacy connection.
*Verify:* `fastmcp_slim/fastmcp/client/client.py` (`_build_extension_kwargs`, `_resolve_claimed_result`, `new()`), `fastmcp_slim/fastmcp/client/mixins/tools.py` (`call_tool_mcp` claim resolution), `fastmcp_slim/fastmcp/client/transports/base.py` (`SessionKwargs.extensions`/`result_claims`), `tests/client/test_client_extensions.py` (fold, composition, live both-bindings-fire, end-to-end claim resolution).
### Protocol helpers delegated to the SDK — Absorbed (internal)
`fastmcp.Client` carried forked copies of three SDK helpers — `_fold_extensions` (with its `_FoldedExtensions` dataclass), `_evicting_message_handler`, and `_synthesize_discover` — written when the SDK had not yet stabilized them. It now imports the SDK's implementations directly. The forks had already drifted: FastMCP's `_fold_extensions` was missing the SEP-2133 `validate_extension_identifier` check, so a non-reverse-DNS extension identifier that the SDK rejects was silently accepted. Adopting the SDK's version closes that gap. No public surface moves; the SDK returns `None` rather than empty collections for the folded claims and bindings, absorbed at the two call sites in `_build_extension_kwargs`.
Full composition — `fastmcp.Client` holding an `mcp.Client` and delegating the connection lifecycle to it — remains blocked upstream. `mcp.Client._build_session` hardcodes `ClientSession(...)` with no override hook, but FastMCP's `TransportOptions.session_class` is load-bearing: `ProxyClient` supplies a `_ForwardingClientSession` that skips output-schema validation so a backend's schema bug surfaces at the end client rather than as a proxy error. Separately, `mcp.Client.__aenter__` raises on reentry, while FastMCP's refcounted reentrant context manager is depended on by proxy session reuse. Both would need an upstream `session_factory=` hook (the same shape as the `notification_bindings=` ask that unblocked extension composition) before the lifecycle itself can be delegated.
*Verify:* `fastmcp_slim/fastmcp/client/client.py` (imports from `mcp.client.client`; no local helper definitions), `fastmcp_slim/fastmcp/client/transports/base.py` (`TransportOptions.session_class`), `fastmcp_slim/fastmcp/server/providers/proxy.py` (`_ForwardingClientSession`, `PROXY_TRANSPORT_OPTIONS`).
### Transports yield 2-tuples — Absorbed
@ -213,7 +307,7 @@ client = Client("my_mcp_server.py", timeout=30.0) # also works
### Connection settings passed to `connect_session` — Breaking (custom transports)
`ClientTransport.connect_session` takes a new keyword-only `transport_options: TransportOptions | None`, describing how the connecting client wants its session built: which `ClientSession` class to instantiate, and whether to forward the caller's authorization header upstream. Proxies use it to relay backend results without enforcing their output schema (see [Proxy Servers](/servers/providers/proxy#tool-results-are-relayed-not-inspected)).
`ClientTransport.connect_session` takes a new keyword-only `transport_options: TransportOptions | None`, describing how the connecting client wants its session built: which `ClientSession` class to instantiate, and whether to forward the caller's authorization header upstream. Proxies use it to relay backend results without enforcing their output schema (see [Proxy Servers](https://gofastmcp.com/servers/providers/proxy#tool-results-are-relayed-not-inspected)).
These settings previously lived on the transport instance, so a transport shared between clients leaked one client's configuration into another — including credential forwarding, which `create_proxy(some_client)` would silently enable on the caller's own client. They now travel with the client that wants them, and `forward_incoming_headers` is no longer a settable transport attribute.
@ -287,9 +381,24 @@ config = CacheConfig(store=store, partition="tenant-a", target_id="weather-api")
*Verify:* `fastmcp_slim/fastmcp/client/caching.py`, `tests/client/client/test_kv_response_cache.py`.
### Machine-to-machine client auth — New (feature)
`fastmcp.client.auth` gains two browser-free auth providers for the OAuth 2.0 `client_credentials` grant, closing the most common client-auth gap (previously only interactive `OAuth` and static `BearerAuth` were available). `ClientCredentialsOAuthProvider(client_id=..., client_secret=...)` authenticates with a client ID and secret; `PrivateKeyJWTOAuthProvider(client_id=..., assertion_provider=...)` uses an RFC 7523 `private_key_jwt` assertion (workload identity federation or a locally signed JWT via the re-exported `SignedJWTParameters` / `static_assertion_provider` helpers). Both are thin wrappers over the SDK's `mcp.client.auth.extensions.client_credentials` providers and implement `httpx2.Auth`, so they slot into the same `Client(auth=...)` path as every other provider. Like interactive `OAuth`, they take the MCP server URL (the token endpoint is discovered from OAuth metadata) and bind to it lazily — omit `mcp_url` and the transport supplies it. In-memory token storage is the default with no warning, since a lost M2M token is re-acquired in one non-interactive request.
```python
from fastmcp import Client
from fastmcp.client.auth import ClientCredentialsOAuthProvider
auth = ClientCredentialsOAuthProvider(client_id="id", client_secret="secret")
async with Client("https://example.com/mcp", auth=auth) as client:
await client.list_tools()
```
*Verify:* `fastmcp_slim/fastmcp/client/auth/client_credentials.py`, `fastmcp_slim/fastmcp/client/transports/{http,sse}.py`, `tests/client/auth/test_client_credentials.py`.
## HTTP
The maintainer asked whether FastMCP can now delete its custom HTTP app and let the SDK's `Server.streamable_http_app()` handle everything. The answer for this PR is **no** — every override earns its keep. Convergence is a v4 project gated on three upstream additions (see [Feature Program](/development/v4-notes/feature-program)).
The maintainer asked whether FastMCP can now delete its custom HTTP app and let the SDK's `Server.streamable_http_app()` handle everything. The answer for this PR is **no** — every override earns its keep. Convergence is a v4 project gated on three upstream additions (see [Feature Program](feature-program.md)).
### Kept overrides — Absorbed
@ -312,7 +421,7 @@ FastMCP owns DNS-rebinding protection through its `HostOriginGuardMiddleware`, w
SDK v2.0.0b2 replaces `httpx` + `httpx-sse` with [httpx2](https://pypi.org/project/httpx2/) (`>=2.5.0`), a next-generation httpx fork with built-in SSE. httpx2 is a near drop-in fork: the public API (`AsyncClient`, `Auth`, `Request`, `Response`, `Timeout`, `MockTransport`, exception hierarchy, `event_hooks`) matches httpx name-for-name. The SDK duck-types the client you hand it — `streamable_http_client(http_client=...)` and `sse_client(httpx_client_factory=...)` are type-hinted `httpx2.AsyncClient` with no `isinstance` gate — but the objects that cross into the SDK must be httpx2.
FastMCP now uses **httpx2 exclusively** and no longer depends on `httpx`. Every FastMCP-owned HTTP path moves to httpx2: the client transports (`client/transports/{base,http,sse}.py`), client auth (`client/auth/{oauth,bearer}.py``BearerAuth`/`OAuth` subclass `httpx2.Auth`), the client-side exception-group handler (`utilities/exceptions.py`), the proxy's upstream client (`server/providers/proxy.py`), the `MCPConfig` client-auth field (`mcp_config.py`), **and** all the server-side code that the earlier seam pass had left on httpx — the ~15 server auth providers' upstream IdP calls, the OpenAPI provider, `from_openapi`/`from_fastapi`, `version_check`, `resources/types.py`, the SSRF download guard, and the `apps_dev` CLI. `httpx` is dropped from the `mcp` extra entirely (it may still arrive transitively via other libraries, but FastMCP never imports it). The ~170 `httpx_mock` calls across the security-critical server-auth test files are ported to a local httpx2-backed `httpx_mock` fixture (`tests/utilities/httpx2_mock.py`) that preserves the `add_response`/`add_exception`/`get_request(s)` API verbatim, so `pytest-httpx` is dropped too.
FastMCP now uses **httpx2 exclusively** and no longer depends on `httpx`. Every FastMCP-owned HTTP path moves to httpx2: the client transports (`client/transports/{base,http,sse}.py`), client auth (`client/auth/{oauth,bearer}.py``BearerAuth`/`OAuth` subclass `httpx2.Auth`), the client-side exception-group handler (`utilities/exceptions.py`), the proxy's upstream client (`server/providers/proxy.py`), the `MCPConfig` client-auth field (`mcp_config.py`), **and** all the server-side code that the earlier migration pass had left on httpx — the ~15 server auth providers' upstream IdP calls, the OpenAPI provider, `from_openapi`/`from_fastapi`, `version_check`, `resources/types.py`, the SSRF download guard, and the `apps_dev` CLI. `httpx` is dropped from the `mcp` extra entirely (it may still arrive transitively via other libraries, but FastMCP never imports it). The ~170 `httpx_mock` calls across the security-critical server-auth test files are ported to a local httpx2-backed `httpx_mock` fixture (`tests/utilities/httpx2_mock.py`) that preserves the `add_response`/`add_exception`/`get_request(s)` API verbatim, so `pytest-httpx` is dropped too.
User-visible deltas:
@ -341,32 +450,45 @@ The push-style Context features that require the server to call back into the cl
| --- | --- | --- |
| `ctx.info` / logging notifications | Supported | Supported |
| Tools, resources, prompts, completions | Supported | Supported |
| `ctx.elicit` (imperative) | Supported | Not on the back-channel — use [elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol) |
| `ctx.sample` / `ctx.sample_step` | Supported (deprecated) | Removed — call an LLM server-side |
| `ctx.list_roots` | Supported | Not yet — MRTR rewrite pending |
| Tasks (via the FastMCP client) | Supported | Not yet |
| `ctx.elicit` (imperative) | Supported | Not on the back-channel — use [elicitation on the modern protocol](https://gofastmcp.com/servers/elicitation#elicitation-on-the-modern-protocol) |
| `ctx.sample` / `ctx.sample_step` | Not in the API | Not in the API — call an LLM server-side |
| `ctx.list_roots` | Not in the API | Not in the API — take paths as arguments, or use the [guard pattern](https://gofastmcp.com/servers/elicitation#elicitation-on-the-modern-protocol) |
| `client.set_logging_level()` | Supported | Raises — `logging/setLevel` is absent from the era's registry |
| Background tasks (`task=True`) | Runs synchronously — never tasked | Supported via the tasks extension |
Tools that rely on `ctx.elicit` or `ctx.list_roots` continue to work against clients on the session-based eras. On the modern era, elicitation is reachable through the multi-round "guard" pattern instead (a tool returns an `InputRequiredResult`; see the New entry below). Sampling is the exception: it is deprecated on every era and will not return on modern connections (see the Deprecated entry below).
Tools that rely on `ctx.elicit` continue to work against clients on the session-based eras; on the modern era, elicitation is reachable through the multi-round "guard" pattern instead (a tool returns an `InputRequiredResult`; see the New entry below). Sampling and roots have no era row to speak of — they left the server API entirely (see the Removed entry below).
Ordinary `ctx.info` usage emits an SDK-level `MCPDeprecationWarning` ("The logging capability is deprecated as of 2026-07-28 (SEP-2577)"). That warning comes from the SDK, not FastMCP, and is benign — logging keeps working on session-based connections per the matrix. `ctx.sample`/`ctx.sample_step` additionally emit a FastMCP-owned `FastMCPDeprecationWarning` (see below). The upgrade guide calls both out explicitly.
Ordinary `ctx.info` usage emits an SDK-level `MCPDeprecationWarning` ("The logging capability is deprecated as of 2026-07-28 (SEP-2577)"). That warning comes from the SDK, not FastMCP, and is benign — logging *notifications* ride the request's own stream and work on every era, including the modern one. The upgrade guide calls it out explicitly.
Wire interop across the transition is verified: a 3.4.3 client against a v4 server and a v4 client against a 3.4.3 server are bidirectionally clean across 9 operations over HTTP (WS2).
*Verify:* `docs/getting-started/upgrading/from-fastmcp-3.mdx` (the published matrix and SDK-warning note), `tests/server/test_protocol_eras.py`.
### Sampling deprecated, era-gated — Deprecated
### Server-initiated sampling and roots removed from the server API — Breaking
`ctx.sample()` and `ctx.sample_step()` are deprecated and slated for removal in a future FastMCP release. Server-initiated sampling relies on the `createMessage` back-channel that SEP-2577 removed from the wire as of `2026-07-28`, and unlike elicitation it has no multi-round-trip replacement (the agentic loop would exhaust the round-trip budget). Both methods now emit a `FastMCPDeprecationWarning` once per process (gated on `settings.deprecation_warnings`), and on a `2026-07-28` connection they raise a clear `ToolError` before touching the wire. The client-side sampling handler infrastructure (anthropic/openai/google_genai) is retained for future MRTR work and is not deprecated. The migration is to call an LLM directly from your server rather than borrowing the client's model.
FastMCP 4 is a modern MCP toolkit, so the capabilities the modern protocol removed are not in its server-authoring API. `Context.sample()`, `Context.sample_step()`, and `Context.list_roots()` are gone, along with the whole `fastmcp/server/sampling/` package (`SamplingTool`, `SampleStep`, `SamplingResult`, the tool loop, structured-result sampling) and the server-side handler arguments `FastMCP(sampling_handler=..., sampling_handler_behavior=...)`. These were previously deprecated-and-era-gated; they are now absent. Calling them raises `AttributeError`; the constructor kwargs raise a `TypeError` naming SEP-2577 and the migration.
The dead TODO at `server/context.py` (a background-task sampling relay that was never built) is removed: that relay is not being built, so the note is gone rather than left as a promise.
The motivating failure is that the gate had become the default experience. `Client` now defaults to `mode="auto"`, which negotiates `2026-07-28` against a FastMCP server, so an unmodified `ctx.sample()` server failed on an ordinary client connection. Four shipped examples (`examples/sampling/`) were broken by that flip; they are deleted rather than ported, and remain available on `release/3.x`.
*Verify:* `fastmcp_slim/fastmcp/server/context.py` (`_warn_sampling_deprecated`, `_is_modern_protocol`, the `sample`/`sample_step` gates), `docs/servers/sampling.mdx` (deprecation banner), `tests/server/test_protocol_eras.py` (warning + era-gate tests).
Server-initiated sampling and roots are *requests* — the server sends one and blocks for the answer — which needs a back-channel the sessionless protocol does not have. What the protocol removed is the *pushing*, not the asking: both capabilities remain reachable through the guard pattern, where a tool returns an `InputRequiredResult` whose `input_requests` map carries a `CreateMessageRequest` or a `ListRootsRequest`, the client answers it, and the tool re-runs and reads `ctx.input_responses`. `Client._drive_input_required()` dispatches those to the same `sampling_handler` / `roots` handler a handshake-era server would have pushed to, and `tests/conformance/server.py` exercises both routes. For roots that guard round is the recommended modern path. For generation it is available but usually the wrong tool — each round is a full request-response cycle, so an agentic loop exhausts the round-trip budget — and the recommended migration stays a direct LLM call from the server.
**What is deliberately kept.** Client-side `Client(sampling_handler=..., roots=...)` and the provider handlers (anthropic/openai/google_genai) stay: a FastMCP client must still answer a legacy server's requests, and removing them would break interop with older servers. `docs/clients/sampling.mdx` and `docs/clients/roots.mdx` stay as real documentation. Logging is untouched — `ctx.log`/`info`/`debug`/`warning`/`error` are notifications that ride the request's own stream and work on every era.
**Proxy relay.** `ProxyClient`'s default `roots` and `sampling_handler` are client-side handlers that relay a handshake-era backend's requests to the proxy's own front client. They are kept, because a proxy is a client to its backend and falls squarely under the interop guarantee above. They no longer route through the removed `Context` methods: both now call the SDK session directly (`ctx.session.list_roots()` / `ctx.session.create_message()`), an internal path with no public authoring surface. The relay is reachable only when both legs speak the handshake era.
*Verify:* `fastmcp_slim/fastmcp/server/context.py` (no `sample`/`sample_step`/`list_roots`), `fastmcp_slim/fastmcp/server/server.py` (`_REMOVED_KWARGS`), `fastmcp_slim/fastmcp/server/providers/proxy.py` (`default_proxy_roots_handler`, `default_proxy_sampling_handler`), `docs/servers/sampling.mdx` (rewritten in place as the explainer), `tests/server/test_protocol_eras.py` (`test_removed_server_initiated_methods_are_absent`), `tests/server/providers/proxy/test_proxy_client.py` (relay still green).
### `client.set_logging_level()` era-gated — Breaking (modern era)
`logging/setLevel` asks a server to remember a level for the rest of the session, and it is absent from the `2026-07-28` method registry because that era has no session to remember it in. It previously surfaced the SDK's opaque "Method not found". `Client.set_logging_level()` now raises a `RuntimeError` naming the era and pointing at level-filtering in the client's `log_handler`; it is unchanged on handshake-era connections. It is never a silent no-op.
*Verify:* `fastmcp_slim/fastmcp/client/client.py` (`set_logging_level`), `tests/server/test_protocol_eras.py` (`test_set_logging_level_is_era_gated_on_modern`).
### Push-feature degradation quality — Resolved (was sdk-feedback #10)
On a `2026-07-28` connection the degradation error used to differ by feature: `ctx.list_roots` raised a clear `NoBackChannelError`, while `ctx.elicit` / `ctx.sample` surfaced a bare "Method not found" because those methods attach a `related_request_id` and reach client dispatch before failing. FastMCP now era-gates `ctx.elicit` and `ctx.sample`/`ctx.sample_step` to raise a clear, era-aware `ToolError` before the wire ("server-initiated sampling is not available on MCP 2026-07-28 connections…" and "elicitation via server-initiated requests is unavailable on 2026-07-28 connections."). The strict xfail that captured #10 is flipped to a passing test.
On a `2026-07-28` connection `ctx.elicit` used to surface a bare "Method not found", because it attaches a `related_request_id` and reaches client dispatch before failing. FastMCP now era-gates `ctx.elicit` to raise a clear, era-aware `ToolError` before the wire ("elicitation via server-initiated requests is unavailable on 2026-07-28 connections."). The strict xfail that captured #10 is flipped to a passing test. The sampling half of #10 is moot: `ctx.sample` no longer exists.
*Verify:* `tests/server/test_protocol_eras.py` (`test_elicit_sample_degradation_message_is_clear_on_modern`, now a real test), `server/context.py` (era gates).
*Verify:* `tests/server/test_protocol_eras.py` (`test_elicit_degradation_message_is_clear_on_modern`, now a real test), `server/context.py` (era gate).
### Server-level cache hints (SEP-2549) — New (opt-in feature)
@ -376,13 +498,37 @@ A FastMCP server can emit SEP-2549 freshness hints so a caching client (`fastmcp
### Elicitation on the modern protocol (SEP-2322), guard form — New (opt-in feature)
A tool can gather client input across rounds on a `2026-07-28` call by returning an `InputRequiredResult` (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)). Each round is a complete request→response cycle: the tool re-runs per round and reads the client's answers off two new `Context` properties, `ctx.input_responses` (`None` on the first round) and `ctx.request_state` (the echoed opaque state) — thin passthroughs matching the SDK's mcpserver semantics. This is the modern-era elicitation path the earlier per-feature matrix flagged as "MRTR rewrite pending"; it mirrors the SDK's base guard model exactly (tool re-runs, checks whether answers are present, returns to ask for more), with no FastMCP-invented resolver or annotation layer. `fastmcp.types` gains `InputRequiredResult`, `ElicitRequest`, and `ElicitRequestFormParams` for authoring these requests. The `request_state` channel is sealed by the framework, not the author: FastMCP installs the SDK's `RequestStateBoundary` middleware on its low-level server, which seals every outgoing `request_state` and unseals and verifies every inbound echo before a tool runs — so a tool only ever sees plaintext and a tampered, expired, or foreign token is rejected with a frozen wire error. `FastMCP(request_state_security=RequestStateSecurity(keys=[...]))` supplies shared keys for multi-replica deployments; omitted, each process seals under an ephemeral key (correct single-process). Returning this result on a handshake-era (≤ 2025-11-25) connection raises a clear era error naming the mismatch rather than failing as a generic invalid result. The client half (`fastmcp.Client` at `mode="auto"`) drives the loop through its existing elicitation/sampling/roots handlers, capped by `input_required_max_rounds`.
A tool can gather client input across rounds on a `2026-07-28` call by returning an `InputRequiredResult` (see [Elicitation on the modern protocol](https://gofastmcp.com/servers/elicitation#elicitation-on-the-modern-protocol)). Each round is a complete request→response cycle: the tool re-runs per round and reads the client's answers off two new `Context` properties, `ctx.input_responses` (`None` on the first round) and `ctx.request_state` (the echoed opaque state) — thin passthroughs matching the SDK's mcpserver semantics. This is the modern-era elicitation path the earlier per-feature matrix flagged as "MRTR rewrite pending"; it mirrors the SDK's base guard model exactly (tool re-runs, checks whether answers are present, returns to ask for more), with no FastMCP-invented resolver or annotation layer. For authoring these requests, `InputRequiredResult`, `ElicitRequest`, and `ElicitRequestFormParams` import from `mcp_types`. The `request_state` channel is sealed by the framework, not the author: FastMCP installs the SDK's `RequestStateBoundary` middleware on its low-level server, which seals every outgoing `request_state` and unseals and verifies every inbound echo before a tool runs — so a tool only ever sees plaintext and a tampered, expired, or foreign token is rejected with a frozen wire error. `FastMCP(request_state_security=RequestStateSecurity(keys=[...]))` supplies shared keys for multi-replica deployments; omitted, each process seals under an ephemeral key (correct single-process). Returning this result on a handshake-era (≤ 2025-11-25) connection raises a clear era error naming the mismatch rather than failing as a generic invalid result. The client half (`fastmcp.Client` at `mode="auto"`) drives the loop through its existing elicitation/sampling/roots handlers, capped by `input_required_max_rounds`.
*Verify:* `fastmcp_slim/fastmcp/server/context.py` (`input_responses`/`request_state` properties), `fastmcp_slim/fastmcp/server/low_level.py` (`RequestStateBoundary` install), `fastmcp_slim/fastmcp/server/server.py` (`request_state_security` param), `fastmcp_slim/fastmcp/server/mixins/mcp_operations.py` (`_on_call_tool` input-required passthrough + era gate), `fastmcp_slim/fastmcp/tools/base.py` (`InputRequiredToolResult`), `tests/server/test_mrtr_guards.py`.
### Proxy era mirroring — New (behavior)
A proxy is a server on its front and a client on its back, and the two eras have mutually exclusive interaction models on a single session: the handshake era pushes server-initiated requests (sampling/elicitation/roots) that the proxy forwards to its client, while the modern era forbids those and round-trips a guard tool's `InputRequiredResult` as a result instead. A proxy created from a non-Client target with no explicit `mode` now MIRRORS the front connection's negotiated era onto its backend session per request, so the whole chain speaks one era end-to-end — a modern client reaches a modern backend (guard round-trips work), a handshake client reaches a handshake backend (push-forwarding works), and the same proxy serves both without a backend session ever crossing eras. Because the default factory builds a fresh backend client per request and derives its `mode` from the front era at call time, only the metadata-only component caches are shared across eras. An explicit `create_proxy(target, mode=...)` still pins the backend era regardless of the front, overriding mirroring for a backend that only speaks one era; the resulting cross-era feature mismatches surface through the existing era gates. `ProxyInitializeMiddleware` no longer force-calls the handshake-only `client.initialize()` when the backend negotiated the modern era, so an explicit modern pin behind a handshake front no longer crashes on connect. The mirrored era carries through a multi-server `MCPConfig` target as well: that form mounts one proxy per configured server onto a composite router, and `TransportOptions.backend_mode` hands the era down to those mounted legs so every real backend negotiates it, not just the router in front of them. That router is also now 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.
*Verify:* `fastmcp_slim/fastmcp/server/providers/proxy.py` (`_mirror_front_era_mode`, the `_create_client_factory` non-Client branch, the era guard in `ProxyInitializeMiddleware.on_initialize`), `fastmcp_slim/fastmcp/client/transports/base.py` (`TransportOptions.backend_mode`), `fastmcp_slim/fastmcp/client/transports/config.py` (`MCPConfigTransport.connect_session` / `_create_proxy`), `fastmcp_slim/fastmcp/server/server.py` (`create_proxy` docstring), `tests/server/test_mrtr_guards.py` (`TestProxyEraMirroring`, `TestMultiServerConfigEraMirroring`).
### Resource and prompt errors survive the modern era — Absorbed (defect fix)
`_on_call_tool` returns a `ResourceError`-equivalent as an error result, but `_on_read_resource` and `_on_get_prompt` caught only `DisabledError`/`NotFoundError`, so a `ResourceError`, `PromptError`, or an argument-conversion failure on a resource template escaped as a raw handler exception. On the handshake eras that reached the wire as `str(exc)`, which is survivable; on `2026-07-28` the runner masks anything that is not an `MCPError` or `ValidationError` as a generic `"Internal server error"`, so a legitimate client-input error became indistinguishable from a server bug. Both handlers now translate a `FastMCPError` through `to_mcp_error` the way tools already do. Masking is unchanged — `mask_error_details` is still applied inside `read_resource`/`render_prompt`, so these paths leak no more than tools do.
*Verify:* `fastmcp_slim/fastmcp/server/mixins/mcp_operations.py` (`_on_read_resource`, `_on_get_prompt`), `tests/server/test_protocol_eras.py`.
### Proxies forward upstream instructions on the modern era — Absorbed (defect fix)
`ProxyInitializeMiddleware` forwards an upstream server's `instructions` by patching the `InitializeResult`, but `on_initialize` only fires for the handshake era. A modern client negotiates via `server/discover`, which the SDK builds from the low-level server's own `instructions`, so a proxy silently dropped its upstream's instructions for every modern client. `FastMCPProxy` now registers a `server/discover` handler (the same `add_request_handler` hook it already uses for `ping`, and a replacement the SDK explicitly sanctions) that delegates to the SDK's own implementation and fills in only the instructions that would otherwise be lost. The proxy's lazy-connect contract is unchanged: the backend is contacted when a client asks, never at construction. Because era mirroring pins a modern backend to an exact version — and a pinned version adopts a synthesized `DiscoverResult` rather than probing the wire — this read negotiates with `mode="auto"`; instructions are metadata with no back-channel, so they do not need the era consistency mirroring exists to protect.
*Verify:* `fastmcp_slim/fastmcp/server/providers/proxy.py` (`FastMCPProxy._setup_proxy_discover_handler`), `tests/server/providers/proxy/test_proxy_server.py` (`TestProxyModernEraInstructions`).
### Proxy list methods raise `MCPError` on backend failure — Breaking (in-process error type)
`ProxyProvider`'s four `_list_*` methods caught only `MCPError`, so a failed backend connection escaped as the `RuntimeError` the client wraps it in (or a raw `httpx2.ConnectError`). On the handshake eras that reached the wire as `str(exc)` and named the real failure; on `2026-07-28` it was masked as `"Internal server error"`, leaving a modern client unable to tell a dead backend from a server bug. The list methods now normalize transport failures through `_proxy_upstream_error`, matching `ProxyInitializeMiddleware.on_initialize`. Code calling a proxy's `list_tools()` (and friends) in-process must now catch `MCPError` rather than `RuntimeError`; the over-the-wire error type is unchanged.
*Verify:* `fastmcp_slim/fastmcp/server/providers/proxy.py` (`_PROXY_TRANSPORT_ERRORS` and the four `_list_*` methods), `tests/server/providers/proxy/test_proxy_server.py` (`TestProxyProviderTransportErrors`).
### The xfail register — Known gap
Roughly forty `xfail` markers across the test tree (concentrated in `tests/server/tasks/`, `tests/client/tasks/`, and `test_protocol_eras.py`) are the built-in beta tracker: each names the SDK gap it waits on. They are enumerated and mapped to sdk-feedback findings on the [Known Gaps](/development/v4-notes/known-gaps) page.
Roughly forty `xfail` markers across the test tree (concentrated in `tests/server/tasks/`, `tests/client/tasks/`, and `test_protocol_eras.py`) are the built-in beta tracker: each names the SDK gap it waits on. They are enumerated and mapped to sdk-feedback findings on the [Known Gaps](known-gaps.md) page.
## Security
@ -396,7 +542,7 @@ FastMCP keeps its own DCR redirect-URI hardening (PRs #4419, #4408) regardless o
### Identity assertion (SEP-990 ID-JAG) — Added (beta)
`OAuthProxy` (and `OIDCProxy`, which inherits it) accepts an optional `identity_assertion=IdentityAssertion(trusted_issuers=[...])`. When configured, the token endpoint accepts the RFC 7523 `urn:ietf:params:oauth:grant-type:jwt-bearer` grant carrying an enterprise IdP-issued ID-JAG, validates it (signature against the trusted issuer's JWKS, `iss`/`aud`/`exp`, `typ` of `oauth-id-jag+jwt`, mandatory `sub`, signed `client_id`/`resource` binding, and `jti` replay rejection), and mints a short-lived FastMCP access token carrying the asserted subject with no refresh token. Authorization server metadata advertises the `jwt-bearer` grant type and the `urn:ietf:params:oauth:grant-profile:id-jag` profile when enabled. This is server-side only; the client-side wrapper ships separately. See [Identity Assertion](/servers/auth/oauth-proxy#identity-assertion-sep-990).
`OAuthProxy` (and `OIDCProxy`, which inherits it) accepts an optional `identity_assertion=IdentityAssertion(trusted_issuers=[...])`. When configured, the token endpoint accepts the RFC 7523 `urn:ietf:params:oauth:grant-type:jwt-bearer` grant carrying an enterprise IdP-issued ID-JAG, validates it (signature against the trusted issuer's JWKS, `iss`/`aud`/`exp`, `typ` of `oauth-id-jag+jwt`, mandatory `sub`, signed `client_id`/`resource` binding, and `jti` replay rejection), and mints a short-lived FastMCP access token carrying the asserted subject with no refresh token. Authorization server metadata advertises the `jwt-bearer` grant type and the `urn:ietf:params:oauth:grant-profile:id-jag` profile when enabled. This is server-side only; the client-side wrapper ships separately. See [Identity Assertion](https://gofastmcp.com/servers/auth/oauth-proxy#identity-assertion-sep-990).
*Verify:* `fastmcp_slim/fastmcp/server/auth/identity_assertion.py`, the `exchange_identity_assertion` and `get_routes` changes in `fastmcp_slim/fastmcp/server/auth/oauth_proxy/proxy.py`, and the jwt-bearer dispatch in `fastmcp_slim/fastmcp/server/auth/auth.py` (`TokenHandler._maybe_handle_id_jag`).
@ -404,7 +550,7 @@ FastMCP keeps its own DCR redirect-URI hardening (PRs #4419, #4408) regardless o
Every templated resource now has its extracted parameter values screened for path-traversal (`..` segments), absolute paths, and null bytes **before the handler runs** — on by default, at the server's read chokepoint, covering local and provider-sourced (mounted/proxied) templates alike. Previously these payloads reached handlers raw; a template whose parameter flowed into a filesystem path or upstream URL was exposed unless the author added their own check. A rejected read now surfaces a non-leaky "resource not found" error (`-32602`) and a debug log.
The check is component-based, matching the SDK's `contains_path_traversal`: only a standalone `..` segment is traversal, so values that merely contain dots (`HEAD~3..HEAD`, `file.tar.gz`) and dotfiles (`.env`) still pass. This can break a template that legitimately accepts `..`-bearing or absolute values — exempt the parameter with `ResourceSecurity(exempt_params={...})`, disable per-component with `security=None`, or set a server-wide default with `FastMCP(resource_security=...)`. See [Resources → Path Security](/servers/resources#path-security).
The check is component-based, matching the SDK's `contains_path_traversal`: only a standalone `..` segment is traversal, so values that merely contain dots (`HEAD~3..HEAD`, `file.tar.gz`) and dotfiles (`.env`) still pass. This can break a template that legitimately accepts `..`-bearing or absolute values — exempt the parameter with `ResourceSecurity(exempt_params={...})`, disable per-component with `security=None`, or set a server-wide default with `FastMCP(resource_security=...)`. See [Resources → Path Security](https://gofastmcp.com/servers/resources#path-security).
*Verify:* `fastmcp_slim/fastmcp/resources/security.py` (`ResourceSecurity`), the screening block in `FastMCP.read_resource` (`fastmcp_slim/fastmcp/server/server.py`), and `tests/resources/test_resource_security.py`.
@ -437,9 +583,13 @@ The `_REMOVED_KWARGS` constructor shim (which raises helpful `TypeError`s for kw
### Tool and component parameters
- **Tool-level `serializer` parameter** — removed from `@tool` / `mcp.tool()`, `Tool.from_function`, `Tool.from_tool`, `TransformedTool.from_tool`, the OpenAPI `OpenAPITool`, and the `mcp_mixin` tool decorator. Return a `ToolResult` from your tool for full control over serialization instead (see [Custom Serialization](/servers/tools#custom-serialization)). The server-level `tool_serializer` constructor kwarg was already removed in 3.0.
- **Tool-level `serializer` parameter** — removed from `@tool` / `mcp.tool()`, `Tool.from_function`, `Tool.from_tool`, `TransformedTool.from_tool`, the OpenAPI `OpenAPITool`, and the `mcp_mixin` tool decorator. Return a `ToolResult` from your tool for full control over serialization instead (see [Custom Serialization](https://gofastmcp.com/servers/tools#custom-serialization)). The server-level `tool_serializer` constructor kwarg was already removed in 3.0.
- **Tool `exclude_args` parameter** — removed from the tool decorator and its plumbing (`ParsedFunction.from_function`, `Tool.from_function`, `mcp.tool()`). Use dependency injection with `Depends()` to hide parameters from the tool schema instead.
- **`decorator_mode` setting** (`FASTMCP_DECORATOR_MODE`) and its `"object"` mode — removed. Decorators always return the original function with metadata attached; the object-returning machinery is gone. Access component objects through the server (e.g. `await mcp.get_tool("name")`) rather than the decorated function.
- **Component-import compatibility shims** — the `__getattr__` shims that re-exported `FunctionTool` / `ParsedFunction` / `tool` from `fastmcp.tools.tool`, `FunctionResource` / `resource` from `fastmcp.resources.resource`, and `FunctionPrompt` / `prompt` from `fastmcp.prompts.prompt` are removed. Import these from their canonical modules (`fastmcp.tools.function_tool`, `fastmcp.resources.function_resource`, `fastmcp.prompts.function_prompt`) instead.
- **Component-import compatibility shims** — Breaking. `fastmcp.tools.tool`, `fastmcp.resources.resource`, and `fastmcp.prompts.prompt` no longer exist as modules. Two separate mechanisms kept them alive and both are now gone: the `__getattr__` shims that re-exported `FunctionTool` / `ParsedFunction` / `tool`, `FunctionResource` / `resource`, and `FunctionPrompt` / `prompt`; and the `sys.modules` aliases that pointed each old module name at its renamed `base.py`. Import the component types from the package itself — `from fastmcp.tools import Tool, ToolResult` — and the function-backed classes from their canonical modules (`fastmcp.tools.function_tool`, `fastmcp.resources.function_resource`, `fastmcp.prompts.function_prompt`).
- **`fastmcp.experimental.sampling`** and **`fastmcp.experimental.sampling.handlers`** (2.x-era re-export shims) — Breaking. These aliased the client-side sampling handlers without warning. Import from `fastmcp.client.sampling.handlers.openai` instead. Note this is unrelated to the SEP-2577 removal of *server-initiated* sampling: a FastMCP client still answers a legacy-era server's sampling requests, so `Client(sampling_handler=...)` and the Anthropic / OpenAI / Google GenAI handlers under `fastmcp.client.sampling.handlers` remain fully supported.
- **`fastmcp.server.auth.authorization`** (3.0-era re-export shim) — Breaking. The module was a pass-through sitting between the `fastmcp.server.auth` package and the real implementation in `fastmcp.utilities.authorization`, and FastMCP's own middleware and local-provider decorators imported through it. Everything internal now imports from `fastmcp.utilities.authorization` directly. The documented public path is unchanged: `from fastmcp.server.auth import require_scopes, require_roles, restrict_tag, run_auth_checks, AuthCheck, AuthContext`. Two names the old module also exported — `run_auth_checks_with_shortfall` and `scope_requirements` — are *not* re-exported from `fastmcp.server.auth` and must be imported from `fastmcp.utilities.authorization`. They are middleware plumbing with no documented user-facing use, so they were deliberately not widened onto the auth package's surface; the upgrade guide names the utilities path for them explicitly.
- **`SkillsProvider`** (3.0-era rename alias) — Breaking. Use `SkillsDirectoryProvider` from `fastmcp.server.providers.skills`. The alias was also re-exported from `fastmcp.server.providers`; both are gone.
- **`ctx.elicit()` without `response_type`** (deprecated 3.2, warned through 3.4.4) — Breaking. The parameter is now required, and passing `None` explicitly raises `TypeError`. The empty-object schema it produced was ambiguous under the MCP spec and left some clients (e.g. VS Code) rendering an empty, non-functional form. Pass a type describing the data you expect back; `bool` covers confirmations. This is the server-authoring API only — the *client* elicitation handler still receives `response_type=None` for URL requests and for empty schemas sent by other servers, which is unchanged.
*Verify:* deletions of `fastmcp_slim/fastmcp/server/proxy.py`, `fastmcp_slim/fastmcp/server/openapi/`, `fastmcp_slim/fastmcp/experimental/server/openapi/`, `fastmcp_slim/fastmcp/experimental/utilities/openapi/`, `fastmcp_slim/fastmcp/server/apps.py`, `fastmcp_slim/fastmcp/server/app.py`; the removed classes in `fastmcp_slim/fastmcp/server/middleware/tool_injection.py`; the removed parameter in `fastmcp_slim/fastmcp/client/transports/http.py`; `fastmcp_slim/fastmcp/server/server.py`; `fastmcp_slim/fastmcp/tools/base.py`, `tools/function_tool.py`, `tools/tool_transform.py`, `tools/function_parsing.py`; `fastmcp_slim/fastmcp/settings.py`, `resources/function_resource.py`, `prompts/function_prompt.py`, and the local-provider decorators; `resources/base.py`, `prompts/base.py`.

View file

@ -0,0 +1,140 @@
---
title: Feature Program
---
The migration is the foundation. The forward v4 program is a sequence of post-merge PRs that build on it. Several have now merged. Each feature below carries an explicit status:
- **Shipped** — merged to `main`, with the PR cited.
- **Designed** — the approach is settled and an API sketch exists; implementation has not started.
- **Planned** — the shape is agreed but design details remain open.
- **Not started** — identified as v4 scope, not yet designed.
Code blocks marked as sketches show the *intended* API and do not resolve against the current tree.
## Sampling removal
**Status: Shipped in 4.0.**
Sampling was the push-shaped API where a server borrows the client's model mid-call (`ctx.sample`, `ctx.sample_step`). The `2026-07-28` era removes server-initiated requests, so it cannot work on modern connections, and `Client`'s flip to `mode="auto"` made a modern connection the default — the era gate had become the default experience rather than an edge case. Background-task sampling was dead under v2 in any event: a worker's back-channel is gone once the submitting request returns, and no relay was ever built (sdk-feedback #9).
Deprecation and era-gating shipped in #4448. The removal completes the plan: `ctx.sample`, `ctx.sample_step`, `ctx.list_roots`, `server/sampling/` (including `SamplingTool` and structured-result sampling), `FastMCP(sampling_handler=..., sampling_handler_behavior=...)`, and `examples/sampling/` are all gone. The server-authoring API is now the modern protocol's API, with nothing in it that only works against old clients.
The migration story is honest: there is **no drop-in**. The guidance is architectural — call an LLM from your server directly, with your own API key, rather than borrowing the client's model. For roots, take paths as tool arguments or ask through the guard pattern, whose `input_requests` map still carries a `ListRootsRequest`.
The client-side provider handlers (Anthropic, OpenAI, Google GenAI) and `Client(sampling_handler=..., roots=...)` are **retained**: a FastMCP client still has to answer a legacy server's requests, and MRTR needs them from the client side. What is removed is the server-side push emitter. `ProxyClient`'s default relay handlers are retained for the same interop reason and now call the SDK session directly.
## MRTR elicitation
**Status: Guard form shipped (4.0). Declarative `Resolve` layer designed.**
Elicitation survives the modern era through multi-round-trip (MRTR). The 2026 wire envelope carries elicitation as a multi-round input-request: a tool returns an `InputRequiredResult` and re-runs per round, each round a complete request→response cycle. Imperative `ctx.elicit` relies on the session back-channel, which is gone on `2026-07-28` foreground calls; on the modern era, elicitation is reachable through MRTR instead.
The **guard form** of this is shipped in 4.0 (see [Elicitation on the modern protocol](https://gofastmcp.com/servers/elicitation#elicitation-on-the-modern-protocol)): a tool returns an `InputRequiredResult` and reads the client's answers off `ctx.input_responses` / `ctx.request_state`, re-running each round. It mirrors the SDK's base guard model exactly — no FastMCP-invented DX, the framework owns `request_state` sealing, and returning this result on a handshake-era connection produces a clear era error.
What remains is the declarative `Resolve(...)` layer that sits *on top of* that shipped primitive. It is designed, not built: a new `fastmcp.elicitation` module — `Resolve`, `Elicit`, and `ElicitationResult` — thin wrappers over the SDK's resolver, wired into FastMCP's own tool layer (FastMCP tools do not inherit the SDK's auto-resolver wiring). It would detect `Annotated[_, Resolve(...)]` parameters, build resolver plans, and return the SDK's `InputRequiredResult` instead of the tool body on the first round.
Imperative `ctx.elicit` is **not** re-plumbed to survive the modern era. It works on the legacy eras through the session back-channel, and on `2026-07-28` foreground calls it is era-gated to raise a clear error (shipped in #4448) pointing at the guard form. The earlier plan to keep imperative `ctx.elicit` alive on modern connections through a background-task relay is dead twice over: the guard model shipped in its place, and the 2025 task machinery the relay depended on is slated for removal (see [Known Gaps](known-gaps.md#the-xfail-register)).
The intended declarative DX (sketch — the module does not exist yet):
```python test="skip"
from typing import Annotated
from pydantic import BaseModel
from fastmcp import FastMCP, Context
from fastmcp.elicitation import Resolve, Elicit, ElicitationResult
mcp = FastMCP("shipping")
class Address(BaseModel):
street: str
city: str
zip: str
async def ask_address(ctx: Context) -> Elicit[Address]:
return Elicit("Where should we ship this order?", Address)
@mcp.tool
async def create_shipment(
order_id: str,
address: Annotated[Address, Resolve(ask_address)], # unwrapped; decline -> ToolError
) -> str:
return f"Shipping {order_id} to {address.city}"
@mcp.tool
async def maybe_ship(
order_id: str,
address: Annotated[ElicitationResult[Address], Resolve(ask_address)], # full outcome
) -> str:
if address.action != "accept":
return "cancelled"
return f"Shipping {order_id} to {address.data.city}"
```
The FastMCP client already dispatches input-requests through its elicitation callback; the remaining declarative work confirms the FastMCP client drives the input-required driver the way the SDK's own client does.
The divergence between elicitation and sampling on 2026 comes down to one fact: the SDK built the server-side emitter for elicitation (`Elicit`/`Resolve`) and not for sampling. The wire carries all three input-request types and the client dispatches all three; only elicitation can produce one server-side. That is why elicitation survives 4.0 via MRTR and push-sampling does not.
## Middleware root dispatch
**Status: Shipped (#4553).**
The migration already routed `initialize` interception through the SDK's `ServerMiddleware` list via `FastMCPServerMiddleware`. #4553 made that entry the root of middleware dispatch: FastMCP's method-agnostic hooks (`on_message`, `on_request`, `on_notification`) now fire for every inbound message — client cancellations, progress notifications, and requests that fail routing or validation — not only the ones that reach a component handler. The component methods keep running their own chain interior, and a method set plus a dispatch flag keep the two passes disjoint so each hook fires exactly once per message.
## First-class 2026 client
**Status: Partly shipped (#4572, #4574); full composition blocked upstream.**
`fastmcp.Client` now defaults to `mode="auto"` (#4572): it probes `server/discover`, falls back to the classic handshake, and answers multi-round-trip `input_required` requests through its existing handlers. The same PR surfaced `extensions=` and `result_claims=` (SEP-2133). The client also dropped its forked protocol helpers — extension folding, the evicting message handler, discover synthesis — in favor of the SDK's own (#4574).
The decision here was **compose, not wrap** (D16): rebuild `fastmcp.Client` on the SDK's high-level `mcp.Client` rather than wrapping `mcp.ClientSession`. The parts that compose cleanly have shipped. The rest is **blocked upstream on two counts**. First, `mcp.Client` constructs its `ClientSession` at a single hardcoded site with no injection hook, while FastMCP's `session_class` is load-bearing (`ProxyClient` substitutes a session that skips result validation so a backend's schema violation surfaces at the end client rather than becoming a proxy error) — a `session_factory=` hook on `mcp.Client`, the same shape as the `notification_bindings=` parameter added earlier, would solve this. Second, `mcp.Client.__aenter__` refuses reentry, but FastMCP's client is deliberately reentrant (its refcounted context manager exists to fix a proxy session-reuse deadlock), so the rebuild also needs the SDK client to tolerate reentrant entry. Both must land upstream before the full rebuild is possible; `session_factory=` alone is necessary but not sufficient.
This workstream also owns the server-side statelessness design holes — `ctx.session_id` / `set_state` round-tripping and stateful-proxy affinity — since they turn on the same "what is a session without a session?" question. See [Statelessness on 2026-07-28](known-gaps.md#statelessness-on-2026-07-28) for the full accounting.
## Subscriptions, cache hints, extensions, OTel
**Status: Mixed — cache hints and OTel shipped; subscriptions not started.**
A cluster of protocol features tracked for v4. Their statuses have diverged:
- **Cache hints — shipped (#4464).** Server-level authoring (`FastMCP(cache_ttl=..., cache_scope=...)`, SEP-2549) stamps every cacheable result, and the FastMCP client honors hints with an opt-in response cache.
- **OpenTelemetry — shipped (#4481).** Spans are on by default (a no-op without an exporter), with SDK-aligned attributes and a `FASTMCP_TELEMETRY_MODE` setting (`native` / `propagation_only` / `off`).
- **Extensions — client side shipped (#4572).** `Client(extensions=..., result_claims=...)` advertises opt-in client extensions (SEP-2133). The server side is a Designed workstream in its own right (see [FastMCP-native extension API](#fastmcp-native-extension-api)). The cross-era reconciliation of the `extensions` / MCP Apps capability advertisement is still open (the capability is stripped at pre-2026 negotiated versions — sdk-feedback #2).
- **Subscriptions — not started.** A `subscriptions/listen` surface backed by a subscription bus.
## FastMCP-native extension API
**Status: Shipped (#4602).**
MCP extensions (SEP-2133) are optional, capability-negotiated protocol features identified by a reverse-DNS string — `io.modelcontextprotocol/ui` (MCP Apps), `io.modelcontextprotocol/tasks` (SEP-2663). They are a genuinely new abstraction in SDK v2; they did not exist in v1. The SDK exposes them through an `Extension` server class that contributes a capability, additive request methods, and a `tools/call` interceptor, plus a symmetric `ClientExtension` with result claims and notification bindings.
FastMCP already forwards `ClientExtension` natively (`Client(extensions=...)`, #4572). The **server** side does not use the SDK's `Extension` class at all: MCP Apps predates the abstraction, so FastMCP hand-splices the `ui` capability into `get_capabilities()` on the low-level server and walks tool metadata directly. That worked for one extension, but every new protocol extension currently means bespoke surgery on core.
The Designed work is a FastMCP-native server extension API — a single registration point (`mcp.add_extension(...)`) that contributes a negotiated capability, request methods, and a `tools/call` interceptor, with access to FastMCP-level constructs the SDK's `Extension` withholds (the component registry, `Context`, auth scope). It is designed against the SEP-2663 tasks extension because tasks exercises the full surface — capability *and* methods *and* interception *and* client claims/notifications — where MCP Apps exercises only a subset. Tasks is the pathfinder; MCP Apps migrates onto the extension API as a fast-follow, deleting the hand-rolled splices, and confirms the design generalizes. The discriminator that keeps the extension API distinct from [middleware](https://gofastmcp.com/servers/middleware): an extension is a *negotiated contract change* the client must understand, where middleware is unilateral server behavior the client never sees. Delete a capability advertisement and nothing about the client changes — that is middleware, not an extension.
## Background tasks (SEP-2663)
**Status: Shipped (#4603).**
Background tasks return to the modern era as `fastmcp-tasks`, an in-repo optional package rebuilt on the `io.modelcontextprotocol/tasks` extension (SEP-2663, Final, merged upstream 2026-05-15). SEP-2663 supersedes SEP-1686 but keeps its polling core: a client that advertises the tasks capability issues an augmented `tools/call`; the server decides whether to run it as a task and returns a `CreateTaskResult` carrying a server-generated task id; the client polls `tasks/get` until terminal and reads the result inlined there. FastMCP's existing SEP-1686 wire layer is removed while the Docket/Redis execution engine underneath moves into `fastmcp-tasks` intact — the spec moved toward what FastMCP already built, so the rebuild is mostly deletion plus a thin wire adapter. `task=True` stays the authoring surface (gated by the `fastmcp[tasks]` extra and an explicit `mcp.add_extension(TasksExtension(...))`, the first consumer of the [extension API](#fastmcp-native-extension-api) above), so a server that already uses tasks needs no code change. Scope for v1 is polling-only and `tools/call`-only.
The full design — wire delta, the engine/wire split, packaging, client experience, sequencing, risks, and the five resolved decisions — is on the dedicated [Background Tasks (SEP-2663)](background-tasks.md) page.
## SDK delegation, round two
**Status: Planned (gated on upstream).**
The real HTTP simplification is a v4 project, not this PR. FastMCP can collapse its `create_streamable_http_app` onto the SDK's `Server.streamable_http_app()` once upstream adds three things:
1. per-session event-store scoping,
2. a user-middleware injection hook,
3. a lifespan hook.
The payoff is not only less code — FastMCP would also inherit the SDK's session-owner credential enforcement, a security gain it lacks today. These are the three upstream feature requests to file (alongside the advisory dossier described in [Known Gaps](known-gaps.md)). Until they land, the four HTTP overrides in the [Change Register](change-register.md#http) stay.
One latent capability worth surfacing on FastMCP's side: `session_idle_timeout` is accepted by the manager but never set by `create_streamable_http_app` — a one-line plumb if FastMCP wants to expose it.

View file

@ -4,9 +4,9 @@ title: v4.0 Development Notes
This directory is the working map of FastMCP v4.0: the complete register of user-facing changes from the MCP Python SDK v2 migration ([PR #4437](https://github.com/PrefectHQ/fastmcp/pull/4437)), plus the forward v4 feature program. It plays three roles at once.
1. **A change register.** Every user-visible change from the migration, organized by subsystem, with a note on how FastMCP handles it (absorbed, bridged, breaking, or deprecated) and where to find it in the diff. This is the [Change Register](/development/v4-notes/change-register).
2. **A feature program.** The forward v4 work — sampling removal, MRTR elicitation, the first-class 2026 client, and the SDK-delegation round-two convergence — each with an explicit status. This is the [Feature Program](/development/v4-notes/feature-program). The shipped side of that program — what a v4 deployment provides on the modern protocol today, including the complete server-side SEP-990 identity assertion implementation — is cataloged in [2026-07-28 Protocol Support](/development/v4-notes/protocol-2026).
3. **A review lens.** Because the migration PR is too large to review line by line, the change register is organized so a reviewer can take one subsystem, read its claimed changes, and verify each against the diff. The [Known Gaps](/development/v4-notes/known-gaps) page collects the deliberate xfails and the upstream dependencies that gate the follow-up work.
1. **A change register.** Every user-visible change from the migration, organized by subsystem, with a note on how FastMCP handles it (absorbed, bridged, breaking, or deprecated) and where to find it in the diff. This is the [Change Register](change-register.md).
2. **A feature program.** The forward v4 work — sampling removal, multi-round-trip elicitation, the first-class 2026 client, a FastMCP-native extension API, the SEP-2663 background-tasks rebuild, and the SDK-delegation round-two convergence — now a mix of shipped, designed, and pending. Multi-round-trip guard tools (#4544), the client's `mode="auto"` default with a partial SDK-composition (#4572/#4574, full composition blocked upstream), the extension API (#4602), and background tasks on SEP-2663 (#4603) have shipped; sampling removal and SDK delegation remain ahead. Each carries an explicit status in the [Feature Program](feature-program.md). The shipped side — what a v4 deployment provides on the modern protocol today, including the complete server-side SEP-990 identity assertion implementation — is cataloged in [2026-07-28 Protocol Support](protocol-2026.md).
3. **A review lens.** Because the migration PR is too large to review line by line, the change register is organized so a reviewer can take one subsystem, read its claimed changes, and verify each against the diff. The [Known Gaps](known-gaps.md) page collects the deliberate xfails and the upstream dependencies that gate the follow-up work.
## Why v4 exists
@ -16,22 +16,34 @@ FastMCP v4.0 is an engine swap. Three forces drive the major version:
**Protocol version 2026-07-28.** The SDK v2 serves multiple protocol eras from one server. Alongside the session-based handshake eras, it introduces the sessionless `2026-07-28` era, which discovers capabilities through `server/discover` and removes server-initiated requests (SEP-2577). This formally supersedes FastMCP's earlier "latest protocol only" stance: a single server now works with clients across the protocol transition.
**Sampling removal.** The `2026-07-28` era removes the server's ability to push a request back to the client mid-call. That takes the push-shaped sampling API (`ctx.sample`, `ctx.sample_step`) off the table on modern connections. Rather than leave it half-working, v4 deprecates it now and removes it in the 4.0 release — a real architectural shift for servers that borrowed the client's model, and one that justifies the major bump.
**Sampling and roots removed from the server API.** The `2026-07-28` era removes the server's ability to push a request back to the client mid-call, which takes `ctx.sample`, `ctx.sample_step`, and `ctx.list_roots` off the table. Rather than leave them half-working against old clients only, 4.0 removes them from the server API entirely — a real architectural shift for servers that borrowed the client's model, and one that justifies the major bump. Client-side handlers stay, because a modern client still has to answer a legacy server.
## Release strategy
The migration merges to `main` and development continues there with subsequent PRs. Releases follow the SDK's own beta timeline:
- **`main` carries the beta pins.** While the SDK is on `mcp==2.0.0b1` / `mcp-types==2.0.0b1`, `main` cuts **pre-releases** (`4.0.0b1`, `4.0.0b2`, …). No stable PyPI release goes out until `mcp 2.0.0` reaches GA — at which point the pins swap to the stable SDK and `4.0.0` ships. The pin-swap is a tracked checklist item on the [Known Gaps](/development/v4-notes/known-gaps) page.
- **`main` carries the beta pins.** While the SDK is on `mcp==2.0.0b1` / `mcp-types==2.0.0b1`, `main` cuts **pre-releases** (`4.0.0b1`, `4.0.0b2`, …). No stable PyPI release goes out until `mcp 2.0.0` reaches GA — at which point the pins swap to the stable SDK and `4.0.0` ships. The pin-swap is a tracked checklist item on the [Known Gaps](known-gaps.md) page.
- **`release/3.x` is the maintenance line.** A `release/3.x` branch is cut from pre-merge `main`. It stays on the SDK v1 line, receives upstream security patches, and serves users who cannot move to the SDK v2 beta yet.
### Release codenames
Following the pun-title convention (`v<version>: <pun>`), the v4 line runs a single "four" motif across the whole cycle, holding the headline name for the stable release the way v3 did ("Three at Last" for `3.0.0`, stage puns for its betas):
| Release | Codename | The nod |
| --- | --- | --- |
| `4.0.0a1` (alpha) | **Fourst Contact** | _first contact_ — the first, cautious look at the new engine |
| `4.0.0a2` (alpha) | **Back and Fourth** | _back and forth_ — the second pass, where background tasks and stateless state land |
| `4.0.0b1` (beta) | **Fourgone Conclusion** | _foregone conclusion_ — once the MCP SDK went v2, v4 was inevitable |
| `4.0.0b2` (beta) | **Fourmidable** | _formidable_ — held in reserve for a second beta if one is needed |
| `4.0.0` (stable) | **Fast Fourward** | _fast forward_ — full speed onto the new foundation |
## How to read the register
Each subsystem section in the [Change Register](/development/v4-notes/change-register) tags its changes with one of four dispositions:
Each subsystem section in the [Change Register](change-register.md) tags its changes with one of four dispositions:
- **Absorbed** — the SDK changed underneath, but FastMCP's public surface is identical. Nothing for users to do.
- **Bridged** — a compatibility shim keeps old code working, usually with a `FastMCPDeprecationWarning`. Users should migrate but are not forced to.
- **Breaking** — user code must change. These are the headline migration items.
- **Deprecated** — still works, warns now, slated for removal in a later release.
The user-facing summary of the migration lives in the published [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) guide. These development notes are the exhaustive version behind it.
The user-facing summary of the migration lives in the published [Upgrading from FastMCP 3](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-3) guide. These development notes are the exhaustive version behind it.

View file

@ -6,17 +6,11 @@ The migration ships with a set of deliberate gaps: temporary shims, xfailed test
## The xfail register
Roughly forty `xfail` markers across the test tree are the built-in beta tracker. Each names the SDK gap it waits on, so re-running the suite against a new SDK beta surfaces exactly which gaps have closed (a strict xfail that starts passing fails the suite, prompting removal of the marker). They cluster in three areas.
Roughly forty `xfail` 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/`).** The large majority. These trace to two SDK gaps:
**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)](background-tasks.md)). 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.
- **sdk-feedback #1** — SEP-1686 ships the task result types but omits them from the method registries, so a task-augmented `tools/call` cannot complete validation. FastMCP's `_sdk_patches.py` registry-widening shim covers the common tool path; the xfails cover paths the shim intentionally does not paper over.
- **sdk-feedback #3**`ReadResourceRequestParams` and `GetPromptRequestParams` have no `task` field, so task-augmented resource reads and prompt gets are not wire-expressible. The xfails in `test_task_resources.py`, `test_task_prompts.py`, `test_client_resource_tasks.py`, and `test_client_prompt_tasks.py` carry the reason "SDK v2 has no `task` field on GetPromptRequestParams / ReadResourceRequestParams."
**Protocol eras (`tests/server/test_protocol_eras.py`).** Two strict xfails:
- The strict xfail at `test_protocol_eras.py:319` maps directly to **sdk-feedback #10**: on `2026-07-28`, `ctx.elicit`/`ctx.sample` attach a `related_request_id` and surface a bare "Method not found" rather than a clear era-aware error. It stays strict until the SDK unifies the degradation path or FastMCP era-gates the calls.
- The strict xfail at `test_protocol_eras.py:400` covers the SDK's first-class high-level client (`mcp.client.Client`) and the sessionless driver that the FastMCP client does not yet adopt (see the [first-class 2026 client](/development/v4-notes/feature-program#first-class-2026-client) feature).
**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.
@ -26,14 +20,14 @@ Every shim in the migration is temporary and carries a documented removal trigge
| Shim | Location | Removal trigger |
| --- | --- | --- |
| `_sdk_patches.py` — task registry widening | `fastmcp_slim/fastmcp/_sdk_patches.py` | SDK adds `tasks/*` rows and `CreateTaskResult` to the `tools/call` result union (sdk-feedback #1). |
| `_sdk_patches.py` — task registry widening | `fastmcp_slim/fastmcp/_sdk_patches.py` | Removed with FastMCP's SEP-1686 wire machinery (`server/tasks/`), which is slated for removal now that the 2025 task protocol left the spec. The SEP-2663 rebuild does not need it — `CreateTaskResult` is claimed on `tools/call` through the extensions mechanism, which the SDK registries already admit. |
| `_compat.py` — camelCase field bridge | `fastmcp_slim/fastmcp/_compat.py` | User-migration aid; removed in a future release after users migrate reads to snake_case. Users can preview removal with `mcp_camelcase_compat = False`. |
| `FastMCPRequestContext` ContextVar | `fastmcp_slim/fastmcp/server/dependencies.py` | The SDK deliberately passes context as an argument with no ContextVar; FastMCP's public `get_context()` needs ambient access, and the shim also lifts `_meta`, which the SDK's `TypedDict` drops. No planned removal — this is a permanent boundary, not a beta gap. |
| `FastMCPServerMiddleware` | `fastmcp_slim/fastmcp/server/low_level.py` | Already the native SDK `ServerMiddleware` path; no cleaner hook exists. Permanent. |
| Client `get_session_id` header sniff | `fastmcp_slim/fastmcp/client/transports/http.py` | SDK exposes session id (or an `on_session_created` callback) from `streamable_http_client`, at parity with `sse_client` (sdk-feedback #5). |
| `_sdk_context_shim.py` — generic handler aliases | `fastmcp_slim/fastmcp/client/_sdk_context_shim.py` | The SDK's `ClientRequestContext` is not subscriptable, so FastMCP keeps the public generic `SamplingHandler`/`RootsHandler`/`ElicitationHandler` aliases. Permanent unless the SDK makes the context subscriptable (sdk-feedback #7). |
The `TaskNotificationHandler` binding (sdk-feedback #8) is the client-side equivalent: it registers a `NotificationBinding` for `notifications/tasks/status` because the SDK no longer tees custom server notifications to the message handler.
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
@ -53,16 +47,16 @@ These are not bugs. The protocol removed the mechanism they depend on, so they a
These work on `2026-07-28` today because they never leaned on a protocol session:
- **`tasks/get` polling.** Task result retrieval is keyed by `task_id` and backed by Docket/Redis, so a client polls across independent requests without any session affinity.
- **`tasks/get` polling.** Task result retrieval is keyed by `task_id` and 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 (poll `tasks/get`, resolve in-task input via `tasks/update`) maps onto the same durable store, and SEP-2663's `Mcp-Name: <taskId>` routing header is moot for a shared-Redis deployment where any replica can serve the poll. See [the xfail register](#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](/development/v4-notes/feature-program#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 affect `2026-07-28` connections only; on the handshake eras every one of them behaves correctly.
The remaining items are real holes, deferred to the [first-class 2026 client](feature-program.md#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 affect `2026-07-28` connections only; on the handshake eras every one of them behaves correctly.
- **`ctx.session_id` and `ctx.set_state` / `ctx.get_state` (broken even single-replica).** On a modern request `ctx.session_id` mints a fresh `uuid4`, cached on the per-request `connection.state` that is discarded when the request returns. So `ctx.set_state` and `ctx.get_state` silently never round-trip across requests — no error, just lost data. The open design decision is whether `session_id` should become `None` with `set_state` documented as session-era-only, or be re-based on an app-level key (the auth subject, or a client-supplied header).
- **Task push and background elicitation (broken even single-replica).** The initial task-status notification is delivered only while the submitting POST is still streaming; the standalone subscription task pushes into a dead sink and its cleanup fires at request end, and the Redis relay is keyed by the throwaway per-request session id. Elicitation from a background task is impossible on 2026 by protocol construction — it needs an explicit era-gate that raises a clear error rather than hanging. Task-status push on 2026 would require adopting `subscriptions/listen` (which does not carry task events) or declaring the era poll-only.
- **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 an `inputRequests` map on `tasks/get`, and the client answers via `tasks/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; the `fastmcp-tasks` rebuild implements the poll-based channel instead. Foreground (non-task) elicitation on 2026 remains the guard-mode `InputRequiredResult`.
- **Stateful proxy affinity (degraded).** The stateful proxy's `_caches` are keyed by the per-request `Connection`, 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 the `session_id` question — same root — or gated to the legacy/stdio transports.
Multi-replica concerns (per-process rate-limiter buckets, shared Redis backends for state and tasks, a Redis `SubscriptionBus`) are deployment configuration rather than protocol gaps and are out of scope for this section.
@ -71,16 +65,16 @@ Multi-replica concerns (per-process rate-limiter buckets, shared Redis backends
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.
- **#2 (bug/question)** — `capabilities.extensions` stripped at pre-2026 negotiated versions.
- **#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 `CreateTaskResult` on `tools/call` through the extensions mechanism, which the registries already admit.*
- **#2 (bug/question)** — `capabilities.extensions` stripped at pre-2026 negotiated versions. **Elevated:** this now gates the `io.modelcontextprotocol/tasks` extension (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_client` drops 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.
- **#10 (hard edge)** — 2026 push-feature degradation error quality is inconsistent. *Resolved on the FastMCP side: `ctx.elicit` / `ctx.sample` are era-gated to raise a clear error on modern connections (#4448).*
Filing is gated on maintainer approval of each issue text.
Separately, the [SDK delegation round two](/development/v4-notes/feature-program#sdk-delegation-round-two) work depends on **three upstream feature requests** — per-session event-store scoping, a user-middleware injection hook, and a lifespan hook — that would let FastMCP collapse its HTTP builders onto the SDK's and inherit the SDK's session-owner credential enforcement.
Separately, the [SDK delegation round two](feature-program.md#sdk-delegation-round-two) work depends on **three upstream feature requests** — per-session event-store scoping, a user-middleware injection hook, and a lifespan hook — that would let FastMCP collapse its HTTP builders onto the SDK's and inherit the SDK's session-owner credential enforcement.
## GA transition checklist

View file

@ -4,7 +4,7 @@ title: 2026-07-28 Protocol Support
FastMCP v4 serves the sessionless `2026-07-28` protocol era and the session-based handshake eras from a single server, with per-connection auto-detection. This page catalogs what FastMCP provides for the modern era — both the protocol machinery it inherits from the MCP Python SDK and the capabilities FastMCP implements itself on top of that layer. It is the reference for what a v4 deployment can actually do on the modern protocol today.
## Identity assertion (SEP-990): a complete server-side implementation
## Identity assertion (SEP-990)
SEP-990 defines enterprise "on-behalf-of" access: a corporate identity provider (Okta, Microsoft Entra, etc.) issues a signed *ID-JAG* asserting an employee's identity, the employee's agent presents it at the MCP authorization server's token endpoint via the RFC 7523 `jwt-bearer` grant, and receives a short-lived access token — no browser login, no per-user consent screen, and revocation lives at the IdP.
@ -23,7 +23,7 @@ auth = OAuthProxy(
mcp = FastMCP("Internal API", auth=auth)
```
Behind that one parameter, FastMCP performs the full SEP-990 §5.1 / RFC 7523 §3 processing: JWKS-based signature verification with automatic OIDC discovery of issuer keys, `typ`/`iss`/`aud`/`sub` validation, temporal checks (`exp`, `iat`, `nbf`, maximum assertion lifetime), enforcement of the assertion's signed `client_id` and `resource` bindings, `jti` replay rejection, scope derivation from the signed assertion (client requests can narrow but never widen), short-lived token issuance with no refresh token, and revocation tracking for the issued tokens. The asserted subject flows into the normal FastMCP auth context, so tools read it through `get_access_token()` like any other identity. See [Identity Assertion](/servers/auth/oauth-proxy#identity-assertion-sep-990) for the full documentation.
Behind that one parameter, FastMCP performs the full SEP-990 §5.1 / RFC 7523 §3 processing: JWKS-based signature verification with automatic OIDC discovery of issuer keys, `typ`/`iss`/`aud`/`sub` validation, temporal checks (`exp`, `iat`, `nbf`, maximum assertion lifetime), enforcement of the assertion's signed `client_id` and `resource` bindings, `jti` replay rejection, scope derivation from the signed assertion (client requests can narrow but never widen), short-lived token issuance with no refresh token, and revocation tracking for the issued tokens. The asserted subject flows into the normal FastMCP auth context, so tools read it through `get_access_token()` like any other identity. See [Identity Assertion](https://gofastmcp.com/servers/auth/oauth-proxy#identity-assertion-sep-990) for the full documentation.
This slots into FastMCP's existing authorization-server stack — the OAuth proxy's dynamic client registration, the consent flow, and self-issued JWTs — which is what makes a one-parameter enterprise deployment possible.
@ -39,15 +39,15 @@ The complete picture of what a FastMCP v4 server and client provide on the `2026
| **Cache hints (SEP-2549)** | Server-level authoring (`FastMCP(cache_ttl=..., cache_scope=...)`) stamps every cacheable result; the FastMCP client honors hints with an opt-in response cache. |
| **Distributed response caching** | `KeyValueResponseCacheStore` backs the client cache with any key-value store (Redis, memory, filetree), so a fleet of clients or proxy replicas shares cache fills across processes. |
| **Resource path security** | Templated resource parameters are screened for traversal, absolute paths, and null bytes before handlers run — on by default, including provider-sourced and mounted templates. |
| **Client protocol negotiation** | `Client(mode="auto")` probes `server/discover` and falls back to the classic handshake; the client answers multi-round-trip `input_required` requests through its existing handlers. |
| **Elicitation on the modern protocol (SEP-2322)** | Tools request user input via multi-round trips: a tool returns an `InputRequiredResult` and re-runs per round, reading the client's answers off `ctx.input_responses` / `ctx.request_state` (the [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol)). Each round is a complete request→response cycle; the framework seals `request_state` on the wire and unseals it before the tool runs, and a shared-key `request_state_security` policy carries state across replicas. On handshake-era connections returning this result produces a clear era error. |
| **Client protocol negotiation** | `Client(mode="auto")` — the default as of v4 — probes `server/discover` and falls back to the classic handshake; the client answers multi-round-trip `input_required` requests through its existing handlers. Pin `mode="legacy"` to force the handshake. |
| **Elicitation on the modern protocol (SEP-2322)** | Tools request user input via multi-round trips: a tool returns an `InputRequiredResult` and re-runs per round, reading the client's answers off `ctx.input_responses` / `ctx.request_state` (the [guard pattern](https://gofastmcp.com/servers/elicitation#elicitation-on-the-modern-protocol)). Each round is a complete request→response cycle; the framework seals `request_state` on the wire and unseals it before the tool runs, and a shared-key `request_state_security` policy carries state across replicas. On handshake-era connections returning this result produces a clear era error. |
| **Spec-standard errors (SEP-2164)** | Missing-resource reads return `-32602`; push-feature calls on modern connections fail with clear era-specific errors rather than generic method-not-found. |
| **Background tasks** | `@mcp.tool(task=True)` runs on a Redis-backed distributed runtime (Docket) with cross-replica notifications — execution infrastructure that is FastMCP's own, independent of the protocol-era task surface. |
| **Middleware** | Typed per-method hooks (`on_call_tool`, `on_list_tools`, …) and a suite of built-ins (auth, rate limiting, caching, error handling, logging, timing, and more). |
| **Composition** | `mount()`, providers, proxying, and tool transforms compose servers dynamically at runtime, with lifespans and middleware driven through the SDK session manager. |
| **Pagination** | Declarative `FastMCP(list_page_size=...)` paginates all list operations in the high-level server; the client auto-paginates with cycle detection. |
| **Telemetry** | OpenTelemetry spans on by default (no-op without an exporter), SDK-aligned attributes (`mcp.method.name`, `mcp.protocol.version`, `gen_ai.*`), plus auth and provider-delegation spans; `FASTMCP_ENABLE_TELEMETRY=false` disables cleanly. |
| **Telemetry** | OpenTelemetry spans on by default (no-op without an exporter), SDK-aligned attributes (`mcp.method.name`, `mcp.protocol.version`, `gen_ai.*`), plus auth and provider-delegation spans; `FASTMCP_TELEMETRY_MODE` selects `native`, `propagation_only` (interop with an outer MCP instrumentation layer), or `off`. |
| **Background tasks (SEP-2663)** | `fastmcp-tasks` implements the `io.modelcontextprotocol/tasks` extension end to end: `mcp.add_extension(TasksExtension())` plus `task=True` runs a tool as a background task, driven by the same Docket engine FastMCP 3 used. A client transparently completes a tasked call; gathering input mid-task uses the same guard pattern as foreground multi-round-trip tools, so a tool is written once and works either way. Modern-protocol only — the `task=True` runtime this replaced (SEP-1686) is gone entirely, not bridged. See [Background Tasks (SEP-2663)](background-tasks.md) for the design and [servers/tasks](https://gofastmcp.com/servers/tasks) for usage. |
## Still in the program
Elicitation on the modern protocol is now shipped in its **guard form** — a tool returns an `InputRequiredResult` and re-runs per round to gather user input via multi-round trips (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)). The declarative `Resolve(...)` layer over that primitive remains staged, tracked in the [Feature Program](/development/v4-notes/feature-program), along with the unified `subscriptions/listen` stream. The [Known Gaps](/development/v4-notes/known-gaps) page tracks the upstream dependencies that gate them.
Elicitation on the modern protocol is now shipped in its **guard form** — a tool returns an `InputRequiredResult` and re-runs per round to gather user input via multi-round trips (see [Elicitation on the modern protocol](https://gofastmcp.com/servers/elicitation#elicitation-on-the-modern-protocol)). The declarative `Resolve(...)` layer over that primitive remains staged, tracked in the [Feature Program](feature-program.md), along with the unified `subscriptions/listen` stream. The [Known Gaps](known-gaps.md) page tracks the upstream dependencies that gate them.

View file

@ -0,0 +1,217 @@
# Stateless session state (2026-07-28)
> Design spec. Status: building.
## Problem
The `2026-07-28` era is stateless by protocol construction: each request builds a
fresh `Connection`, `connection.session_id` is always `None`, and
`connection.state` is a new dict discarded when the request returns. So
`ctx.session_id` mints a throwaway `uuid4` per request and `ctx.set_state` /
`ctx.get_state` **silently never round-trip** — no error, just lost data. A user
who wants cross-call state (a cart, a conversation, accumulated context) has no
safe mechanism, and the failure is invisible.
The one identifier every modern request carries that is stable and
**non-spoofable** is the authenticated principal — `get_access_token().claims["sub"]`,
or the `(client_id, issuer, subject)` triple. Everything else on the wire is
client-declared and forgeable.
## The model
State lives **server-side** in the one `AsyncKeyValue` (py-key-value) store the
server already holds (`session_state_store`). The framework calls `get`/`put`/
`delete` and **never imposes a TTL** — retention is entirely the store's
(configure it on the store you pass: a Redis TTL, a py-key-value TTL wrapper,
whatever). There is no second store and no framework-owned TTL knob.
Isolation comes from the **authenticated principal, not from the session id.**
State is keyed by `(principal, session_id)`. A request under principal B keys
into B's own namespace — it can never address A's keys no matter what
`session_id` it passes. The id only organizes sessions *within* a principal. The
handle is a bare `uuid4` string; it is **not sealed** — the principal prefix is
the wall. Sessions are also create-then-validate (below): an id that was never
minted by `create_session` under this principal is rejected outright, not
resolved to an empty session.
## Two explicit patterns
A tool opts into exactly one, on purpose. There is deliberately **no** optional
"id if given, else default" parameter — that would silently misroute a call
whose id the agent forgot to pass into the shared per-user bucket, which is the
invisible-degradation failure this whole feature exists to remove.
### Per-user state — injected
```python
from fastmcp.server.sessions import UserSession
@mcp.tool
async def remember(fact: str, session: UserSession) -> str:
await session.set("fact", fact)
return "noted"
```
`session: UserSession` is **dependency-injected** (like `ctx: Context`): keyed by
the request's authenticated principal, not present in the input schema, nothing
for the agent to pass. Requires auth — with no principal it raises a clear error.
Use it when one bucket per user is what you want. `UserSession` is only the
injection annotation — the value the handler receives is an ordinary `Session`,
so its `get`/`set`/`delete`/`clear` accessors work as usual.
### Distinct sessions — an argument
```python
from fastmcp.server.sessions import SessionId
from fastmcp.server.dependencies import get_session
@mcp.tool
async def add_to_cart(item: str, session_id: SessionId) -> str:
session = await get_session(session_id)
cart = await session.get("cart", default=[])
cart.append(item)
await session.set("cart", cart)
return f"{len(cart)} items"
```
`session_id: SessionId` is a **required string argument** — it *is* in the schema,
the agent supplies it. `SessionId` is a marker type so the framework
auto-populates the argument's description with the protocol:
> "Session identifier. Use a tool to create a session, then pass the resulting id
> here to persist state across calls in the same session."
The tool becomes self-teaching — an agent reads the schema and learns the
create-then-pass contract with no hand-prompting. The description names no
specific tool: composition can rename the lifecycle tool (mounting under a
namespace exposes it as `child_create_session`), so it points at the
*capability* rather than a name that may not exist under that mount.
The standalone `await get_session(session_id)` resolves the id to a `Session`
keyed by `(principal, session_id)`, **validating** that it was created under this
principal — an unknown or foreign id raises `InvalidSession` rather than opening a
fresh bucket. It is a plain function, not a `Context` method, so it needs no
foreground context and works from a `task=True` tool's worker. Use this pattern
when a user needs more than one session.
## The `Session` object
Async accessors over the server store, scoped to one `(principal, session_id)`:
- `session.id` — the session's id (set for a `session_id`-resolved session; `None`
for an injected `UserSession`, which has no distinct id).
- `await session.get(key, default=None)`
- `await session.set(key, value)`
- `await session.delete(key)`
- `await session.clear()` — empties user state but **keeps the session valid**.
- `await session.end()` — deletes the session (what `end_session` calls).
A session's state is stored as a **single dict under one key**
(`session:{sha256(principal)}:{session_id}`, and `session:anon:{session_id}` when
unauthenticated — the principal is hashed into a fixed-length, delimiter-safe
segment, never embedded raw). That dict holds user state in a `state` sub-dict
alongside a small `_created` marker, so a created-but-empty session is
distinguishable from a missing one even if the store collapses empty dicts.
`get`/`set`/`delete` read-modify-write the sub-dict and never touch the marker;
`clear` resets the sub-dict but leaves the marker (the session still resolves);
`end` deletes the key. Namespacing user state under `state` is what keeps a user
key named `_created` from colliding with the marker. One key per session means
one TTL per session (the store's), refreshed on write — no key index to maintain,
and `end` is a single delete. (Trade-off: concurrent writes to one session race
on the read-modify-write; session state is small and typically driven serially by
one agent, so this is acceptable — noted, not hidden.)
## `SessionProvider`
Session ids are minted by `SessionProvider`, which contributes two tools:
- `create_session()` → mints an unguessable `uuid4`, **records** the session
under the current principal, and returns the id as a string.
- `end_session(session_id: SessionId)` → validates the id, then deletes the
session so it no longer resolves.
Register it whenever your tools take a `session_id` — providers are the idiomatic
way to add functionality like this:
```python
from fastmcp.server.sessions import SessionProvider
mcp.add_provider(SessionProvider())
```
There is **no enforcement** that a provider is registered, and there was: an
earlier version scanned the tool set at list/resolve time and raised if a
`session_id` tool had no provider. That check had to reason about the whole
composition pipeline — `isinstance` on providers, unwrapping namespaced ones,
tool transforms, session visibility, enabled state — and produced false
positives that broke valid servers (a namespaced provider, a session-disabled
tool). It was deleted. The guarantee never needed it: `get_session` validates
that an id was recorded (create-then-validate), so a server with no provider
simply cannot mint ids, and every `get_session` rejects — a misconfiguration
caught the first time the tools run, not a security hole.
`SessionProvider` subclasses `Provider`, takes **no store** (uses the server's)
and **no ttl** (the store's). It exists to mint and end owned ids.
`create_session` matters most without auth, where an unguessable id is the only
defense against a caller *guessing* onto another session.
When an application already mints its own identifiers — conversation ids, workflow
ids — take them as ordinary string arguments rather than `SessionId`, and register
no provider; `SessionId` is specifically the create-then-pass contract backed by
`create_session`.
## Security
Keyed by `(principal, session_id)`:
- **Authenticated → strong isolation.** `principal` is the validated token
subject, unforgeable. B keys into B's namespace; A's data is unreachable no
matter what id B passes. Guessing is pointless; a session id appearing in agent
context or logs is harmless (it is not a capability without the principal).
Caller-chosen ids are safe here.
- **Unauthenticated → single-tenant-safe only.** No principal, so the key is just
the id in a shared namespace: the id becomes a bearer capability, and exposure
in logs/conversation leaks the session. `create_session`'s `uuid4` gives
guess-*resistance*, not isolation. Documented in bold: not a tenant boundary;
without auth, force minted ids and never treat sessions as a wall between
clients.
- **Isolation is auth; the id is organization.** No id scheme substitutes for a
principal, which is why sealing the handle buys nothing load-bearing and is
dropped.
- **Not FastMCP's job:** transport (use TLS), encryption at rest (the store's), a
malicious *authorized* client acting within its rights.
## Rework plan (from the current prototype)
The prototype (`sessions.py`, `context.py`, `function_tool.py`, `server.py`) built
a `Scope` enum, a sealed `SessionCodec`, and `ctx.get_state(scope=...)`. Rework to
the above:
1. **Remove `Scope`** and the `scope=` parameter; revert `ctx.get_state`/
`set_state` to their original request-scoped behavior.
2. **Remove the `SessionCodec`/sealing** — ids are bare `uuid4`.
3. **`Session` object** with async `get`/`set`/`delete`/`clear` over the server
store, single-dict-per-session key scheme.
4. **`session: UserSession`** injection (principal-keyed; error without auth) —
wire into the same parameter-detection path as `Context`. `UserSession` is the
injection marker; the injected value is a `Session`.
5. **`session_id: SessionId`** marker type: string in the schema, auto-filled
description, standalone `await get_session(id)` resolver that validates the id
(works from a task worker — no foreground context needed).
6. **`SessionProvider(Provider)`** with `create_session` (records the session) /
`end_session` (deletes it), registered explicitly via `add_provider`. No
enforcement that it is present — `get_session`'s validation is the guarantee.
7. Rewrite the tests to cover both patterns, principal isolation, no-auth
behavior, and `end_session`.
## Docs plan
Written against the final API once the rework verifies:
- A concept guide — why stateless removes the session, the two patterns, when to
reach for each. Why before how.
- A security page — the two tiers, "isolation is auth, the id is organization,"
the bold no-multitenant-without-auth warning.
- Fully runnable examples for both patterns (pass the doc-import guard, register
in `docs.json`).
- A migration note from the old `ctx.session_id` / `set_state`.

View file

@ -61,17 +61,43 @@ The final tool result has two parts: `content` (a list of `TextContent` blocks f
## Tool call routing
Normal tool calls go through the provider chain, which applies transforms (namespace prefixes, visibility filters) before resolving by name. App UI calls need a different path.
A tool has two things that behave very differently. Its **name** is unstable by design — namespace transforms rename it, so `save_contact` becomes `contacts_save_contact` in one composition and something else in another. Its **identity** is a hash of the app name and the registered tool name, written once at registration and never changed.
### The hashed lookup bypass
A UI is serialized during the entry tool's call, deep inside whatever composition the server happens to have, so it cannot know what its backend tools will be called by the time the payload reaches a host.
Backend tools are typically hidden from the model (`visibility=["app"]`). Visibility transforms would filter them out of normal resolution. And namespace transforms might rename them — `save_contact` becomes `contacts_save_contact` — while the renderer needs a stable way to call the original backend.
### Late-bound tool names
Hashed lookup solves both problems. FastMCP first tries normal tool resolution. If no visible tool matches and the requested name looks like `<hash>_<local_name>`, FastMCP calls `get_tool_by_hash(hash, local_name)`. That lookup walks the provider tree directly, skipping transforms. It finds an app-visible tool by its original registered name and verifies that its stored `meta["fastmcp"]["_tool_hash"]` matches the requested hash.
The payload leaves the app addressed by identity, and every FastMCP server rewrites those references on the way out to whatever it lists that tool as. Servers unwind innermost-first, so the outermost server rewrites last — and its names are the only ones a client can actually invoke.
That's why `CallTool(save_contact)` keeps working when the server is mounted under a namespace. The renderer sends a deterministic hashed backend name; the server uses `get_tool_by_hash` to find the original tool without transforms in the way.
Rewriting a name in place would destroy the identity for the next layer up, so the payload carries a name-to-identity map under `_meta.fastmcp.toolNames`. Each layer resolves through the map and updates it. The action objects keep the exact shape `prefab_ui` defines: only the value of `tool` changes, and only ever to another valid tool name.
Authorization still applies. The hashed bypass skips name and visibility transforms, but auth checks still run against the tool's `auth` config before execution.
The result is that a renderer receives names that exist in the listing the host is looking at. Under three layers of namespacing the button calls `c_b_a_save`; behind a gateway it calls whatever the gateway lists. No intermediary has to understand a FastMCP-specific convention.
A reference this server cannot resolve is left alone rather than corrupted. This is what keeps apps working behind [tool search](/servers/transforms/tool-search) and code mode, which replace `tools/list` with a handful of synthetic tools: there is no better name to bind to, so the reference stays identity-addressed and the fallback below carries it.
### One copy of an app per server
**An app name must be unique within a server.** Composing the same app twice breaks its UI, and no namespace or mount arrangement makes it work.
The reason is structural. Identity is derived from the app name and the tool's registered name, and deliberately nothing else — that is what makes it survive renaming. Two copies of one app therefore produce two tools claiming a single identity, and no fact anywhere in the listing says which copy a given button belongs to. The information needed to choose was never recorded.
FastMCP declines to bind rather than picking a copy, so buttons stop working instead of quietly invoking the wrong tenant's tool. Expect a message naming the cause:
```
Ambiguous app tool 'save': 2 components share the identity '10c0803009ff'.
The same app is composed more than once, so this call cannot be routed to a
single tool.
```
Give each copy its own app name. Two tenants running the same product want `FastMCPApp("contacts-acme")` and `FastMCPApp("contacts-globex")` — not two instances of `FastMCPApp("contacts")` under different namespaces, since namespaces rename tools and identity is immune to renaming by design.
### The hashed lookup fallback
The identity-addressed form `<hash>_<local_name>` remains callable. FastMCP first tries normal tool resolution; if no tool matches and the name has that shape, it calls `get_tool_by_hash(hash, local_name)`, which walks the provider tree directly, skipping transforms.
When one identity is claimed by more than one tool — which happens when the same app is composed into two branches — the call is refused rather than resolved, since picking either one would silently route into the wrong branch.
Authorization still applies. The hashed path skips name and visibility transforms, but auth checks still run against the tool's `auth` config before execution.
### Provider delegation

View file

@ -89,7 +89,11 @@ A fair question. Any [Interactive Tool](/apps/prefab) can call a server tool —
- What happens to `CallTool("add_note")` when you mount this server under a namespace and the tool becomes `notes_add_note`?
- How do you keep it all wired correctly as you compose servers?
`FastMCPApp` owns these concerns. Entry points register as model-visible. Backend tools register as UI-only by default. Backend tools get globally stable identifiers that survive namespacing, and `CallTool` accepts function references, so references stay valid when you compose servers.
`FastMCPApp` owns these concerns. Entry points register as model-visible, backend tools register as UI-only, and hosts act on those declarations to decide what the model sees.
Composition is handled by never writing the name down. `CallTool` takes a function reference, and FastMCP resolves it when the UI is serialized — to whatever that tool is actually called by then. Mount the server under a namespace and the button calls `notes_add_note`; put a gateway in front and it calls whatever the gateway lists. Since you never wrote a name, renaming cannot break it. [The architecture page](/apps/architecture) covers how that resolution works.
The one rule that comes with this: **an app name must be unique within a server.** Composing the same app twice breaks its UI — two copies of `FastMCPApp("notes")` are indistinguishable no matter what namespaces you mount them under, so FastMCP declines to bind rather than picking one. Name apps for what they serve: `FastMCPApp("notes-acme")` and `FastMCPApp("notes-globex")`. [The architecture page](/apps/architecture) explains why identity works this way.
The rest of this page covers each piece in turn.

View file

@ -70,11 +70,15 @@ def my_tool() -> str:
The `visibility` field controls where a tool appears:
- `["model"]` — visible to the LLM (the default behavior)
- `["app"]` — only callable from within the app UI, hidden from the LLM
- `["app"]` — callable from within the app UI, kept out of the LLM's tool list
- `["model", "app"]` — both
This is useful when you have tools that only make sense as part of the app's interactive flow, not as standalone LLM actions.
Visibility is a declaration, and on `tools/list` the host does the filtering — the division the MCP Apps specification defines. Every tool is advertised carrying its `visibility` metadata, which is also what lets a proxy or gateway forward it: an intermediary can only route to a tool it can see.
That division assumes a host stands between the server and the model. Where one doesn't, FastMCP applies the declaration itself. [Tool search](/servers/transforms/tool-search) and code mode reach the model as ordinary tool output rather than as an advertised listing, and their call-tool proxies execute a name the model supplies — nothing downstream can filter either, so app-only tools are excluded from both. The app's own UI still reaches its backends, because a UI calling by identity is not the model.
```python
@mcp.tool(
app=AppConfig(
@ -216,7 +220,7 @@ import qrcode
from fastmcp import FastMCP
from fastmcp.apps import AppConfig, ResourceCSP
from fastmcp.tools import ToolResult
from fastmcp.types import ImageContent
from mcp.types import ImageContent
mcp = FastMCP("QR Code Server")

View file

@ -59,16 +59,24 @@ This works with **stdio**, **SSE**, and **stateful HTTP** transports, where sess
In **stateless HTTP** mode, each request creates a new session object with a new ID. Files stored during one request (e.g. the UI upload) will be invisible to the next request (e.g. the LLM calling `list_files`). You **must** override `_get_scope_key` to use a stable identifier like a user ID from your auth token.
</Warning>
For stateless deployments, override `_get_scope_key` to return a stable identifier. For example, to scope files by authenticated user:
For stateless deployments, override `_get_scope_key` to return a stable identifier. To scope files by authenticated user, read the caller from `get_access_token()`.
Reject the request when there is no subject to key on. `get_access_token()` returns `None` on an unauthenticated request, and `subject` is optional even on a valid token, since not every verifier populates it. Returning a fallback in either case would put every such caller in one shared bucket, so they would see each other's uploads.
```python
from fastmcp.apps.file_upload import FileUpload
from fastmcp.server.dependencies import get_access_token
class UserScopedUpload(FileUpload):
def _get_scope_key(self, ctx):
return ctx.access_token["sub"]
token = get_access_token()
if token is None or not token.subject:
raise ValueError("File scoping requires an authenticated user with a subject")
return token.subject
```
If your provider carries the user identity in a different claim, read it from `token.claims` and validate it the same way.
For process-wide shared storage (all users see all files):
```python
@ -85,10 +93,17 @@ The default implementation stores files in memory for the lifetime of the server
import base64
from fastmcp.apps.file_upload import FileUpload
from fastmcp.server.dependencies import get_access_token
class S3Upload(FileUpload):
def _get_scope_key(self, ctx):
token = get_access_token()
if token is None or not token.subject:
raise ValueError("File scoping requires an authenticated user with a subject")
return token.subject
def on_store(self, files, ctx):
user_id = ctx.access_token["sub"]
user_id = self._get_scope_key(ctx)
for f in files:
s3.put_object(
Bucket="uploads",
@ -98,7 +113,7 @@ class S3Upload(FileUpload):
return self.on_list(ctx)
def on_list(self, ctx):
user_id = ctx.access_token["sub"]
user_id = self._get_scope_key(ctx)
objects = s3.list_objects(Bucket="uploads", Prefix=f"{user_id}/")
return [
{
@ -112,7 +127,7 @@ class S3Upload(FileUpload):
]
def on_read(self, name, ctx):
user_id = ctx.access_token["sub"]
user_id = self._get_scope_key(ctx)
obj = s3.get_object(Bucket="uploads", Key=f"{user_id}/{name}")
content = obj["Body"].read()
return {

View file

@ -5,6 +5,218 @@ rss: true
tag: NEW
---
<Update label="v3.4.6" description="2026-08-05">
**[v3.4.6: Trust, but Proxy](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.6)**
FastMCP 3.4.6 backports trusted-proxy support for SSRF-protected OAuth metadata and JWKS fetches. Deployments can now route these requests through a mandated corporate proxy while preserving custom CA certificates; FastMCP refuses the fetch when no proxy is configured instead of risking an unprotected direct request.
### Fixes 🐞
* Backport #4412 to 3.x: support trusted SSRF proxies by [@jlowin](https://github.com/jlowin) in [#4755](https://github.com/PrefectHQ/fastmcp/pull/4755)
### Docs 📚
* Docs: add v3.4.6 changelog entries by [@jlowin](https://github.com/jlowin) in [#4761](https://github.com/PrefectHQ/fastmcp/pull/4761)
**Full Changelog**: [v3.4.5...v3.4.6](https://github.com/PrefectHQ/fastmcp/compare/v3.4.5...v3.4.6)
</Update>
<Update label="v4.0.0b1" description="2026-07-28">
**[v4.0.0b1: Fourgone Conclusion](https://github.com/PrefectHQ/fastmcp/releases/tag/v4.0.0b1)**
FastMCP 4 makes stateful MCP applications work on the sessionless `2026-07-28` protocol while one deployment continues serving handshake-era clients. Tools can ask follow-up questions across requests, preserve authenticated user state, and move long-running work into background tasks without sticky sessions. Protocol extensions and enterprise identity become first-class surfaces, and most FastMCP 3 servers upgrade unchanged even though MCP Python SDK v2 rewrote the engine underneath them. Server-initiated sampling and roots are removed from the server API; the [upgrade guide](/getting-started/upgrading/from-fastmcp-3) covers their replacements.
### New Features 🎉
* Migrate to MCP Python SDK v2 by [@jlowin](https://github.com/jlowin) in [#4437](https://github.com/PrefectHQ/fastmcp/pull/4437)
* Teach fastmcp.Client the modern protocol: mode negotiation, MRTR driver, response cache by [@jlowin](https://github.com/jlowin) in [#4450](https://github.com/PrefectHQ/fastmcp/pull/4450)
* Forward-port Hugging Face auth provider by [@jlowin](https://github.com/jlowin) in [#4475](https://github.com/PrefectHQ/fastmcp/pull/4475)
* Add server-side identity assertion (SEP-990 ID-JAG) by [@jlowin](https://github.com/jlowin) in [#4483](https://github.com/PrefectHQ/fastmcp/pull/4483)
* Add guard-mode multi-round-trip tools (SEP-2322) by [@jlowin](https://github.com/jlowin) in [#4544](https://github.com/PrefectHQ/fastmcp/pull/4544)
* Add FastMCP-native server extension API (SEP-2133) by [@jlowin](https://github.com/jlowin) in [#4602](https://github.com/PrefectHQ/fastmcp/pull/4602)
* Add stateless session state (UserSession / SessionId) by [@jlowin](https://github.com/jlowin) in [#4604](https://github.com/PrefectHQ/fastmcp/pull/4604)
* Add background tasks via the io.modelcontextprotocol/tasks extension (SEP-2663) by [@jlowin](https://github.com/jlowin) in [#4603](https://github.com/PrefectHQ/fastmcp/pull/4603)
### Breaking Changes ⚠️
* Emit one SERVER span per request and adopt spec-correct error codes by [@jlowin](https://github.com/jlowin) in [#4445](https://github.com/PrefectHQ/fastmcp/pull/4445)
* Remove 3.x deprecated module shims and dead parameters by [@jlowin](https://github.com/jlowin) in [#4447](https://github.com/PrefectHQ/fastmcp/pull/4447)
* Remove 3.0-deprecated FastMCP server methods by [@jlowin](https://github.com/jlowin) in [#4451](https://github.com/PrefectHQ/fastmcp/pull/4451)
* Remove 3.x deprecated parameters and object-mode decorators by [@jlowin](https://github.com/jlowin) in [#4453](https://github.com/PrefectHQ/fastmcp/pull/4453)
* Migrate to MCP SDK v2.0.0b2 (httpx2) by [@jlowin](https://github.com/jlowin) in [#4503](https://github.com/PrefectHQ/fastmcp/pull/4503)
* Fix typos by [@szepeviktor](https://github.com/szepeviktor) in [#4498](https://github.com/PrefectHQ/fastmcp/pull/4498)
* Stop proxies from validating backend results or mutating shared transports by [@jlowin](https://github.com/jlowin) in [#4552](https://github.com/PrefectHQ/fastmcp/pull/4552)
* Surface resource, prompt, and proxy errors on the modern protocol by [@jlowin](https://github.com/jlowin) in [#4579](https://github.com/PrefectHQ/fastmcp/pull/4579)
* Negotiate the best mutual protocol era by default by [@jlowin](https://github.com/jlowin) in [#4572](https://github.com/PrefectHQ/fastmcp/pull/4572)
* Remove server-initiated sampling and roots from the server API by [@jlowin](https://github.com/jlowin) in [#4648](https://github.com/PrefectHQ/fastmcp/pull/4648)
* Remove 3.x-era compatibility shims by [@jlowin](https://github.com/jlowin) in [#4661](https://github.com/PrefectHQ/fastmcp/pull/4661)
### Enhancements ✨
* Deprecate ctx.sample and add clear errors for push features on 2026 connections by [@jlowin](https://github.com/jlowin) in [#4448](https://github.com/PrefectHQ/fastmcp/pull/4448)
* Add server-level cache hints (SEP-2549) by [@jlowin](https://github.com/jlowin) in [#4464](https://github.com/PrefectHQ/fastmcp/pull/4464)
* Add KeyValueResponseCacheStore for distributed client response caching by [@jlowin](https://github.com/jlowin) in [#4479](https://github.com/PrefectHQ/fastmcp/pull/4479)
* Test lifespan fires once per process over HTTP by [@jlowin](https://github.com/jlowin) in [#4480](https://github.com/PrefectHQ/fastmcp/pull/4480)
* Add telemetry off-switch and mcp.protocol.version span attribute by [@jlowin](https://github.com/jlowin) in [#4481](https://github.com/PrefectHQ/fastmcp/pull/4481)
* Trace client task management requests by [@jlowin](https://github.com/jlowin) in [#4525](https://github.com/PrefectHQ/fastmcp/pull/4525)
* Stabilize upgraded ty checks by [@jlowin](https://github.com/jlowin) in [#4526](https://github.com/PrefectHQ/fastmcp/pull/4526)
* Improve DescopeProvider scope discovery and well-known URL support by [@gaokevin1](https://github.com/gaokevin1) in [#4489](https://github.com/PrefectHQ/fastmcp/pull/4489)
* Add examples/ to the ty static-analysis gate by [@jlowin](https://github.com/jlowin) in [#4466](https://github.com/PrefectHQ/fastmcp/pull/4466)
* Expose telemetry attributes on span start by [@zzstoatzz](https://github.com/zzstoatzz) in [#4487](https://github.com/PrefectHQ/fastmcp/pull/4487)
* Fix-issue-4284 : Add Auth0MCPProvider for Auth0 Auth for MCP by [@vijaydeepsinha](https://github.com/vijaydeepsinha) in [#4411](https://github.com/PrefectHQ/fastmcp/pull/4411)
* Run FastMCP middleware for every inbound message by [@jlowin](https://github.com/jlowin) in [#4553](https://github.com/PrefectHQ/fastmcp/pull/4553)
* Add 'prs welcome' label to waive the PR assignment gate by [@jlowin](https://github.com/jlowin) in [#4557](https://github.com/PrefectHQ/fastmcp/pull/4557)
* Rename martian workflows to marvin by [@jlowin](https://github.com/jlowin) in [#4558](https://github.com/PrefectHQ/fastmcp/pull/4558)
* Bump pinned Claude models to current versions by [@jlowin](https://github.com/jlowin) in [#4561](https://github.com/PrefectHQ/fastmcp/pull/4561)
* Make the unit suite fast: in-process HTTP tests, no real sleeps, parallel Windows CI by [@jlowin](https://github.com/jlowin) in [#4554](https://github.com/PrefectHQ/fastmcp/pull/4554)
* Mirror the frontend's protocol era on a proxy's backend connection by [@jlowin](https://github.com/jlowin) in [#4573](https://github.com/PrefectHQ/fastmcp/pull/4573)
* Drop forked client protocol helpers in favor of the SDK's by [@jlowin](https://github.com/jlowin) in [#4574](https://github.com/PrefectHQ/fastmcp/pull/4574)
* Bring the v4 developer notes up to date with what shipped by [@jlowin](https://github.com/jlowin) in [#4581](https://github.com/PrefectHQ/fastmcp/pull/4581)
* Trim fastmcp.types to FastMCP-unique types by [@jlowin](https://github.com/jlowin) in [#4584](https://github.com/PrefectHQ/fastmcp/pull/4584)
* Let a server answer argument-completion requests by [@jlowin](https://github.com/jlowin) in [#4582](https://github.com/PrefectHQ/fastmcp/pull/4582)
* Add machine-to-machine client authentication by [@jlowin](https://github.com/jlowin) in [#4583](https://github.com/PrefectHQ/fastmcp/pull/4583)
* Expose era-neutral client server metadata by [@zzstoatzz](https://github.com/zzstoatzz) in [#4599](https://github.com/PrefectHQ/fastmcp/pull/4599)
* Support routable transport headers for gateways (SEP-2243) by [@jlowin](https://github.com/jlowin) in [#4622](https://github.com/PrefectHQ/fastmcp/pull/4622)
* Emit scope step-up challenges for incremental authorization (SEP-2350) by [@jlowin](https://github.com/jlowin) in [#4623](https://github.com/PrefectHQ/fastmcp/pull/4623)
* Honor OAuth application_type in DCR (SEP-837) by [@jlowin](https://github.com/jlowin) in [#4621](https://github.com/PrefectHQ/fastmcp/pull/4621)
* Drop stale label-noting instructions from CLAUDE.md by [@jlowin](https://github.com/jlowin) in [#4654](https://github.com/PrefectHQ/fastmcp/pull/4654)
* Add require_roles auth check by [@jlowin](https://github.com/jlowin) in [#4656](https://github.com/PrefectHQ/fastmcp/pull/4656)
* Add `valid_scopes` parameter to OIDC proxy valid scopes by [@Educg550](https://github.com/Educg550) in [#4660](https://github.com/PrefectHQ/fastmcp/pull/4660)
* feat: Add telemetry interop mode for FastMCP by [@strawgate](https://github.com/strawgate) in [#4046](https://github.com/PrefectHQ/fastmcp/pull/4046)
* Note that review comment threads should get an acknowledgement by [@jlowin](https://github.com/jlowin) in [#4678](https://github.com/PrefectHQ/fastmcp/pull/4678)
* Soften the review-comment reply guidance by [@jlowin](https://github.com/jlowin) in [#4683](https://github.com/PrefectHQ/fastmcp/pull/4683)
* Resolve review threads on fix, reply on decline by [@jlowin](https://github.com/jlowin) in [#4685](https://github.com/PrefectHQ/fastmcp/pull/4685)
* Move to the stable MCP Python SDK 2.0.0 by [@jlowin](https://github.com/jlowin) in [#4655](https://github.com/PrefectHQ/fastmcp/pull/4655)
### Security 🔒
* Drive the FastMCP lifespan through the SDK session manager by [@jlowin](https://github.com/jlowin) in [#4446](https://github.com/PrefectHQ/fastmcp/pull/4446)
* Route skill file access through SDK path-security primitives by [@jlowin](https://github.com/jlowin) in [#4449](https://github.com/PrefectHQ/fastmcp/pull/4449)
* Screen templated resource parameters for path traversal by default by [@jlowin](https://github.com/jlowin) in [#4482](https://github.com/PrefectHQ/fastmcp/pull/4482)
* [codex] Add OAuthProxy RFC 9207 issuer responses by [@jlowin](https://github.com/jlowin) in [#4438](https://github.com/PrefectHQ/fastmcp/pull/4438)
* Apply app visibility where no host can by [@jlowin](https://github.com/jlowin) in [#4692](https://github.com/PrefectHQ/fastmcp/pull/4692)
### Fixes 🐞
* Capture SharedContext for task-enabled Docket servers by [@jlowin](https://github.com/jlowin) in [#4443](https://github.com/PrefectHQ/fastmcp/pull/4443)
* Fix stale mcp.types imports in examples by [@jlowin](https://github.com/jlowin) in [#4452](https://github.com/PrefectHQ/fastmcp/pull/4452)
* Forward-port HTTP host guard compatibility by [@jlowin](https://github.com/jlowin) in [#4474](https://github.com/PrefectHQ/fastmcp/pull/4474)
* Fix Azure scope fallback by [@zzstoatzz](https://github.com/zzstoatzz) in [#4469](https://github.com/PrefectHQ/fastmcp/pull/4469)
* fix(server): omit ScalarElicitationType wrapper title from elicitation schemas by [@syf2211](https://github.com/syf2211) in [#4502](https://github.com/PrefectHQ/fastmcp/pull/4502)
* Skip unsupported JWKS keys instead of failing the whole key set (#4515) by [@earfman](https://github.com/earfman) in [#4517](https://github.com/PrefectHQ/fastmcp/pull/4517)
* Don't mutate the caller's schema in compress_schema by [@winklemad](https://github.com/winklemad) in [#4492](https://github.com/PrefectHQ/fastmcp/pull/4492)
* Forward upstream instructions through create_proxy by [@verdie-g](https://github.com/verdie-g) in [#4512](https://github.com/PrefectHQ/fastmcp/pull/4512)
* Serialize deep object query parameters by [@jlowin](https://github.com/jlowin) in [#4523](https://github.com/PrefectHQ/fastmcp/pull/4523)
* Reject positional-only tool parameters by [@jlowin](https://github.com/jlowin) in [#4524](https://github.com/PrefectHQ/fastmcp/pull/4524)
* Clarify PR-reopen flow and fix label-race that broke auto-reopen by [@jlowin](https://github.com/jlowin) in [#4518](https://github.com/PrefectHQ/fastmcp/pull/4518)
* Clean up disconnected task sessions by [@jlowin](https://github.com/jlowin) in [#4519](https://github.com/PrefectHQ/fastmcp/pull/4519)
* Handle expired OAuth client registrations by [@jlowin](https://github.com/jlowin) in [#4520](https://github.com/PrefectHQ/fastmcp/pull/4520)
* Fix OAuth request annotation after httpx2 migration by [@jlowin](https://github.com/jlowin) in [#4534](https://github.com/PrefectHQ/fastmcp/pull/4534)
* Fix docs banner contrast by [@jlowin](https://github.com/jlowin) in [#4522](https://github.com/PrefectHQ/fastmcp/pull/4522)
* Preserve component metadata in response cache by [@jlowin](https://github.com/jlowin) in [#4521](https://github.com/PrefectHQ/fastmcp/pull/4521)
* Clean up task sessions on connection exit by [@jlowin](https://github.com/jlowin) in [#4535](https://github.com/PrefectHQ/fastmcp/pull/4535)
* Include scopes in auth challenges by [@jlowin](https://github.com/jlowin) in [#4527](https://github.com/PrefectHQ/fastmcp/pull/4527)
* Make examples/ actually trigger the ty gate by [@jlowin](https://github.com/jlowin) in [#4541](https://github.com/PrefectHQ/fastmcp/pull/4541)
* Add subject field to AccessToken initialization by [@piaudonn](https://github.com/piaudonn) in [#4267](https://github.com/PrefectHQ/fastmcp/pull/4267)
* Restore Mintlify's fixed banner positioning by [@jlowin](https://github.com/jlowin) in [#4542](https://github.com/PrefectHQ/fastmcp/pull/4542)
* Fix #4292: SSRF guard breaks OAuth/JWKS fetches behind a corporate HTTP proxy by [@endofcake](https://github.com/endofcake) in [#4412](https://github.com/PrefectHQ/fastmcp/pull/4412)
* Preserve telemetry attributes when a sampler does not forward them by [@jlowin](https://github.com/jlowin) in [#4539](https://github.com/PrefectHQ/fastmcp/pull/4539)
* Speed up the unit test suite, and fix the task-notification race it surfaced by [@jlowin](https://github.com/jlowin) in [#4550](https://github.com/PrefectHQ/fastmcp/pull/4550)
* Fix label triage applying no labels, and make blocked tool calls fail by [@jlowin](https://github.com/jlowin) in [#4555](https://github.com/PrefectHQ/fastmcp/pull/4555)
* Fix AI workflow allowlists being destroyed by tokenization by [@jlowin](https://github.com/jlowin) in [#4560](https://github.com/PrefectHQ/fastmcp/pull/4560)
* Make transformed tool `required` order deterministic by [@Kludex](https://github.com/Kludex) in [#4564](https://github.com/PrefectHQ/fastmcp/pull/4564)
* Stop gather() from creating coroutines it may never schedule by [@jlowin](https://github.com/jlowin) in [#4559](https://github.com/PrefectHQ/fastmcp/pull/4559)
* Restore upgraded dependency checks by [@zzstoatzz](https://github.com/zzstoatzz) in [#4576](https://github.com/PrefectHQ/fastmcp/pull/4576)
* Fix skill frontmatter parsing with UTF-8 BOM by [@hxaxd](https://github.com/hxaxd) in [#4533](https://github.com/PrefectHQ/fastmcp/pull/4533)
* Fix File helper extension handling by [@VectorPeak](https://github.com/VectorPeak) in [#4531](https://github.com/PrefectHQ/fastmcp/pull/4531)
* Fix percent-encoded skill file names unreadable in resources mode by [@jlowin](https://github.com/jlowin) in [#4590](https://github.com/PrefectHQ/fastmcp/pull/4590)
* Fix flaky stdio crash-recovery tests by [@jlowin](https://github.com/jlowin) in [#4594](https://github.com/PrefectHQ/fastmcp/pull/4594)
* Bridge camelCase ToolAnnotations reads by [@zzstoatzz](https://github.com/zzstoatzz) in [#4597](https://github.com/PrefectHQ/fastmcp/pull/4597)
* Preserve raw CallToolResult tool returns by [@LarryHu0217](https://github.com/LarryHu0217) in [#4587](https://github.com/PrefectHQ/fastmcp/pull/4587)
* Advertise only supported token endpoint auth methods in OAuthProxy metadata by [@jlowin](https://github.com/jlowin) in [#4608](https://github.com/PrefectHQ/fastmcp/pull/4608)
* Fix OAuth proxy override typing by [@zzstoatzz](https://github.com/zzstoatzz) in [#4612](https://github.com/PrefectHQ/fastmcp/pull/4612)
* Pin burner-redis below the Windows-crashing 0.1.7 release by [@jlowin](https://github.com/jlowin) in [#4618](https://github.com/PrefectHQ/fastmcp/pull/4618)
* fix : canonical mime type mapping from formats to remove inconsistency #4627 by [@Aman071106](https://github.com/Aman071106) in [#4628](https://github.com/PrefectHQ/fastmcp/pull/4628)
* fix: accept callable roots handlers by [@ShuyingZhang](https://github.com/ShuyingZhang) in [#4639](https://github.com/PrefectHQ/fastmcp/pull/4639)
* Pass the MCP conformance suite's draft and pending scenarios by [@jlowin](https://github.com/jlowin) in [#4650](https://github.com/PrefectHQ/fastmcp/pull/4650)
* Use issuer_url for OAuth issuer identity by [@jlowin](https://github.com/jlowin) in [#4652](https://github.com/PrefectHQ/fastmcp/pull/4652)
* Fix the ty failure blocking upgrade checks on main by [@jlowin](https://github.com/jlowin) in [#4657](https://github.com/PrefectHQ/fastmcp/pull/4657)
* Bind CIMD assertion audience to the advertised token endpoint by [@jlowin](https://github.com/jlowin) in [#4659](https://github.com/PrefectHQ/fastmcp/pull/4659)
* Record effective scopes on the OAuth transaction by [@jlowin](https://github.com/jlowin) in [#4670](https://github.com/PrefectHQ/fastmcp/pull/4670)
* Copy schemas iteratively so deep nesting still compresses by [@jlowin](https://github.com/jlowin) in [#4671](https://github.com/PrefectHQ/fastmcp/pull/4671)
* Fix OpenAPI allOf reference fields by [@hxaxd](https://github.com/hxaxd) in [#4653](https://github.com/PrefectHQ/fastmcp/pull/4653)
* Flatten OpenAPI discriminator subtypes into request bodies by [@jlowin](https://github.com/jlowin) in [#4677](https://github.com/PrefectHQ/fastmcp/pull/4677)
* Let maintenance releases publish without fastmcp-tasks by [@jlowin](https://github.com/jlowin) in [#4676](https://github.com/PrefectHQ/fastmcp/pull/4676)
* Read CLI-scanned MCP config files as UTF-8 explicitly by [@jlowin](https://github.com/jlowin) in [#4690](https://github.com/PrefectHQ/fastmcp/pull/4690)
* Late-bind app tool names so UIs survive composition by [@jlowin](https://github.com/jlowin) in [#4682](https://github.com/PrefectHQ/fastmcp/pull/4682)
### Docs 📚
* Docs: forward-port v3.4.4 changelog entries by [@jlowin](https://github.com/jlowin) in [#4476](https://github.com/PrefectHQ/fastmcp/pull/4476)
* Document icon theme support by [@jlowin](https://github.com/jlowin) in [#4537](https://github.com/PrefectHQ/fastmcp/pull/4537)
* Add missing 4.0.0 version badge to Path Security docs by [@jlowin](https://github.com/jlowin) in [#4540](https://github.com/PrefectHQ/fastmcp/pull/4540)
* Align server component docs by [@strawgate](https://github.com/strawgate) in [#4260](https://github.com/PrefectHQ/fastmcp/pull/4260)
* Align CLI, deployment, and config docs by [@strawgate](https://github.com/strawgate) in [#4259](https://github.com/PrefectHQ/fastmcp/pull/4259)
* Align client, Apps, and integration docs by [@strawgate](https://github.com/strawgate) in [#4261](https://github.com/PrefectHQ/fastmcp/pull/4261)
* Fix stale MRTR/elicitation framing in client and upgrade docs by [@jlowin](https://github.com/jlowin) in [#4551](https://github.com/PrefectHQ/fastmcp/pull/4551)
* docs: quote pip extras install examples by [@RachGranville](https://github.com/RachGranville) in [#4568](https://github.com/PrefectHQ/fastmcp/pull/4568)
* Document Windows CI parallelism and the subprocess_heavy marker by [@jlowin](https://github.com/jlowin) in [#4575](https://github.com/PrefectHQ/fastmcp/pull/4575)
* Document v3->v4 removals and add upgrade-reality tests by [@jlowin](https://github.com/jlowin) in [#4585](https://github.com/PrefectHQ/fastmcp/pull/4585)
* Archive v3 docs and publish v4 as the primary version by [@jlowin](https://github.com/jlowin) in [#4613](https://github.com/PrefectHQ/fastmcp/pull/4613)
* Document targeted v4 prerelease installation by [@zzstoatzz](https://github.com/zzstoatzz) in [#4598](https://github.com/PrefectHQ/fastmcp/pull/4598)
* Fix stale Mac/Windows-vs-Linux OAuth key/storage docs by [@jlowin](https://github.com/jlowin) in [#4617](https://github.com/PrefectHQ/fastmcp/pull/4617)
* v4 docs quality pass: stale task/era claims, broken links, polish by [@jlowin](https://github.com/jlowin) in [#4619](https://github.com/PrefectHQ/fastmcp/pull/4619)
* whats-new: add the argument completion capability by [@jlowin](https://github.com/jlowin) in [#4620](https://github.com/PrefectHQ/fastmcp/pull/4620)
* docs: fix ProxyProvider docstring example calling nonexistent with_namespace() by [@andrew-stelmach-fleet](https://github.com/andrew-stelmach-fleet) in [#4633](https://github.com/PrefectHQ/fastmcp/pull/4633)
* Unpublish v4 development notes; prep docs for beta 1 by [@jlowin](https://github.com/jlowin) in [#4644](https://github.com/PrefectHQ/fastmcp/pull/4644)
* Expand the FAQ for the v4 transition by [@jlowin](https://github.com/jlowin) in [#4649](https://github.com/PrefectHQ/fastmcp/pull/4649)
* Document the issuer_url identity change for upgraders by [@jlowin](https://github.com/jlowin) in [#4658](https://github.com/PrefectHQ/fastmcp/pull/4658)
* Cover require_roles in the v4 highlights by [@jlowin](https://github.com/jlowin) in [#4666](https://github.com/PrefectHQ/fastmcp/pull/4666)
* Fix FAQ: sampling/roots/elicitation legacy-mode advice, SessionProvider registration by [@jlowin](https://github.com/jlowin) in [#4672](https://github.com/PrefectHQ/fastmcp/pull/4672)
* Audit v4 docs: fix missing version badges, fill whats-new gaps by [@jlowin](https://github.com/jlowin) in [#4668](https://github.com/PrefectHQ/fastmcp/pull/4668)
* Docs: add v3.4.5 changelog entries to main by [@jlowin](https://github.com/jlowin) in [#4674](https://github.com/PrefectHQ/fastmcp/pull/4674)
* Split the SDK upgrade guides by SDK version by [@jlowin](https://github.com/jlowin) in [#4684](https://github.com/PrefectHQ/fastmcp/pull/4684)
### Dependencies 📦
* chore(deps): bump mcp from 1.26.0 to 1.27.2 in /examples/testing_demo in the uv group across 1 directory by [@dependabot](https://github.com/apps/dependabot) in [#4514](https://github.com/PrefectHQ/fastmcp/pull/4514)
* chore(deps): bump actions/setup-node from 6 to 7 by [@dependabot](https://github.com/apps/dependabot) in [#4546](https://github.com/PrefectHQ/fastmcp/pull/4546)
* Bump actions/upload-artifact from 4 to 7 by [@dependabot](https://github.com/apps/dependabot) in [#4640](https://github.com/PrefectHQ/fastmcp/pull/4640)
* Bump actions/setup-python from 6 to 7 by [@dependabot](https://github.com/apps/dependabot) in [#4641](https://github.com/PrefectHQ/fastmcp/pull/4641)
* chore(deps): bump mcp from 1.27.2 to 1.28.1 in /examples/testing_demo in the uv group across 1 directory by [@dependabot](https://github.com/apps/dependabot) in [#4614](https://github.com/PrefectHQ/fastmcp/pull/4614)
### Other Changes 🦾
* Test: HTTP lifespan fires once per process across sessions by [@jlowin](https://github.com/jlowin) in [#4470](https://github.com/PrefectHQ/fastmcp/pull/4470)
## New Contributors
* @syf2211 made their first contribution in [#4502](https://github.com/PrefectHQ/fastmcp/pull/4502)
* @earfman made their first contribution in [#4517](https://github.com/PrefectHQ/fastmcp/pull/4517)
* @winklemad made their first contribution in [#4492](https://github.com/PrefectHQ/fastmcp/pull/4492)
* @verdie-g made their first contribution in [#4512](https://github.com/PrefectHQ/fastmcp/pull/4512)
* @vijaydeepsinha made their first contribution in [#4411](https://github.com/PrefectHQ/fastmcp/pull/4411)
* @piaudonn made their first contribution in [#4267](https://github.com/PrefectHQ/fastmcp/pull/4267)
* @szepeviktor made their first contribution in [#4498](https://github.com/PrefectHQ/fastmcp/pull/4498)
* @endofcake made their first contribution in [#4412](https://github.com/PrefectHQ/fastmcp/pull/4412)
* @Kludex made their first contribution in [#4564](https://github.com/PrefectHQ/fastmcp/pull/4564)
* @RachGranville made their first contribution in [#4568](https://github.com/PrefectHQ/fastmcp/pull/4568)
* @hxaxd made their first contribution in [#4533](https://github.com/PrefectHQ/fastmcp/pull/4533)
* @VectorPeak made their first contribution in [#4531](https://github.com/PrefectHQ/fastmcp/pull/4531)
* @LarryHu0217 made their first contribution in [#4587](https://github.com/PrefectHQ/fastmcp/pull/4587)
* @andrew-stelmach-fleet made their first contribution in [#4633](https://github.com/PrefectHQ/fastmcp/pull/4633)
* @Aman071106 made their first contribution in [#4628](https://github.com/PrefectHQ/fastmcp/pull/4628)
* @ShuyingZhang made their first contribution in [#4639](https://github.com/PrefectHQ/fastmcp/pull/4639)
* @Educg550 made their first contribution in [#4660](https://github.com/PrefectHQ/fastmcp/pull/4660)
**Full Changelog**: [v3.4.5...v4.0.0b1](https://github.com/PrefectHQ/fastmcp/compare/v3.4.5...v4.0.0b1)
</Update>
<Update label="v3.4.5" description="2026-07-27">
**[v3.4.5: Key Change](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.5)**
FastMCP 3.4.5 collects five fixes for the 3.x line, led by `JWTVerifier` no longer rejecting every token when an authorization server publishes an unrecognized key type such as Ed25519.
### Fixes 🐞
* Backport #4517 to release/3.x: skip unsupported JWKS keys (#4515) by [@kakiii](https://github.com/kakiii) in [#4631](https://github.com/PrefectHQ/fastmcp/pull/4631)
* Backport #4469 to release/3.x: fix Azure scope fallback by [@jlowin](https://github.com/jlowin) in [#4662](https://github.com/PrefectHQ/fastmcp/pull/4662)
* Backport #4523 to release/3.x: serialize deep object query parameters by [@jlowin](https://github.com/jlowin) in [#4664](https://github.com/PrefectHQ/fastmcp/pull/4664)
* Backport #4564 to release/3.x: make transformed tool required order deterministic by [@jlowin](https://github.com/jlowin) in [#4665](https://github.com/PrefectHQ/fastmcp/pull/4665)
* Backport #4492 to release/3.x: don't mutate the caller's schema in compress_schema by [@jlowin](https://github.com/jlowin) in [#4663](https://github.com/PrefectHQ/fastmcp/pull/4663)
## New Contributors
* @kakiii made their first contribution in [#4631](https://github.com/PrefectHQ/fastmcp/pull/4631)
**Full Changelog**: [v3.4.4...v3.4.5](https://github.com/PrefectHQ/fastmcp/compare/v3.4.4...v3.4.5)
</Update>
<Update label="v3.4.4" description="2026-07-08">
**[v3.4.4: Host in Translation](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.4)**

View file

@ -89,10 +89,10 @@ To skip authentication entirely — useful for local development servers — pas
fastmcp call http://localhost:8000/mcp my_tool --auth none
```
You can also pass a bearer token directly:
You can also pass a bearer token directly. Give the token value on its own; FastMCP adds the `Bearer` prefix when it builds the `Authorization` header.
```bash
fastmcp list http://localhost:8000/mcp --auth "Bearer sk-..."
fastmcp list http://localhost:8000/mcp --auth "sk-..."
```
## Transport Override

View file

@ -37,7 +37,7 @@ async with Client(
"https://your-server.fastmcp.app/mcp",
auth="<your-token>",
) as client:
await client.ping()
await client.list_tools()
```
You can also supply a Bearer token to a transport instance, such as `StreamableHttpTransport` or `SSETransport`:
@ -52,7 +52,7 @@ transport = StreamableHttpTransport(
)
async with Client(transport) as client:
await client.ping()
await client.list_tools()
```
## `BearerAuth` Helper
@ -67,7 +67,7 @@ async with Client(
"https://your-server.fastmcp.app/mcp",
auth=BearerAuth(token="<your-token>"),
) as client:
await client.ping()
await client.list_tools()
```
## Custom Headers
@ -84,5 +84,5 @@ async with Client(
headers={"X-API-Key": "<your-token>"},
),
) as client:
await client.ping()
await client.list_tools()
```

View file

@ -32,7 +32,7 @@ async with Client(
client_metadata_url="https://myapp.example.com/oauth/client.json",
),
) as client:
await client.ping()
await client.list_tools()
```
When the server supports CIMD, the client uses your metadata URL as its `client_id` instead of performing Dynamic Client Registration. The server fetches your document, validates it, and proceeds with the standard OAuth authorization flow.

View file

@ -0,0 +1,89 @@
---
title: Machine-to-Machine Authentication
sidebarTitle: Client Credentials
description: Authenticate your FastMCP client to a protected server without a browser.
icon: robot
---
import { VersionBadge } from "/snippets/version-badge.mdx"
<VersionBadge version="4.0.0" />
<Tip>
Machine-to-machine authentication is only relevant for HTTP-based transports.
</Tip>
When a FastMCP client runs without a human present — a backend service, a scheduled job, a CI pipeline, one MCP server calling another — it cannot complete the browser-based [OAuth](/clients/auth/oauth) flow. Instead it authenticates as itself using the OAuth 2.0 **client credentials** grant: the client presents its own credentials directly to the authorization server, receives an access token, and attaches that token to every request. There is no redirect, no consent screen, and no user.
FastMCP provides two providers for this, both implementing the `httpx2.Auth` interface so they drop into the same `auth=` parameter as every other client auth option. You pass the **MCP server URL**, not a token endpoint — the token endpoint is discovered from the server's OAuth metadata, exactly as the interactive `OAuth` helper does. As with `OAuth`, you can omit the URL entirely and let the transport supply it.
## Client ID and Secret
The common case is a pre-registered client with an ID and a secret. Use `ClientCredentialsOAuthProvider` and pass it to the `auth` parameter of your `Client` or transport:
```python {2, 4-8, 10}
from fastmcp import Client
from fastmcp.client.auth import ClientCredentialsOAuthProvider
auth = ClientCredentialsOAuthProvider(
client_id="my-client-id",
client_secret="my-client-secret",
scopes=["read", "write"],
)
async with Client("https://example.com/mcp", auth=auth) as client:
await client.list_tools()
```
The provider discovers the authorization server, exchanges the credentials for an access token, and caches the token in memory for the life of the client. When the token expires it is re-acquired automatically on the next request. Because re-acquiring a token is a single non-interactive request, tokens are held in memory by default with no warning — unlike the interactive `OAuth` flow, losing the cache on restart costs nothing.
### `ClientCredentialsOAuthProvider` Parameters
- **`mcp_url`** (`str`, optional): Full URL to the MCP endpoint. Omit it when passing the provider to `Client(auth=...)` — the transport supplies the URL automatically.
- **`client_id`** (`str`, required): The pre-registered OAuth client ID.
- **`client_secret`** (`str`, required): The OAuth client secret.
- **`scopes`** (`str | list[str]`, optional): Scopes to request, as a space-separated string or a list.
- **`token_endpoint_auth_method`** (`"client_secret_basic" | "client_secret_post"`, optional): How the credentials are presented to the token endpoint. Defaults to `"client_secret_basic"` (an HTTP Basic `Authorization` header); use `"client_secret_post"` to send them in the request body instead.
- **`token_storage`** (`AsyncKeyValue`, optional): A key-value store for the acquired token. Defaults to in-memory storage.
## Private Key JWT
Some authorization servers require the client to prove its identity with a signed JWT assertion (RFC 7523 `private_key_jwt`) instead of a shared secret. This is common with workload identity federation, where the assertion comes from a cloud identity provider. Use `PrivateKeyJWTOAuthProvider` and supply an `assertion_provider` — an async callback that receives the authorization server's issuer identifier (the required JWT audience) and returns the assertion.
For a locally signed assertion, build the callback with `SignedJWTParameters`:
```python {4-7, 9, 11-15, 17-20, 22}
from pathlib import Path
from fastmcp import Client
from fastmcp.client.auth import (
PrivateKeyJWTOAuthProvider,
SignedJWTParameters,
)
private_key_pem = Path("client-signing-key.pem").read_text()
jwt_params = SignedJWTParameters(
issuer="my-client-id",
subject="my-client-id",
signing_key=private_key_pem,
)
auth = PrivateKeyJWTOAuthProvider(
client_id="my-client-id",
assertion_provider=jwt_params.create_assertion_provider(),
)
async with Client("https://example.com/mcp", auth=auth) as client:
await client.list_tools()
```
If you already have a JWT from an identity provider, wrap it with `static_assertion_provider`, or pass your own `async def provider(audience: str) -> str` callback to fetch one on demand.
### `PrivateKeyJWTOAuthProvider` Parameters
- **`mcp_url`** (`str`, optional): Full URL to the MCP endpoint. Omit it when passing the provider to `Client(auth=...)`.
- **`client_id`** (`str`, required): The OAuth client ID.
- **`assertion_provider`** (`Callable[[str], Awaitable[str]]`, required): Async callback that receives the authorization server's issuer identifier and returns a signed JWT assertion.
- **`scopes`** (`str | list[str]`, optional): Scopes to request, as a space-separated string or a list.
- **`token_storage`** (`AsyncKeyValue`, optional): A key-value store for the acquired token. Defaults to in-memory storage.

View file

@ -29,7 +29,7 @@ from fastmcp import Client
# Uses default OAuth settings
async with Client("https://your-server.fastmcp.app/mcp", auth="oauth") as client:
await client.ping()
await client.list_tools()
```
@ -44,7 +44,7 @@ from fastmcp.client.auth import OAuth
oauth = OAuth(scopes=["user"])
async with Client("https://your-server.fastmcp.app/mcp", auth=oauth) as client:
await client.ping()
await client.list_tools()
```
<Note>
@ -125,7 +125,7 @@ encrypted_storage = FernetEncryptionWrapper(
oauth = OAuth(token_storage=encrypted_storage)
async with Client("https://your-server.fastmcp.app/mcp", auth=oauth) as client:
await client.ping()
await client.list_tools()
```
You can use any `AsyncKeyValue`-compatible backend from the [key-value library](https://github.com/strawgate/py-key-value) including Redis, DynamoDB, and more. Wrap your storage in `FernetEncryptionWrapper` for encryption.
@ -150,7 +150,7 @@ async with Client(
client_metadata_url="https://myapp.example.com/oauth/client.json",
),
) as client:
await client.ping()
await client.list_tools()
```
See the [CIMD Authentication](/clients/auth/cimd) page for complete documentation on creating, hosting, and validating CIMD documents.
@ -172,7 +172,7 @@ async with Client(
client_secret="my-client-secret",
),
) as client:
await client.ping()
await client.list_tools()
```
Public clients that rely on PKCE for security can omit `client_secret`:

View file

@ -37,9 +37,6 @@ client = Client("my_mcp_server.py")
async def main():
async with client:
# Basic server interaction
await client.ping()
# List available operations
tools = await client.list_tools()
resources = await client.list_resources()
@ -126,7 +123,7 @@ async with client:
## Connection Lifecycle
The client uses context managers for connection management. When you enter the context, the client establishes a connection and performs an MCP initialization handshake with the server. This handshake exchanges capabilities, server metadata, and instructions.
The client uses context managers for connection management. When you enter the context, the client establishes a connection and negotiates the protocol era with the server. Metadata returned by either legacy initialization or modern discovery is exposed through the same client properties.
```python
from fastmcp import Client, FastMCP
@ -139,18 +136,20 @@ def greet(name: str) -> str:
return f"Hello, {name}!"
async with Client(mcp) as client:
# Initialization already happened automatically
print(f"Server: {client.initialize_result.server_info.name}")
print(f"Instructions: {client.initialize_result.instructions}")
print(f"Capabilities: {client.initialize_result.capabilities.tools}")
# Protocol negotiation already happened automatically
assert client.server_info is not None
assert client.server_capabilities is not None
print(f"Server: {client.server_info.name}")
print(f"Instructions: {client.instructions}")
print(f"Capabilities: {client.server_capabilities.tools}")
```
For advanced scenarios where you need precise control over when initialization happens, disable automatic initialization and call `initialize()` manually:
For advanced scenarios where you need precise control over when initialization happens, disable automatic initialization and call `initialize()` manually. `initialize()` is a handshake-era operation, so pin the connection with `mode="legacy"`: the modern protocol has no `initialize` round trip, and calling it on a modern connection raises.
```python
from fastmcp import Client
client = Client("my_mcp_server.py", auto_initialize=False)
client = Client("my_mcp_server.py", auto_initialize=False, mode="legacy")
async with client:
# Connection established, but not initialized yet
@ -171,20 +170,26 @@ async with client:
MCP has two protocol eras: the original *legacy* era, which begins every connection with an `initialize` handshake, and the *modern* era (protocol version `2026-07-28` and later), which a client discovers by probing the server's `server/discover` endpoint. The `mode` parameter controls which era the client negotiates when it connects.
By default, `mode="legacy"`. This runs the initialize handshake and behaves identically to earlier FastMCP versions, so existing code connecting to any server keeps working unchanged.
By default, `mode="auto"`. The client probes `server/discover` and adopts the modern protocol when the server responds; for any server that is not positive evidence of modern support, it falls back to the legacy handshake. This makes the default safe against a mixed fleet of legacy and modern servers.
```python
from fastmcp import Client
# Legacy handshake (the default)
# Negotiate the newest era the server supports (the default)
client = Client("https://example.com/mcp", mode="auto")
```
Set `mode="legacy"` to force the initialize handshake. This behaves identically to earlier FastMCP versions and is the opt-out if a server misbehaves under discovery or you need the legacy `initialize` result object.
```python
client = Client("https://example.com/mcp", mode="legacy")
```
Set `mode="auto"` to negotiate the newest era the server supports. The client probes `server/discover` and adopts the modern protocol when the server responds; for any server that is not positive evidence of modern support, it falls back to the legacy handshake. This makes `"auto"` safe to use against a mixed fleet of legacy and modern servers.
Legacy mode is also what carries the *pushed* form of a server's requests. The handshake opens a persistent back-channel down which a server can send a sampling, roots, or elicitation request mid-call, and the modern era removed it. Your handlers are unaffected by that: a [sampling](/clients/sampling), [roots](/clients/roots), or [elicitation](/clients/elicitation) handler you register answers a modern server's [input-required rounds](/clients/elicitation#input-required-rounds) from the same registration. Pin `mode="legacy"` when you connect to a server that pushes, or when your code calls `client.ping()` or `transport.get_session_id()`, which need the session the modern era does not open.
```python
client = Client("https://example.com/mcp", mode="auto")
```
Conversely, [background tasks](/clients/tasks) are **modern-only**: the tasks capability is negotiated over `2026-07-28` connections, so `mode="legacy"` never triggers one and a task-enabled tool just runs synchronously.
A FastMCP server serves both eras, so a default client negotiates the modern one and the session-dependent calls raise an era-specific error there. Pinning the handshake restores them.
You can also pin a specific modern protocol version to adopt it directly, without a discovery probe:
@ -192,23 +197,29 @@ You can also pin a specific modern protocol version to adopt it directly, withou
client = Client("https://example.com/mcp", mode="2026-07-28")
```
Once connected, the negotiated version and the server's advertised capabilities are available as properties. Both are populated regardless of which era was negotiated, and both are `None` while the client is disconnected.
Once connected, the negotiated version, server identity, capabilities, and instructions are available as properties. They are populated from either the legacy `InitializeResult` or modern `DiscoverResult`, and reset to `None` when the client disconnects. `instructions` is also `None` when the server does not provide any.
When you pin a modern version directly, the client skips discovery and adopts that version with minimal synthesized metadata. In that mode, `server_info` has an empty name and `instructions` is `None`.
```python
async with Client("https://example.com/mcp", mode="auto") as client:
print(client.protocol_version) # e.g. "2026-07-28"
print(client.server_info) # Implementation | None
print(client.server_capabilities) # ServerCapabilities | None
print(client.instructions) # str | None
```
<Note>
`mode="auto"` is not the default yet — the conservative `"legacy"` remains the default to preserve byte-identical behavior against pre-2026 servers. Whether `"auto"` becomes the default is a future release decision.
`mode="auto"` is the default as of FastMCP 4.0. Earlier versions defaulted to `"legacy"`. If a server behaves unexpectedly under discovery, or you depend on the legacy `initialize` result, pin the old behavior with `Client(..., mode="legacy")`.
The SSE transport is legacy-only — it cannot carry the sessionless modern era — so a client connecting over SSE always negotiates the legacy handshake, even under `mode="auto"`. A multi-server config (`MCPConfigTransport` with more than one server) is likewise legacy-only, because it mounts each backend behind a legacy-era proxy; a single-server config mirrors its one backend transport's era.
</Note>
## Response caching
<VersionBadge version="4.0.0" />
The client can cache the results of `list_tools`, `list_resources`, `list_prompts`, and `read_resource` so that repeated calls avoid a network round-trip. Caching is opt-in and honors the server's own cache hints, so it only takes effect against modern-era servers that advertise them — a cache is inert on a legacy connection.
The client can cache the results of `list_tools`, `list_resources`, and `list_prompts` so that repeated calls avoid a network round-trip. Caching is opt-in and honors the server's own cache hints, so it only takes effect against modern-era servers that advertise them — a cache is inert on a legacy connection.
Enable the default in-memory cache by passing `cache=True`. It respects the `ttlMs` and `cacheScope` hints the server attaches to each response.
@ -232,7 +243,7 @@ config = CacheConfig(target_id="weather-api", default_ttl_ms=60_000)
client = Client("https://example.com/mcp", mode="auto", cache=config)
```
The high-level `list_tools`, `list_resources`, `list_prompts`, and `read_resource` methods always use the cache when one is configured. To override the behavior for a single call, use the lower-level `*_mcp` variants, which accept a `cache_mode` argument: `"use"` (the default) serves and stores, `"refresh"` stores a fresh result without serving a cached one, and `"bypass"` skips the cache entirely.
The high-level `list_tools`, `list_resources`, and `list_prompts` methods always use the cache when one is configured. To override the behavior for a single call, use the lower-level `list_tools_mcp`, `list_resources_mcp`, `list_resource_templates_mcp`, and `list_prompts_mcp` variants, which accept a `cache_mode` argument: `"use"` (the default) serves and stores, `"refresh"` stores a fresh result without serving a cached one, and `"bypass"` skips the cache entirely.
```python
async with client:
@ -260,6 +271,31 @@ client = Client("https://example.com/mcp", mode="auto", cache=config)
The adapter serializes each result through a type-tagged envelope validated against an allowlist of cacheable result models, so a value naming an unknown type is treated as a cache miss rather than deserialized blindly. Each store instance owns its own collection namespace; `clear()` affects only that namespace, never another tenant's entries.
## Client extensions
<VersionBadge version="4.0.0" />
Client extensions (SEP-2133) are the advanced mechanism a client uses to opt into vendor capabilities that live outside the core protocol. An extension is a `ClientExtension` instance that bundles three things: a capability *advertisement* the server can read, one or more *result claims* that let the client parse extra `tools/call` result shapes, and *notification bindings* that observe server notifications the core protocol doesn't define. Pass a sequence of them to `extensions=`.
```python
from fastmcp import Client
from myproject.extensions import AppsExtension
client = Client("https://example.com/mcp", extensions=[AppsExtension()])
```
Each extension's contributions are threaded into the underlying session. FastMCP folds in its own internal extension for [background tasks](/clients/tasks) automatically, and your own extensions *compose* with it rather than replacing it — pass your own tasks extension with the same identifier if you need to override it. When a tool returns a shape an extension claims, `client.call_tool()` resolves it transparently through the owning claim's resolver and hands you back an ordinary result. Result claims and their advertisements are honored only on modern-era connections, so they are inert on a legacy handshake.
For the rare case where you need to register additional result claims against an extension that is already advertised, pass them through `result_claims=`, keyed by the extension's identifier. Prefer declaring claims on the extension itself; this parameter merges extra claims with an extension's own.
```python
client = Client(
"https://example.com/mcp",
extensions=[AppsExtension()],
result_claims={"example.com/apps": [extra_claim]},
)
```
## Operations
FastMCP clients interact with three types of server components.
@ -301,6 +337,8 @@ See [Prompts](/clients/prompts) for detailed documentation including argument se
The client supports callback handlers for advanced server interactions. These let you respond to server-initiated requests and receive notifications.
Sampling, elicitation, and roots are the requests a server makes of the client. A server reaches your handler by whichever route its [era](#protocol-negotiation) allows — pushed down the open session on the handshake, returned as an input-required result on the modern protocol — and both routes dispatch to the same handler, so one registration covers both. Logging and progress arrive as notifications on the response stream and work in either era.
```python
from fastmcp import Client
from fastmcp.client.logging import LogMessage

View file

@ -13,6 +13,10 @@ Use this when you need to respond to server requests for user input during tool
Elicitation allows MCP servers to request structured input from users during operations. Instead of requiring all inputs upfront, servers can interactively ask for missing parameters, request clarification, or gather additional context.
<Note>
**These sections show the server-initiated flow, which the handshake-era protocol uses.** On `2026-07-28` the server asks by returning a request instead — see [input-required rounds](#input-required-rounds). One `elicitation_handler` serves both, so the examples below pin `mode="legacy"` only to exercise the pushed form.
</Note>
## Handler Template
```python
@ -54,6 +58,7 @@ async def elicitation_handler(
client = Client(
"my_mcp_server.py",
mode="legacy",
elicitation_handler=elicitation_handler,
)
```
@ -138,6 +143,7 @@ async def elicitation_handler(message, response_type, params, context):
client = Client(
"my_mcp_server.py",
mode="legacy",
elicitation_handler=elicitation_handler
)
```
@ -146,7 +152,7 @@ client = Client(
<VersionBadge version="4.0.0" />
On modern-era connections (protocol version `2026-07-28` and later), a server can ask for input before it returns a final result. Nothing is held open: the tool *returns* a description of what it needs, which completes that round as an ordinary response, and the client answers by issuing a **new** `call_tool`, `get_prompt`, or `read_resource` request carrying the answer. `fastmcp.Client` drives that loop for you — it fulfils each round's requests using the callbacks you already configured (your `elicitation_handler`, `sampling_handler`, and roots) and repeats until the call reaches a terminal result. No extra wiring is needed beyond the handlers described above.
On protocol version `2026-07-28` and later, a server can ask for input before it returns a final result. Nothing is held open: the tool *returns* a description of what it needs, which completes that round as an ordinary response, and the client answers by issuing a **new** `call_tool`, `get_prompt`, or `read_resource` request carrying the answer. `fastmcp.Client` drives that loop for you — it fulfils each round's requests using the callbacks you already configured (your `elicitation_handler`, `sampling_handler`, and roots) and repeats until the call reaches a terminal result. No extra wiring is needed beyond the handlers described above.
The `input_required_max_rounds` parameter caps how many rounds the client will answer before giving up, guarding against a server that never terminates. It defaults to `10`.

View file

@ -28,7 +28,16 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
LOGGING_LEVEL_MAP = logging.getLevelNamesMapping()
LOGGING_LEVEL_MAP = {
"DEBUG": logging.DEBUG,
"INFO": logging.INFO,
"NOTICE": logging.INFO,
"WARNING": logging.WARNING,
"ERROR": logging.ERROR,
"CRITICAL": logging.CRITICAL,
"ALERT": logging.CRITICAL,
"EMERGENCY": logging.CRITICAL,
}
async def log_handler(message: LogMessage):
"""Forward MCP server logs to Python's logging system."""

View file

@ -47,23 +47,23 @@ For fine-grained targeting, subclass `MessageHandler` to use specific hooks:
```python
from fastmcp import Client
from fastmcp.client.messages import MessageHandler
import mcp_types
import mcp.types
class MyMessageHandler(MessageHandler):
async def on_tool_list_changed(
self, notification: mcp_types.ToolListChangedNotification
self, notification: mcp.types.ToolListChangedNotification
) -> None:
"""Handle tool list changes."""
print("Tool list changed - refreshing available tools")
async def on_resource_list_changed(
self, notification: mcp_types.ResourceListChangedNotification
self, notification: mcp.types.ResourceListChangedNotification
) -> None:
"""Handle resource list changes."""
print("Resource list changed")
async def on_prompt_list_changed(
self, notification: mcp_types.PromptListChangedNotification
self, notification: mcp.types.PromptListChangedNotification
) -> None:
"""Handle prompt list changes."""
print("Prompt list changed")
@ -78,7 +78,7 @@ client = Client(
```python
from fastmcp.client.messages import MessageHandler
import mcp_types
import mcp.types
class MyMessageHandler(MessageHandler):
async def on_message(self, message) -> None:
@ -86,49 +86,49 @@ class MyMessageHandler(MessageHandler):
pass
async def on_notification(
self, notification: mcp_types.ServerNotification
self, notification: mcp.types.ServerNotification
) -> None:
"""Called for notifications (fire-and-forget)."""
pass
async def on_tool_list_changed(
self, notification: mcp_types.ToolListChangedNotification
self, notification: mcp.types.ToolListChangedNotification
) -> None:
"""Called when the server's tool list changes."""
pass
async def on_resource_list_changed(
self, notification: mcp_types.ResourceListChangedNotification
self, notification: mcp.types.ResourceListChangedNotification
) -> None:
"""Called when the server's resource list changes."""
pass
async def on_prompt_list_changed(
self, notification: mcp_types.PromptListChangedNotification
self, notification: mcp.types.PromptListChangedNotification
) -> None:
"""Called when the server's prompt list changes."""
pass
async def on_progress(
self, notification: mcp_types.ProgressNotification
self, notification: mcp.types.ProgressNotification
) -> None:
"""Called for progress updates during long-running operations."""
pass
async def on_resource_updated(
self, notification: mcp_types.ResourceUpdatedNotification
self, notification: mcp.types.ResourceUpdatedNotification
) -> None:
"""Called when a specific resource changes."""
pass
async def on_cancelled(
self, notification: mcp_types.CancelledNotification
self, notification: mcp.types.CancelledNotification
) -> None:
"""Called when a request is cancelled."""
pass
async def on_logging_message(
self, notification: mcp_types.LoggingMessageNotification
self, notification: mcp.types.LoggingMessageNotification
) -> None:
"""Called for log messages from the server."""
pass
@ -141,14 +141,14 @@ A practical example of maintaining a tool cache that refreshes when tools change
```python
from fastmcp import Client
from fastmcp.client.messages import MessageHandler
import mcp_types
import mcp.types
class ToolCacheHandler(MessageHandler):
def __init__(self):
self.cached_tools = []
async def on_tool_list_changed(
self, notification: mcp_types.ToolListChangedNotification
self, notification: mcp.types.ToolListChangedNotification
) -> None:
"""Clear tool cache when tools change."""
print("Tools changed - clearing cache")

View file

@ -21,7 +21,7 @@ Request a rendered prompt with `get_prompt()`:
async with client:
# Simple prompt without arguments
result = await client.get_prompt("welcome_message")
# result -> fastmcp.types.GetPromptResult
# result -> mcp_types.GetPromptResult
# Access the generated messages
for message in result.messages:
@ -143,5 +143,5 @@ For complete control, use `get_prompt_mcp()` which returns the full MCP protocol
```python
async with client:
result = await client.get_prompt_mcp("example_prompt", {"arg": "value"})
# result -> fastmcp.types.GetPromptResult
# result -> mcp_types.GetPromptResult
```

View file

@ -58,18 +58,25 @@ async with client:
Binary resources include images, PDFs, and other non-text data:
Binary resources arrive as `BlobResourceContents`, whose `blob` field is a base64 **string**, so decode it before writing bytes to disk:
```python
import base64
from mcp_types import BlobResourceContents
async with client:
content = await client.read_resource("resource://images/logo.png")
for item in content:
if hasattr(item, 'blob'):
print(f"Binary content: {len(item.blob)} bytes")
if isinstance(item, BlobResourceContents):
data = base64.b64decode(item.blob)
print(f"Binary content: {len(data)} bytes")
print(f"MIME type: {item.mime_type}")
# Save to file
with open("downloaded_logo.png", "wb") as f:
f.write(item.blob)
f.write(data)
```
## Multi-Server Clients
@ -106,5 +113,5 @@ For complete control, use `read_resource_mcp()` which returns the full MCP proto
```python
async with client:
result = await client.read_resource_mcp("resource://example")
# result -> fastmcp.types.ReadResourceResult
# result -> mcp_types.ReadResourceResult
```

View file

@ -1,7 +1,7 @@
---
title: Client Roots
sidebarTitle: Roots
description: Provide local context and resource boundaries to MCP servers.
description: Tell servers which local paths your client can reach.
icon: folder-tree
---
@ -11,24 +11,26 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
Use this when you need to tell servers what local resources the client has access to.
Roots inform servers about resources the client can provide. Servers can use this information to adjust behavior or provide more relevant responses.
A root is a path your client is willing to expose — a project directory, a workspace, a document store. Servers read them to scope their work, so a tool that searches files searches where you pointed it, and a server that gets no roots has to ask the user for paths instead. Roots describe where the client can reach; the server takes them as its working boundary.
Register them once with `roots=`, and the client answers however the server asks. A handshake-era server pushes a `roots/list` request down the open session and reads the reply mid-call; a modern (`2026-07-28`) server has no such channel, so it returns a roots request and `fastmcp.Client` fulfils it from the same registration and re-issues the call with the answer attached. The default `mode="auto"` negotiates whichever era the server speaks, so the examples below work on either — see [protocol negotiation](/clients/client#protocol-negotiation) for how that choice is made, and [the guard pattern](/servers/elicitation#sampling-and-roots) for how a server issues the modern form.
## Static Roots
Provide a list of roots when creating the client:
When the paths are known up front, pass them as a list. The client holds them for the life of the connection and hands back the same set every time a server asks.
```python
from fastmcp import Client
client = Client(
"my_mcp_server.py",
roots=["/path/to/root1", "/path/to/root2"]
roots=["file:///path/to/root1", "file:///path/to/root2"]
)
```
## Dynamic Roots
Use a callback to compute roots dynamically when the server requests them:
Pass a callback instead when the roots depend on something the client learns at runtime, such as the workspace the user has open. It runs at the moment a server asks, on either route, and receives the request context so you can see which request it is answering:
```python
from fastmcp import Client
@ -36,7 +38,7 @@ from fastmcp.client.roots import RequestContext
async def roots_callback(context: RequestContext) -> list[str]:
print(f"Server requested roots (Request ID: {context.request_id})")
return ["/path/to/root1", "/path/to/root2"]
return ["file:///path/to/root1", "file:///path/to/root2"]
client = Client(
"my_mcp_server.py",

View file

@ -1,7 +1,7 @@
---
title: LLM Sampling
sidebarTitle: Sampling
description: Handle server-initiated LLM completion requests.
description: Answer a server's request for an LLM completion.
icon: robot
---
@ -9,52 +9,46 @@ import { VersionBadge } from "/snippets/version-badge.mdx";
<VersionBadge version="2.0.0" />
Use this when you need to respond to server requests for LLM completions.
Use this when a server asks your client to run an LLM completion on its behalf.
MCP servers can request LLM completions from clients during tool execution. This enables servers to delegate AI reasoning to the client, which controls which LLM is used and how requests are made.
Sampling is how a server borrows your model. Rather than hold an API key of its own, the server describes the messages it wants completed and asks you to run them — you pick the model, and you pay for the tokens. Your side of that arrangement is one function, a **sampling handler**, registered when you create the client.
The handler receives the conversation the server wants completed, the parameters it asked for, and a request context carrying metadata about the call. Return the generated text as a string and FastMCP wraps it in the protocol's result for you; return a `CreateMessageResult` yourself when you want to report the real model name or hand back content that isn't text. If the handler raises, the client sends the error back in place of a completion and the server's tool decides what to do about it.
## Handler Template
```python
from fastmcp import Client
from fastmcp.client.sampling import SamplingMessage, SamplingParams, RequestContext
from mcp.types import TextContent
async def sampling_handler(
messages: list[SamplingMessage],
params: SamplingParams,
context: RequestContext
context: RequestContext,
) -> str:
"""
Handle server requests for LLM completions.
Args:
messages: Conversation messages to send to the LLM
params: Sampling parameters (temperature, max_tokens, etc.)
context: Request context with metadata
Returns:
Generated text response from your LLM
"""
# Extract message content
conversation = []
for message in messages:
content = message.content.text if hasattr(message.content, 'text') else str(message.content)
conversation.append(f"{message.role}: {content}")
# Use the system prompt if provided
"""Run the server's messages against your LLM and return the completion."""
conversation = [
f"{message.role}: {message.content.text}"
for message in messages
if isinstance(message.content, TextContent)
]
system_prompt = params.system_prompt or "You are a helpful assistant."
# Integrate with your LLM service here
# Call your LLM here with `conversation` and `system_prompt`.
return "Generated response based on the messages"
client = Client(
"my_mcp_server.py",
sampling_handler=sampling_handler,
)
client = Client("my_mcp_server.py", sampling_handler=sampling_handler)
```
The client answers with this handler however the server asks for a completion. The default `mode="auto"` negotiates whichever protocol era the server speaks, and one handler covers both of the routes an era can use — see [Request Routes](#request-routes).
## Handler Parameters
Everything the server sends arrives in the first two arguments. The messages are the conversation to complete; the parameters are how the server would like it completed. You decide how much of that to honor, since the client owns the model — a preference your provider cannot express is yours to ignore.
<Card icon="code" title="SamplingMessage">
<ResponseField name="role" type='Literal["user", "assistant"]'>
The role of the message
@ -66,11 +60,11 @@ client = Client(
</Card>
<Card icon="code" title="SamplingParams">
<ResponseField name="systemPrompt" type="str | None">
<ResponseField name="system_prompt" type="str | None">
Optional system prompt the server wants to use
</ResponseField>
<ResponseField name="modelPreferences" type="ModelPreferences | None">
<ResponseField name="model_preferences" type="ModelPreferences | None">
Server preferences for model selection (hints, cost/speed/intelligence priorities)
</ResponseField>
@ -78,11 +72,11 @@ client = Client(
Sampling temperature
</ResponseField>
<ResponseField name="maxTokens" type="int">
<ResponseField name="max_tokens" type="int">
Maximum tokens to generate
</ResponseField>
<ResponseField name="stopSequences" type="list[str] | None">
<ResponseField name="stop_sequences" type="list[str] | None">
Stop sequences for sampling
</ResponseField>
@ -90,14 +84,14 @@ client = Client(
Tools the LLM can use during sampling
</ResponseField>
<ResponseField name="toolChoice" type="ToolChoice | None">
<ResponseField name="tool_choice" type="ToolChoice | None">
Tool usage behavior (`auto`, `required`, or `none`)
</ResponseField>
</Card>
## Built-in Handlers
FastMCP provides built-in handlers for OpenAI, Anthropic, and Google Gemini APIs that support the full sampling API including tool use.
Writing the provider call yourself is rarely worth it. FastMCP ships handlers for OpenAI, Anthropic, and Google Gemini that implement the full sampling API, tool use included, and translate the protocol's parameters into each provider's own. Give one a default model and pass it where your own handler would go. Write a custom handler when you need routing across providers, caching, or a provider FastMCP does not cover.
### OpenAI Handler
@ -113,9 +107,11 @@ client = Client(
)
```
For OpenAI-compatible APIs (like local models):
Point the handler at any OpenAI-compatible API, including a local model server, by passing your own provider client:
```python
from fastmcp import Client
from fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler
from openai import AsyncOpenAI
client = Client(
@ -128,7 +124,7 @@ client = Client(
```
<Note>
Install the OpenAI handler with `pip install fastmcp[openai]`.
Install the OpenAI handler with `pip install 'fastmcp[openai]'`.
</Note>
### Anthropic Handler
@ -146,7 +142,7 @@ client = Client(
```
<Note>
Install the Anthropic handler with `pip install fastmcp[anthropic]`.
Install the Anthropic handler with `pip install 'fastmcp[anthropic]'`.
</Note>
### Google Gemini Handler
@ -164,27 +160,35 @@ client = Client(
```
<Note>
Install the Google Gemini handler with `pip install fastmcp[gemini]`.
Install the Google Gemini handler with `pip install 'fastmcp[gemini]'`.
</Note>
## Sampling Capabilities
The [source of these handlers](https://github.com/PrefectHQ/fastmcp/tree/main/fastmcp_slim/fastmcp/client/sampling/handlers) is the best reference for writing your own.
When you provide a `sampling_handler`, FastMCP automatically advertises full sampling capabilities to the server, including tool support. To disable tool support for simpler handlers:
## Tool Use
A sampling request can carry tools. When it does, your handler passes them to the model and returns whatever comes back, tool calls included — the server executes the tools itself and sends a follow-up sampling request with the results if it needs another turn. Your handler never runs a tool.
Registering any `sampling_handler` advertises full sampling support, tools included. A handler that only generates text should say so, so servers know not to send tools it will drop:
```python
from fastmcp.types import SamplingCapability
from fastmcp import Client
from mcp.types import SamplingCapability
async def text_only_handler(messages, params, context) -> str:
return "Generated response based on the messages"
client = Client(
"my_mcp_server.py",
sampling_handler=basic_handler,
sampling_capabilities=SamplingCapability(), # No tool support
sampling_handler=text_only_handler,
sampling_capabilities=SamplingCapability(),
)
```
## Tool Execution
## Request Routes
Tool execution happens on the server side. The client's role is to pass tools to the LLM and return the LLM's response (which may include tool use requests). The server then executes the tools and may send follow-up sampling requests with tool results.
Servers reach your handler by two routes, and which one applies depends on the protocol era the connection negotiated. A handshake-era server pushes a `sampling/createMessage` request down the open session while a tool is running and waits for the reply. A modern (`2026-07-28`) connection has no such channel, so the tool ends its round by returning a request for a completion instead; the client answers from your handler and calls the tool again with the result attached.
<Tip>
To implement a custom sampling handler, see the [handler source code](https://github.com/PrefectHQ/fastmcp/tree/main/fastmcp_slim/fastmcp/client/sampling/handlers) as a reference.
</Tip>
One registration covers both, so this is rarely something you configure — it matters only when you pin an era, since `mode="legacy"` is the sole route that carries a pushed request. See [protocol negotiation](/clients/client#protocol-negotiation) for how the era is chosen, and [Sampling](/servers/sampling) under Servers for how a server issues these requests.

View file

@ -1,180 +1,138 @@
---
title: Background Tasks
sidebarTitle: Tasks
description: Execute operations asynchronously and track their progress.
description: Call long-running tools without blocking, and answer questions they ask mid-run.
icon: clock
tag: "NEW"
---
import { VersionBadge } from "/snippets/version-badge.mdx"
<VersionBadge version="2.14.0" />
<VersionBadge version="4.0.0" />
Use this when you need to run long operations asynchronously while doing other work.
Some tool calls take a while. The MCP background tasks extension lets a server run one in the background instead of holding the request open, and FastMCP's client drives the whole thing for you — most of the time you don't need to know a call was tasked at all.
The MCP task protocol lets you request operations to run in the background. The call returns a Task object immediately, letting you track progress, cancel operations, or await results.
<Note>
**Client task support is opt-in.** Install the `fastmcp-tasks` package (`pip install "fastmcp[tasks]"`) and import it — importing `fastmcp_tasks` anywhere (which you do to use `call_tool_task`) enables task support for every `Client` in the process. Without it, a `Client` never advertises the tasks capability, so the server runs its calls synchronously and background tasks simply don't happen.
## Requesting Background Execution
**Tasks also require the modern protocol.** The capability is negotiated over `2026-07-28` connections. `mode="auto"` (the client default) negotiates it automatically; `mode="legacy"` never does. See [protocol negotiation](/clients/client#protocol-negotiation).
</Note>
Pass `task=True` to run an operation as a background task:
## Transparent Calls
With task support enabled, just call the tool. If the server runs it as a background task, `call_tool` polls it to completion under the hood and returns the same result you'd get from a synchronous call — the task is invisible.
```python
import fastmcp_tasks # enables client task support
from fastmcp import Client
async with Client(server, mode="auto") as client:
result = await client.call_tool("slow_computation", {"duration": 10})
print(result.data)
```
This is the right default for most code: it works whether or not the server actually tasks the call, so you can write ordinary tool-calling code without checking server capabilities.
## Driving a Task Explicitly
When you want to do other work while a task runs — or check on it, or cancel it — use `call_tool_task` instead. It returns a `ToolTask` handle immediately rather than waiting for completion.
```python
from fastmcp import Client
from fastmcp_tasks import call_tool_task
async with Client(server) as client:
# Start a background task
task = await client.call_tool("slow_computation", {"duration": 10}, task=True)
async with Client(server, mode="auto") as client:
task = await call_tool_task(client, "slow_computation", {"duration": 10})
print(f"Task started: {task.task_id}")
# Do other work while it runs...
# Get the result when ready
result = await task.result()
```
This works with tools, resources, and prompts:
```python
tool_task = await client.call_tool("my_tool", args, task=True)
resource_task = await client.read_resource("file://large.txt", task=True)
prompt_task = await client.get_prompt("my_prompt", args, task=True)
```
## Task API
All task types share a common interface.
### Getting Results
Call `await task.result()` or simply `await task` to block until the task completes:
```python
task = await client.call_tool("analyze", {"text": "hello"}, task=True)
# Wait for result (blocking)
result = await task.result()
# or: result = await task
```
`call_tool_task` requires the server to actually run the call as a task — if the tool isn't `task=True`, or the server doesn't have the tasks extension registered, it raises `ToolError`. Use it when you specifically need the handle; use `call_tool` when you just want the result.
### Checking Status
Check the current status without blocking:
```python
status = await task.status()
print(f"{status.status}: {status.statusMessage}")
print(f"{status.status}: {status.status_message}")
# status.status is "working", "input_required", "completed", "failed", or "cancelled"
```
### Waiting with Control
Use `task.wait()` for more control over waiting. With no `state`, it returns when the task leaves `working`; pass a specific state when you need to wait for a particular transition:
`task.wait()` polls until a terminal state (or a specific one you name), without answering any input the task asks for — use it when you want to observe an `input_required` pause yourself rather than have it answered automatically.
```python
# Wait up to 30 seconds for completion
status = await task.wait(timeout=30.0)
# Wait for a specific state
status = await task.wait(state="completed", timeout=30.0)
status = await task.wait(state="input_required", timeout=30.0)
```
### Cancellation
### Getting the Result
Cancel a running task:
`task.result()` drives the task the rest of the way — including answering any input it asks for — and returns the finished result, same as `client.call_tool` would. Awaiting the task directly is shorthand for this.
```python
result = await task.result()
# or: result = await task
```
By default a failed or cancelled task raises `ToolError`. Pass `raise_on_error=False` to `call_tool_task` to get an error result back instead.
### Cancellation
```python
await task.cancel()
```
## Status Updates
Cancellation is cooperative — the task may still finish before the server notices the request.
Register callbacks to receive real-time status updates as the server reports progress:
## Answering Questions Mid-Task
```python
def on_status_change(status):
print(f"Task {status.taskId}: {status.status} - {status.statusMessage}")
task.on_status_change(on_status_change)
# Async callbacks work too
async def on_status_async(status):
await log_status(status)
task.on_status_change(on_status_async)
```
### Handler Template
A task can pause partway through to ask a question, the same way a foreground [multi-round-trip](/clients/elicitation#input-required-rounds) tool does. Pass an `elicitation_handler` and both `call_tool` and `task.result()` answer it automatically as part of driving the task to completion:
```python
from fastmcp import Client
def status_handler(status):
"""
Handle task status updates.
async def handle_elicitation(message, response_type, params, context):
return {"cuisine": "Thai", "vegetarian": True}
Args:
status: Task status object with:
- taskId: Unique task identifier
- status: "working", "input_required", "completed", "failed", or "cancelled"
- statusMessage: Optional progress message from server
"""
if status.status == "working":
print(f"Progress: {status.statusMessage}")
elif status.status == "completed":
print("Task completed")
elif status.status == "failed":
print(f"Task failed: {status.statusMessage}")
task.on_status_change(status_handler)
async with Client(server, mode="auto", elicitation_handler=handle_elicitation) as client:
result = await client.call_tool("plan_dinner", {})
print(result.data)
```
## Graceful Degradation
You can always pass `task=True` regardless of whether the server supports background tasks. Per the MCP specification, servers without task support execute the operation immediately and return the result inline.
```python
task = await client.call_tool("my_tool", args, task=True)
if task.returned_immediately:
print("Server executed immediately (no background support)")
else:
print("Running in background")
# Either way, this works
result = await task.result()
```
This lets you write task-aware client code without worrying about server capabilities.
Without an `elicitation_handler`, a task that asks for input raises `ToolError` rather than hanging. See [server-side background tasks](/servers/tasks#gathering-input-mid-task) for how a tool asks a question in the first place.
## Example
Putting it together, here is a client that submits a background task with `call_tool_task` and awaits its result:
```python
import asyncio
from fastmcp import Client
from fastmcp_tasks import call_tool_task
async def main():
async with Client(server) as client:
# Start background task
task = await client.call_tool(
"slow_computation",
{"duration": 10},
task=True,
)
async with Client(server, mode="auto") as client:
# Return immediately and drive the task yourself
task = await call_tool_task(client, "slow_computation", {"duration": 10})
print(f"Task started: {task.task_id}")
# Subscribe to updates
def on_update(status):
print(f"Progress: {status.statusMessage}")
# Do other work while the task runs
while True:
status = await task.status()
if status.status in ("completed", "failed", "cancelled"):
break
print(f"Still working... ({status.status})")
await asyncio.sleep(1)
task.on_status_change(on_update)
# Do other work while task runs
print("Doing other work...")
await asyncio.sleep(2)
# Wait for completion and get result
result = await task.result()
print(f"Result: {result.content}")
print(f"Result: {result.data}")
asyncio.run(main())
```

View file

@ -80,7 +80,7 @@ async with client:
Fully hydrated Python objects with complex type support (datetimes, UUIDs, custom classes). FastMCP exclusive.
</ResponseField>
<ResponseField name=".content" type="list[fastmcp.types.ContentBlock]">
<ResponseField name=".content" type="list[mcp_types.ContentBlock]">
Standard MCP content blocks (`TextContent`, `ImageContent`, `AudioContent`, etc.).
</ResponseField>
@ -173,7 +173,7 @@ For complete control, use `call_tool_mcp()` which returns the raw MCP protocol o
```python
async with client:
result = await client.call_tool_mcp("my_tool", {"param": "value"})
# result -> fastmcp.types.CallToolResult
# result -> mcp_types.CallToolResult
if result.is_error:
print(f"Tool failed: {result.content}")

View file

@ -86,7 +86,7 @@ client = Client(transport)
async def efficient_multiple_operations():
async with client:
await client.ping()
await client.list_tools()
async with client: # Reuses the same subprocess
await client.call_tool("process_data", {"file": "data.csv"})

View file

@ -0,0 +1,57 @@
/* Language dropdown: injected by language-dropdown.js into the sidebar
footer, to the right of Mintlify's theme selector. Mirrors the almond
theme pill's exact metrics (lg:h-7 desktop / 2.375rem mobile, rounded-full,
border-gray-200/70, dark:border-white/[0.07]) so the two controls read as
one family. */
#language-switch {
margin-left: auto;
display: inline-flex;
align-items: center;
}
#language-switch select {
appearance: none;
-webkit-appearance: none;
background-color: transparent;
border: 1px solid rgb(229 231 235 / 0.7);
border-radius: 9999px;
color: rgb(107 114 128);
cursor: pointer;
font-size: 0.75rem;
line-height: 1rem;
height: 2.375rem;
padding: 0 1.375rem 0 0.75rem;
/* Chevron, drawn in the same gray as the label text. */
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.5rem center;
background-size: 0.7rem;
transition: border-color 0.2s;
}
@media (min-width: 1024px) {
#language-switch select {
height: 1.75rem;
}
}
#language-switch select:hover {
color: rgb(75 85 99);
border-color: rgb(229 231 235);
}
#language-switch select:focus-visible {
outline: 2px solid rgb(45 0 247 / 0.4);
outline-offset: 1px;
}
.dark #language-switch select {
border-color: rgb(255 255 255 / 0.07);
color: rgb(156 163 175);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}
.dark #language-switch select:hover {
color: rgb(209 213 219);
border-color: rgb(255 255 255 / 0.1);
}

View file

@ -57,6 +57,42 @@ h6 code:not(pre code) {
background: linear-gradient(135deg, #2d00f7 0%, #4cc9f0 100%);
}
/* V3 banner - inside content-container, breaks out of padding with negative margins */
#v3-banner {
display: block;
background: linear-gradient(135deg, #4cc9f0 0%, #2d00f7 100%);
color: white;
text-align: center;
padding: 10px 16px;
font-size: 0.875rem;
font-weight: 600;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin: -2rem -2rem 1.5rem -2rem;
width: calc(100% + 4rem);
border-radius: 8px 8px 0 0;
}
#v3-banner a {
color: white;
text-decoration: underline;
font-weight: 700;
}
#v3-banner a:hover {
opacity: 0.9;
}
@media (min-width: 1024px) {
#v3-banner {
margin: -3rem -4rem 1.5rem -4rem;
width: calc(100% + 8rem);
}
}
.dark #v3-banner {
background: linear-gradient(135deg, #2d00f7 0%, #4cc9f0 100%);
}

View file

@ -103,7 +103,7 @@ If you're mounting an authenticated server under a path prefix, see [Mounting Au
### Host and Origin Protection
FastMCP can validate `Host` and browser `Origin` headers for Streamable HTTP requests before they reach MCP session handling. This request guard protects localhost-bound servers from DNS rebinding attacks, and it remains opt-in in FastMCP 3.x to preserve compatibility with existing ASGI, serverless, and reverse-proxy deployments.
FastMCP can validate `Host` and browser `Origin` headers for Streamable HTTP requests before they reach MCP session handling. This request guard protects localhost-bound servers from DNS rebinding attacks, and it stays opt-in to preserve compatibility with existing ASGI, serverless, and reverse-proxy deployments.
Think of this as a request guard rather than CORS middleware. It decides whether a request can reach MCP session handling. CORS remains a separate browser response-header policy; configure CORS middleware separately when browser JavaScript must read cross-origin responses.
@ -149,6 +149,48 @@ export FASTMCP_HTTP_ALLOWED_ORIGINS='["https://app.example.com"]'
Use `host_origin_protection="auto"` to protect localhost-bound direct servers while allowing ASGI, serverless, and reverse-proxy deployments to keep their existing Host handling unless they configure explicit trust rules. Use `host_origin_protection=False` to keep the request guard disabled.
### Gateway Routing Headers
<VersionBadge version="4.0.0" />
A gateway, load balancer, or reverse proxy in front of your MCP server often needs to route a request before it reads the JSON-RPC body — the body may be an SSE stream, or the gateway may simply want to avoid parsing it. On a connection that negotiates the modern `2026-07-28` protocol, Streamable HTTP clients built on the MCP Python SDK (including FastMCP's own client) attach routing information to each request as HTTP headers so an intermediary can dispatch on headers alone:
- `Mcp-Method` carries the JSON-RPC method (for example `tools/call`) on every request.
- `Mcp-Name` carries the target's name on named operations — the tool name for `tools/call`, the prompt name for `prompts/get`, the resource URI for `resources/read`.
- `Mcp-Param-*` carries selected argument values for a `tools/call`, one header per opted-in parameter.
FastMCP's HTTP transport neither strips nor rewrites these headers, so a gateway sees them exactly as the client sent them. The `Host`/`Origin` request guard inspects only `Host` and `Origin` and leaves the routing headers untouched.
<Warning>
These headers are a feature of the modern `2026-07-28` protocol. A client connected over an earlier protocol revision — including one running in legacy mode or one that has fallen back to a legacy server — sends no routing headers at all. Design gateway routing to require the headers rather than assume their presence: if a request arrives without them, fall back to inspecting the body or route it to a default backend, rather than dropping it.
</Warning>
To expose an argument as an `Mcp-Param-*` header, annotate the parameter with the `x-mcp-header` JSON Schema extension. FastMCP carries the annotation into the tool's advertised input schema, and a conforming client mirrors the argument into a header named `Mcp-Param-<token>`:
```python
from typing import Annotated
from pydantic import Field
from fastmcp import FastMCP
mcp = FastMCP("My Server")
@mcp.tool
def query_tenant(
tenant: Annotated[str, Field(json_schema_extra={"x-mcp-header": "Tenant"})],
sql: str,
) -> str:
"""A call to this tool sends the tenant value as an `Mcp-Param-Tenant` header."""
...
```
A gateway can now route on `Mcp-Param-Tenant` — for example, pinning each tenant to a dedicated backend — without inspecting the request body. The annotation is only permitted on `string`, `integer`, and `boolean` parameters. These headers advertise routing intent; treat them as untrusted hints, since the server still validates the request body as the source of truth.
<Tip>
When you put a FastMCP [proxy](/servers/providers/proxy) in front of another server, the proxy re-advertises each backend tool's `x-mcp-header` annotation, so routing headers work across the proxy hop as well. The headers themselves are regenerated per hop rather than forwarded verbatim, since each describes a single HTTP request.
</Tip>
### Health Checks
Health check endpoints are essential for monitoring your deployed server and ensuring it's responding correctly. FastMCP allows you to add custom routes alongside your MCP endpoints, making it easy to implement health checks that work with both deployment approaches.
@ -504,7 +546,7 @@ base_url="http://localhost:8000/api" # Includes mount prefix
mcp_path="/mcp" # Internal MCP path, NOT the mount prefix
```
**`issuer_url`** (optional) controls the authorization server identity for OAuth discovery. Defaults to `base_url`.
**`issuer_url`** (optional) controls the authorization server identity for OAuth discovery. Defaults to `base_url`. It sets the `issuer` advertised in the authorization server metadata and the `iss` on issued tokens, while the endpoints in that metadata continue to point at `base_url`.
```python
# Usually not needed - just set base_url and it works
@ -658,7 +700,7 @@ When deploying FastMCP behind a load balancer or running multiple server instanc
#### Understanding Sessions
By default, FastMCP's Streamable HTTP transport maintains server-side sessions. Sessions enable stateful MCP features like [elicitation](/servers/elicitation) and [sampling](/servers/sampling), where the server needs to maintain context across multiple requests from the same client.
By default, FastMCP's Streamable HTTP transport maintains server-side sessions. A session holds the context a server keeps across multiple requests from the same client, and it carries the handshake-era back-channel that server-initiated requests like [elicitation](/servers/elicitation) push down.
This works perfectly for single-instance deployments. However, sessions are stored in memory on each server instance, which creates challenges when scaling horizontally.
@ -741,9 +783,7 @@ If you're using the [OAuth Proxy](/servers/auth/oauth-proxy), FastMCP issues its
**Default Behavior (Development Only):**
By default, FastMCP automatically manages cryptographic keys:
- **Mac/Windows**: Keys are generated and stored in your system keyring, surviving server restarts. Suitable **only** for development and local testing.
- **Linux**: Keys are ephemeral (random salt at startup), so tokens are invalidated on restart.
By default, FastMCP automatically manages cryptographic keys the same way on every platform: the signing key is deterministically derived from your OAuth client secret, so it survives server restarts as long as the secret doesn't change. Suitable **only** for development and local testing.
This automatic approach is convenient for development but not suitable for production deployments.

View file

@ -5,7 +5,7 @@ description: The MCP platform from the FastMCP team
icon: cloud
---
[Prefect Horizon](https://www.prefect.io/horizon) is a platform for deploying and managing MCP servers. Built by the FastMCP team at [Prefect](https://www.prefect.io), Horizon provides managed hosting, authentication, access control, and a registry of MCP capabilities.
[Prefect Horizon](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_horizon&utm_content=guide_intro) is a platform for deploying and managing MCP servers. Built by the FastMCP team at [Prefect](https://www.prefect.io), Horizon provides managed hosting, authentication, access control, and a registry of MCP capabilities.
Horizon includes a **free personal tier for FastMCP users**, making it the fastest way to get a secure, production-ready server URL with built-in OAuth authentication.

View file

@ -39,30 +39,33 @@ The `fastmcp.json` configuration answers three fundamental questions about your
This conceptual model helps you understand the purpose of each configuration section and organize your settings effectively. The configuration file maps directly to these three concerns:
`source` is the *where*, `environment` the *what*, and `deployment` the *how*:
```json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
// WHERE: Location of your server code
"type": "filesystem", // Optional, defaults to "filesystem"
"type": "filesystem",
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
// WHAT: Environment setup and dependencies
"type": "uv", // Optional, defaults to "uv"
"type": "uv",
"python": ">=3.10",
"dependencies": ["pandas", "numpy"]
},
"deployment": {
// HOW: Runtime configuration
"transport": "stdio",
"log_level": "INFO"
}
}
```
Only the `source` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed.
Only the `source` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed. Both `type` fields shown above are optional too, defaulting to `"filesystem"` and `"uv"` respectively.
<Warning>
`fastmcp.json` is parsed as strict JSON, so it accepts no comments or trailing commas.
</Warning>
### JSON Schema Support

View file

@ -53,8 +53,8 @@ We expect this exemption to last through at least the 2.12.x and 2.13.x release
Pin to exact versions:
```
fastmcp==2.11.0 # Good
fastmcp>=2.11.0 # Bad - will install breaking changes
fastmcp==4.0.0 # Good
fastmcp>=4.0.0 # Bad - will install breaking changes
```
## Creating Releases
@ -65,7 +65,7 @@ Our release process is intentionally simple:
2. Generate release notes automatically, and curate or add additional editorial information as needed
3. GitHub releases automatically trigger PyPI deployments
Current-major releases target `main`. Maintenance releases target their release branch, such as `release/3.x` for 3.x patches and `release/2.x` for 2.x patches. Stable releases from `main` update the `published-docs` branch after PyPI publishing succeeds; maintenance releases publish packages and GitHub release notes without repointing the live docs branch.
Current-major releases target `main`. Maintenance releases target their release branch, such as `release/3.x` for 3.x patches and `release/2.x` for 2.x patches. Stable releases from `main` open a PR that syncs the release commit to `published-docs` after PyPI publishing succeeds; merging that PR publishes the live docs. Prereleases skip the automatic PR and use the same PR-based sync when their docs are ready to publish. Maintenance releases publish packages and GitHub release notes without repointing the live docs branch.
This automation lets maintainers focus on code quality rather than release mechanics.

View file

@ -26,7 +26,7 @@ uv run pytest --cov=fastmcp
uv run pytest -m "not integration"
# Skip tests that spawn processes
uv run pytest -m "not integration and not client_process"
uv run pytest -m "not integration and not client_process and not subprocess_heavy"
```
Tests should complete in under 1 second unless marked as integration tests. This speed encourages running them frequently, catching issues early.
@ -61,6 +61,40 @@ async def test_stdio_transport():
assert result.content[0].text == "test"
```
A third marker, `subprocess_heavy`, exists specifically for Windows CI stability. See [Windows CI and Test Parallelism](#windows-ci-and-test-parallelism) below for when to use it and why it exists.
### Windows CI and Test Parallelism
Windows CI ran the unit suite serially for months. [#2715](https://github.com/PrefectHQ/fastmcp/pull/2715) tried enabling `pytest-xdist` parallelism there in December 2025; [#2726](https://github.com/PrefectHQ/fastmcp/pull/2726) reverted it the next day because "Windows tests continue to fail with intermittent worker crashes." [#4554](https://github.com/PrefectHQ/fastmcp/pull/4554) re-enabled it after removing most of the subprocess pressure that caused those crashes, taking the Windows unit step from roughly 460s to 175s.
That pressure came from three sources, all addressed by #4554: most HTTP tests moved in-process via `asgi_client` instead of binding real sockets, stdio lifecycle tests spawn a minimal stdlib responder (`tests/client/minimal_stdio_server.py`, ~0.03s to start) instead of a subprocess that runs `import fastmcp` (~0.7s), and roughly 80 real `sleep()` calls became deterministic waits on the condition each test actually cared about. Fewer, cheaper subprocesses competing under parallel workers left fewer chances for a worker to die.
#### The `subprocess_heavy` marker
One class of test still spawns a full Python interpreter that imports FastMCP — checking that a bare install doesn't need optional dependencies, or that a decorator works from a fresh process. Each spawn pays a full interpreter's startup and memory footprint, and a 2-core Windows runner already running 2 xdist workers has little headroom left to absorb that. These tests carry `@pytest.mark.subprocess_heavy` and run in the existing serial `client_process` CI step instead of alongside the parallel workers — `.github/actions/run-pytest/action.yml` routes `client_process or subprocess_heavy` to that step (`MAX_PROCS=0`) and excludes both markers from the parallel unit step.
If a test runs `subprocess.run([sys.executable, "-c", ...])`, or otherwise starts a fresh interpreter that imports `fastmcp`, mark it `subprocess_heavy`. A subprocess that runs a minimal stdlib script with no FastMCP import doesn't need the marker — it's the interpreter startup and import that's expensive, not the subprocess itself.
#### This is a mitigation, not a proof
There is no root-cause diagnosis behind this fix, only a plausible one. During validation, one Windows run genuinely crashed a worker on `test_fastmcp_imports_without_legacy_httpx` — a fresh-interpreter test — with pytest-xdist reporting `worker 'gw1' crashed while running '...'` after execnet's channel saw `ConnectionResetError: [WinError 10054]`. Nothing in that log says *why* the worker died: memory exhaustion, handle exhaustion, and some Windows-specific `subprocess`/`execnet` interaction are all still consistent with what was observed. Marking the fresh-interpreter tests `subprocess_heavy` made the crash stop recurring, but "it stopped" is not the same as "we know why."
Treat the next Windows worker crash as a test of this diagnosis. **If it lands on a test that is not a fresh-interpreter spawner, the `subprocess_heavy` theory was wrong** — the real problem is subprocess-under-xdist on Windows more generally, and isolating one marker's worth of tests was never going to fix that. The fallback is one conditional back in `run-pytest/action.yml`, restoring the pre-#4554 behavior:
```bash
PARALLEL_FLAGS=""
if [ "$MAX_PROCS" != "0" ] && [ "${{ runner.os }}" != "Windows" ]; then
PARALLEL_FLAGS="--numprocesses auto --maxprocesses $MAX_PROCS --dist worksteal"
fi
```
#### Two traps that aren't Windows-specific
Two test-authoring bugs surfaced while validating this change. Neither is about Windows or parallelism, but both are worth watching for anywhere a real `sleep()` gets replaced with a wait:
- **Match the wait condition to the assertion.** A test waited for "any callback fired," then asserted that a `completed` callback existed. That races, because an earlier `working` notification satisfies the wait before the `completed` one arrives. A deterministic wait is only as good as the condition it waits on — wait for the thing you actually assert.
- **Don't assert on incidental timing.** A crash-recovery test asserted "at least one concurrent request fails" while a subprocess restarts, which quietly depended on the restart being slow. Once restart got faster, recovery could beat every in-flight request and the test started failing because the behavior *improved*. Assert the invariant instead: no hang, and no result served by the dead process.
## Writing Tests
@ -335,7 +369,7 @@ async def test_tenant_header_is_visible_to_tools():
headers={"X-Tenant-ID": "acme"},
timeout=5,
) as client:
await client.ping()
await client.list_tools()
```
#### Sharing One Server Across Tests
@ -364,11 +398,16 @@ async def test_greet(http_server: ASGIServer):
assert greeting.data == "Hello, World!"
async def test_sessions_are_isolated(http_server: ASGIServer):
async with http_server.client() as first, http_server.client() as second:
async with (
http_server.client(mode="legacy") as first,
http_server.client(mode="legacy") as second,
):
assert await first.ping() is True
assert await second.ping() is True
```
Sessions belong to the handshake era of the MCP protocol, and so does `ping`, so a test that is about session behavior pins `mode="legacy"`. Every keyword argument `client()` doesn't consume itself is passed straight to `Client`. See [protocol negotiation](/clients/client#protocol-negotiation).
For assertions about raw HTTP — status codes, response headers, metadata endpoints — `http_client()` returns an `httpx.AsyncClient` bound to the same app. Because nothing is listening on the network, this is the only way to make raw requests; a plain `httpx.AsyncClient()` cannot reach the server.
```python
@ -396,7 +435,7 @@ async def test_server_binds_a_real_port():
async with run_server_async(server) as url:
assert url.startswith("http://127.0.0.1:")
async with Client(url) as client:
assert await client.ping() is True
assert await client.list_tools() == []
```
#### Subprocess Testing (Special Cases)
@ -430,8 +469,8 @@ async def test_http_transport(http_server: str):
async with Client(
transport=StreamableHttpTransport(http_server)
) as client:
result = await client.ping()
assert result is True
tools = await client.list_tools()
assert "greet" in [tool.name for tool in tools]
```
The `run_server_in_process` utility handles server lifecycle, port allocation, and cleanup automatically. Use this only when subprocess isolation is truly necessary, as it's slower and harder to debug than in-process testing. FastMCP uses the `client_process` marker to isolate these tests in CI.

View file

@ -1,131 +0,0 @@
---
title: Feature Program
---
The migration is the foundation. The forward v4 program is a sequence of post-merge PRs that build on it. Each feature below carries an explicit status:
- **Designed** — the approach is settled and an API sketch exists; implementation has not started.
- **Planned** — the shape is agreed but design details remain open.
- **Not started** — identified as v4 scope, not yet designed.
Code blocks marked as sketches show the *intended* API and do not resolve against the current tree.
## Sampling: deprecate now, remove in 4.0
**Status: Designed.**
Sampling is the push-shaped API where a server borrows the client's model mid-call (`ctx.sample`, `ctx.sample_step`). The `2026-07-28` era removes server-initiated requests, so this API cannot work on modern connections. Background-task sampling is already dead under v2 — a worker's back-channel is gone once the submitting request returns, and no sampling relay was ever built (sdk-feedback #9).
The plan is Option A: **deprecate the push-sampling API now and remove it in the 4.0 release.**
- Deprecate `ctx.sample` / `ctx.sample_step` and the server sampling module now.
- Era-gate them to raise a clear error on `2026-07-28` (this also fixes the opaque "Method not found" of sdk-feedback #10).
- Remove `ctx.sample`, `ctx.sample_step`, `server/sampling/`, `SamplingTool`, and structured-result sampling in 4.0.
The migration story is honest: there is **no drop-in** on modern connections. The guidance is architectural — call an LLM from your server directly, with your own API key, rather than borrowing the client's model. That shift is the real answer, and it is why the removal justifies a major version.
The client-side provider handlers (Anthropic, OpenAI, Google GenAI) are **retained** regardless: MRTR needs them to answer sampling input-requests from the client side. What is removed is the server-side push emitter, which the SDK never built for the modern era.
In this PR, sampling still functions on the legacy eras. Users already see an SDK-level `MCPDeprecationWarning` on ordinary `ctx.sample` usage (the SDK deprecated the capability wire-side per SEP-2577, verified empirically by WS2), but FastMCP's own deprecation — warnings with migration guidance, plus the era-gating — lands as the first follow-up PR.
## MRTR elicitation
**Status: Guard form shipped (4.0). Declarative `Resolve` layer designed.**
Elicitation survives the modern era through multi-round-trip (MRTR). The 2026 wire envelope carries elicitation as a multi-round input-request: a tool returns an `InputRequiredResult` and re-runs per round, each round a complete request→response cycle. Imperative `ctx.elicit` relies on the session back-channel, which is gone on `2026-07-28` foreground calls; on the modern era, elicitation is reachable through MRTR instead.
The **guard form** of this is shipped in 4.0 (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)): a tool returns an `InputRequiredResult` and reads the client's answers off `ctx.input_responses` / `ctx.request_state`, re-running each round. It mirrors the SDK's base guard model exactly — no FastMCP-invented DX, the framework owns `request_state` sealing, and returning this result on a handshake-era connection produces a clear era error. The declarative `Resolve(...)` layer below sits *on top of* this primitive and remains designed but not yet built.
The design does both, so the imperative DX survives where it can and a declarative surface covers the modern era:
**1. Keep `ctx.elicit` as the primary imperative DX,** re-plumbed to be era-aware: legacy connections use the session elicit-form path; background tasks on any era use the existing Redis relay (the task's `input_required` status *is* the MRTR suspension boundary); foreground calls on `2026-07-28` raise a clear era-aware error pointing at the declarative form.
**2. Add a declarative surface** in a new `fastmcp.elicitation` module — `Resolve`, `Elicit`, and `ElicitationResult` — thin wrappers over the SDK's resolver, wired into FastMCP's own tool layer (FastMCP tools do not inherit the SDK's auto-resolver wiring).
The intended DX (sketch — the module does not exist yet):
```python test="skip"
from typing import Annotated
from pydantic import BaseModel
from fastmcp import FastMCP, Context
from fastmcp.elicitation import Resolve, Elicit, ElicitationResult
mcp = FastMCP("shipping")
class Address(BaseModel):
street: str
city: str
zip: str
async def ask_address(ctx: Context) -> Elicit[Address]:
return Elicit("Where should we ship this order?", Address)
@mcp.tool
async def create_shipment(
order_id: str,
address: Annotated[Address, Resolve(ask_address)], # unwrapped; decline -> ToolError
) -> str:
return f"Shipping {order_id} to {address.city}"
@mcp.tool
async def maybe_ship(
order_id: str,
address: Annotated[ElicitationResult[Address], Resolve(ask_address)], # full outcome
) -> str:
if address.action != "accept":
return "cancelled"
return f"Shipping {order_id} to {address.data.city}"
@mcp.tool(task=True)
async def slow_ship(ctx: Context) -> str:
# imperative ctx.elicit survives 2026 via the background-task relay
result = await ctx.elicit("Confirm address", Address)
if result.action == "accept":
return f"Shipping to {result.data.city}"
return "cancelled"
```
The registration path detects `Annotated[_, Resolve(...)]` parameters, builds resolver plans, and returns the SDK's `InputRequiredResult` instead of the tool body on the first round. The FastMCP client already dispatches input-requests through its elicitation callback; the follow-up work confirms the FastMCP client wrapper drives the input-required driver the way the SDK's own client does.
The divergence between elicitation and sampling on 2026 comes down to one fact: the SDK built the server-side emitter for elicitation (`Elicit`/`Resolve`) and not for sampling. The wire carries all three input-request types and the client dispatches all three; only elicitation can produce one server-side. That is why elicitation survives 4.0 via MRTR and push-sampling does not.
## Middleware on the SDK `ServerMiddleware` seam
**Status: Planned.**
The migration already routes `initialize` interception through the SDK's new `ServerMiddleware` seam via `FastMCPServerMiddleware`. The forward work is to lean into that seam more fully — moving more of FastMCP's request-lifecycle middleware onto the native SDK composition point rather than FastMCP-side wrappers, now that the SDK composes middleware around every request and notification.
## First-class 2026 client
**Status: Planned.**
The migration keeps `fastmcp.Client` as a wrapper around `mcp.ClientSession` in legacy/handshake mode. The v4 client work adopts the SDK's first-class `mcp.client.Client`: a `mode='auto'` that negotiates the era, `discover()` for sessionless capability discovery, and the MRTR input-required driver so the client can answer multi-round elicitation and sampling input-requests. This is the client-side half of full `2026-07-28` support.
This workstream also owns the server-side statelessness design holes — `ctx.session_id` / `set_state` round-tripping, task push and background elicitation, and stateful-proxy affinity — since all three turn on the same "what is a session without a session?" question. See [Statelessness on 2026-07-28](/development/v4-notes/known-gaps#statelessness-on-2026-07-28) for the full accounting.
## Subscriptions, cache hints, extensions, OTel
**Status: Not started.**
A cluster of protocol features tracked for v4 once the core client and elicitation work lands: a `subscriptions/listen` surface backed by a subscription bus, resource cache hints, reconciliation of the `extensions` / MCP Apps capability advertisement across eras (the `extensions` capability is stripped at pre-2026 negotiated versions today — sdk-feedback #2), and the OpenTelemetry integration re-checked against the SDK's own OTel middleware.
## SDK delegation, round two
**Status: Planned (gated on upstream).**
The real HTTP simplification is a v4 project, not this PR. FastMCP can collapse its `create_streamable_http_app` onto the SDK's `Server.streamable_http_app()` once upstream adds three things:
1. per-session event-store scoping,
2. a user-middleware injection hook,
3. a lifespan hook.
The payoff is not only less code — FastMCP would also inherit the SDK's session-owner credential enforcement, a security gain it lacks today. These are the three upstream feature requests to file (alongside the advisory dossier described in [Known Gaps](/development/v4-notes/known-gaps)). Until they land, the four HTTP overrides in the [Change Register](/development/v4-notes/change-register#http) stay.
One latent capability worth surfacing on FastMCP's side: `session_idle_timeout` is accepted by the manager but never set by `create_streamable_http_app` — a one-line plumb if FastMCP wants to expose it.

View file

@ -16,7 +16,7 @@
"dark": "#475569",
"light": "#1e3a5f"
},
"content": "Meet [Prefect Horizon](https://prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_banner&utm_content=sitewide_banner), the enterprise MCP gateway built by the team behind FastMCP"
"content": "FastMCP 4 is in beta — build stateful applications on sessionless MCP. [See what's new](/getting-started/whats-new)."
},
"colors": {
"dark": "#f72585",
@ -67,7 +67,7 @@
"label": ""
},
{
"href": "https://prefect.io/horizon",
"href": "https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_horizon&utm_content=header",
"icon": "cloud",
"label": "Prefect Horizon"
}
@ -89,7 +89,8 @@
"pages": [
"getting-started/welcome",
"getting-started/installation",
"getting-started/quickstart"
"getting-started/quickstart",
"getting-started/whats-new"
]
},
{
@ -144,6 +145,7 @@
"pages": [
"servers/elicitation",
"servers/sampling",
"servers/completions",
"servers/progress",
"servers/logging",
"servers/pagination",
@ -159,6 +161,8 @@
"servers/dependency-injection",
"servers/lifespan",
"servers/storage-backends",
"servers/sessions",
"servers/extensions",
"servers/tasks",
"servers/versioning"
]
@ -264,6 +268,7 @@
"icon": "key",
"pages": [
"clients/auth/oauth",
"clients/auth/client-credentials",
"clients/auth/cimd",
"clients/auth/bearer"
],
@ -354,10 +359,12 @@
"group": "Upgrading",
"icon": "up",
"pages": [
"getting-started/upgrading/from-fastmcp-2",
"getting-started/upgrading/from-fastmcp-3",
"getting-started/upgrading/from-mcp-sdk",
"getting-started/upgrading/from-low-level-sdk"
"getting-started/upgrading/from-fastmcp-2",
"getting-started/upgrading/from-mcp-sdk-v1",
"getting-started/upgrading/from-mcp-sdk-v2",
"getting-started/upgrading/from-low-level-sdk-v1",
"getting-started/upgrading/from-low-level-sdk-v2"
]
},
{
@ -368,18 +375,7 @@
"development/contributing",
"development/tests",
"development/releases",
"patterns/contrib",
{
"collapsed": true,
"group": "v4 Notes",
"pages": [
"development/v4-notes/index",
"development/v4-notes/change-register",
"development/v4-notes/feature-program",
"development/v4-notes/protocol-2026",
"development/v4-notes/known-gaps"
]
}
"patterns/contrib"
]
},
{
@ -408,7 +404,10 @@
"icon": "code"
}
],
"version": "v3"
"version": "v4.0.0 (beta 1)"
},
{
"$ref": "./v3-navigation.json"
},
{
"$ref": "./v2-navigation.json"
@ -416,6 +415,30 @@
]
},
"redirects": [
{
"destination": "/getting-started/whats-new",
"source": "/development/v4-notes/index"
},
{
"destination": "/getting-started/upgrading/from-fastmcp-3",
"source": "/development/v4-notes/change-register"
},
{
"destination": "/getting-started/whats-new",
"source": "/development/v4-notes/feature-program"
},
{
"destination": "/getting-started/whats-new",
"source": "/development/v4-notes/protocol-2026"
},
{
"destination": "/getting-started/upgrading/from-fastmcp-3",
"source": "/development/v4-notes/known-gaps"
},
{
"destination": "/servers/tasks",
"source": "/development/v4-notes/background-tasks"
},
{
"destination": "/apps/fastmcp-app",
"source": "/apps/interactive-apps"
@ -489,13 +512,21 @@
"source": "/development/upgrade-guide"
},
{
"destination": "/getting-started/upgrading/from-mcp-sdk",
"destination": "/getting-started/upgrading/from-mcp-sdk-v1",
"source": "/getting-started/upgrading-from-sdk"
},
{
"destination": "/getting-started/upgrading/from-low-level-sdk",
"destination": "/getting-started/upgrading/from-mcp-sdk-v1",
"source": "/getting-started/upgrading/from-mcp-sdk"
},
{
"destination": "/getting-started/upgrading/from-low-level-sdk-v1",
"source": "/getting-started/low-level-sdk"
},
{
"destination": "/getting-started/upgrading/from-low-level-sdk-v1",
"source": "/getting-started/upgrading/from-low-level-sdk"
},
{
"destination": "/getting-started/upgrading/from-fastmcp-3",
"source": "/getting-started/upgrading/to-mcp-sdk-v2"

View file

@ -7,15 +7,19 @@ icon: arrow-down-to-line
We recommend using [uv](https://docs.astral.sh/uv/getting-started/installation/) to install and manage FastMCP.
```bash
uv add fastmcp
```
Or with pip:
```bash
pip install fastmcp
```
Or with uv:
```bash
uv add fastmcp
```
<Note>
**FastMCP 4 is in prerelease.** The commands above install the latest stable release, which is still 3.x. To get v4, pin the beta explicitly with `pip install "fastmcp==4.0.0b1"`, or see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease) for the uv constraint you'll need.
</Note>
### Optional Dependencies
@ -40,8 +44,8 @@ You should see output like the following:
```bash
$ fastmcp version
FastMCP version: 3.0.0
MCP version: 1.25.0
FastMCP version: 4.0.0b1
MCP version: 2.0.0
Python version: 3.12.2
Platform: macOS-15.3.1-arm64-arm-64bit
FastMCP root path: ~/Developer/fastmcp
@ -62,19 +66,27 @@ Alternatively, wait for the stable v5 release. See [this issue](https://github.c
</Info>
## Upgrading
### From FastMCP 3.0
Most FastMCP 3 servers run on 4 without changes. See [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) for the breaks that do exist, and [What's New](/getting-started/whats-new) for what the new version adds.
### From FastMCP 2.0
See the [Upgrade Guide](/getting-started/upgrading/from-fastmcp-2) for a complete list of breaking changes and migration steps.
### From the MCP SDK
#### From FastMCP 1.0
Which guide you want depends on which `mcp` version you're on and which of its two server APIs you used.
If you're using FastMCP 1.0 via the `mcp` package (meaning you import FastMCP as `from mcp.server.fastmcp import FastMCP`), upgrading is straightforward — for most servers, it's a single import change. See the [full upgrade guide](/getting-started/upgrading/from-mcp-sdk) for details.
#### From the high-level server
#### From the Low-Level Server API
If you're using FastMCP 1.0 via SDK v1 (meaning you import FastMCP as `from mcp.server.fastmcp import FastMCP`), upgrading is straightforward — for most servers it's a single import change. See [Upgrading from MCP SDK v1](/getting-started/upgrading/from-mcp-sdk-v1), which also explains why that route is usually easier than moving to MCP SDK v2.
If you built your server directly on the `mcp` package's `Server` class — with `list_tools()`/`call_tool()` handlers and hand-written JSON Schema — see the [migration guide](/getting-started/upgrading/from-low-level-sdk) for a full walkthrough.
If you already moved to SDK v2 and write against `MCPServer`, see [Upgrading from MCP SDK v2](/getting-started/upgrading/from-mcp-sdk-v2) — that migration is mostly renaming.
#### From the low-level server
If you built your server directly on the `mcp` package's `Server` class, the guide you want depends on how its handlers are registered. Decorators like `@server.list_tools()` mean SDK v1 — see [Upgrading from the Low-Level SDK v1](/getting-started/upgrading/from-low-level-sdk-v1). Handlers passed to the constructor as `on_list_tools=` mean SDK v2 — see [Upgrading from the Low-Level SDK v2](/getting-started/upgrading/from-low-level-sdk-v2).
## Troubleshooting
@ -103,16 +115,12 @@ FastMCP follows semantic versioning with pragmatic adaptations for the rapidly e
For production use, always pin to exact versions:
```
fastmcp==3.0.0 # Good
fastmcp>=3.0.0 # Bad - may install breaking changes
fastmcp==4.0.0b1 # Good - an exact version
fastmcp>=4.0.0 # Bad - may install breaking changes
```
See the full [versioning and release policy](/development/releases#versioning-policy) for details on our public API, deprecation practices, and breaking change philosophy.
## Contributing to FastMCP
Interested in contributing to FastMCP? See the [Contributing Guide](/development/contributing) for details on:
- Setting up your development environment
- Running tests and pre-commit hooks
- Submitting issues and pull requests
- Code standards and review process
The [Contributing Guide](/development/contributing) covers setting up a development environment, running the test suite and pre-commit hooks, and the standards we hold contributed code to.

View file

@ -3,7 +3,7 @@ title: Quickstart
icon: rocket-launch
---
Welcome! This guide will help you quickly set up FastMCP, run your first MCP server, give it a visual UI, and deploy it to Prefect Horizon.
This guide builds a working MCP server from scratch: a tool, a way to run it, a client that calls it, and a visual UI for the result. It ends with the server deployed and reachable over the internet.
If you haven't already installed FastMCP, follow the [installation instructions](/getting-started/installation).
@ -112,10 +112,7 @@ async def call_tool(name: str):
asyncio.run(call_tool("Ford"))
```
Note that:
- FastMCP clients are asynchronous, so we need to use `asyncio.run` to run the client
- We must enter a client context (`async with client:`) before using the client
- You can make multiple client calls within the same context
FastMCP clients are asynchronous, so the call goes through `asyncio.run`. Entering the client context with `async with client:` is what opens the connection, and it stays open for as many calls as you want to make inside the block.
## Give Your Tool a UI
@ -145,9 +142,11 @@ def greet(name: str) -> PrefabApp:
You can preview app tools locally with `fastmcp dev apps my_server.py` — no MCP host required. See the [Apps overview](/apps/overview) for the full guide, including state management, forms, charts, and server-connected interactivity.
## Deploy to Prefect Horizon
## Deploy Your Server
[Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides managed hosting, authentication, access control, and observability for MCP servers.
FastMCP HTTP servers run anywhere you can host a Python application. The [HTTP deployment guide](/deployment/http) covers the transport settings and security boundaries for self-managed infrastructure.
For a managed deployment, [Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides hosting, authentication, access control, and observability for MCP servers.
<Info>
Horizon is **free for personal projects** and offers enterprise governance for teams.

View file

@ -1,11 +1,15 @@
---
title: Upgrading from FastMCP 2
sidebarTitle: "From FastMCP 2"
description: Migration instructions for upgrading between FastMCP versions
description: What changed in FastMCP 3 for servers written against FastMCP 2
icon: up
---
This guide covers breaking changes and migration steps when upgrading FastMCP.
This guide covers the breaking changes a FastMCP 2 server meets on its way to FastMCP 3, newest release first.
<Note>
**Going all the way to FastMCP 4?** You need this page and [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3), in that order. The two describe different transitions: this one covers the v3 API changes, while the FastMCP 3 guide covers the MCP Python SDK v2 rebuild underneath v4. Where a v3 deprecation was later removed outright, this page marks it **Removed in v4**.
</Note>
## v3.0.0
@ -21,7 +25,7 @@ pip install --upgrade fastmcp
uv add --upgrade fastmcp
```
If you pin versions in a requirements file or `pyproject.toml`, update your pin to `fastmcp>=3.0.0,<4`.
If you pin versions in a requirements file or `pyproject.toml`, update your pin to `fastmcp>=3.0.0,<4`. Going on to FastMCP 4 is a second hop: finish this page, then work through [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) and move the pin to `fastmcp>=4.0.0` at the end of it.
<Info>
**New repository home.** As part of the v3 release, FastMCP's GitHub repository has moved from `jlowin/fastmcp` to [`PrefectHQ/fastmcp`](https://github.com/PrefectHQ/fastmcp) under [Prefect](https://prefect.io)'s stewardship. GitHub automatically redirects existing clones and bookmarks, so nothing breaks — but you can update your local remote whenever convenient:
@ -81,7 +85,7 @@ BREAKING CHANGES (will crash at import or runtime):
12. REPO MOVE: GitHub repository moved from jlowin/fastmcp to PrefectHQ/fastmcp. Update git remotes and dependency URLs that reference the old location.
13. BACKGROUND TASKS: FastMCP's background task system (SEP-1686) is now an optional dependency. If the code uses task=True or TaskConfig, add pip install "fastmcp[tasks]".
13. BACKGROUND TASKS: FastMCP's background task system is now an optional dependency. If the code uses task=True or TaskConfig, add pip install "fastmcp[tasks]".
DEPRECATIONS (still work but emit warnings):
@ -101,7 +105,7 @@ For each issue found, show the original line, explain why it breaks, and provide
In v2, you could configure transport settings directly in the `FastMCP()` constructor. In v3, `FastMCP()` is purely about your server's identity and behavior — transport configuration happens when you actually start serving. Passing any of the old kwargs now raises `TypeError` with a migration hint.
```python
```python test="skip"
# Before
mcp = FastMCP("server", host="0.0.0.0", port=8080)
mcp.run()
@ -140,7 +144,7 @@ Keeping `DiskStore` requires `pip install 'py-key-value-aio[disk]'`, which re-in
In v2, you could enable or disable individual components by calling methods on the component object itself. In v3, visibility is controlled through the server (or provider), which lets you target components by name, tag, or type without needing a reference to the object:
```python
```python test="skip"
# Before
tool = await server.get_tool("my_tool")
tool.disable()
@ -155,7 +159,7 @@ Calling `.enable()` or `.disable()` on a component object now raises `NotImpleme
The `get_tools()`, `get_resources()`, `get_prompts()`, and `get_resource_templates()` methods have been renamed to `list_tools()`, `list_resources()`, `list_prompts()`, and `list_resource_templates()`. More importantly, they now return lists instead of dicts — so code that indexes by name needs to change:
```python
```python test="skip"
# Before
tools = await server.get_tools()
tool = tools["my_tool"]
@ -169,9 +173,9 @@ tool = next((t for t in tools if t.name == "my_tool"), None)
Prompt functions now use FastMCP's `Message` class instead of `mcp.types.PromptMessage`. The new class is simpler — it accepts a plain string and defaults to `role="user"`, so most prompts become one-liners:
```python
```python test="skip"
# Before
from fastmcp.types import PromptMessage, TextContent
from mcp.types import PromptMessage, TextContent
@mcp.prompt
def my_prompt() -> PromptMessage:
@ -187,7 +191,7 @@ def my_prompt() -> Message:
If your prompt functions return raw dicts with `role` and `content` keys, those also need to change. v2 silently coerced dicts into prompt messages, but v3 requires typed `Message` objects (or plain strings for single user messages):
```python
```python test="skip"
# Before (v2 accepted this)
@mcp.prompt
def my_prompt():
@ -211,7 +215,7 @@ def my_prompt() -> list[Message]:
`ctx.set_state()` and `ctx.get_state()` are now async because state in v3 is session-scoped and backed by a pluggable storage backend (rather than a simple dict). This means state persists across multiple tool calls within the same session:
```python
```python test="skip"
# Before
ctx.set_state("key", "value")
value = ctx.get_state("key")
@ -223,7 +227,7 @@ value = await ctx.get_state("key")
State values must also be JSON-serializable by default (dicts, lists, strings, numbers, etc.). If you need to store non-serializable values like an HTTP client, pass `serializable=False` — these values are request-scoped and only available during the current tool call:
```python
```python test="skip"
await ctx.set_state("client", my_http_client, serializable=False)
```
@ -245,7 +249,7 @@ parent.mount(child, namespace="child")
In v2, auth providers like `GitHubProvider` could auto-load configuration from environment variables with a `FASTMCP_SERVER_AUTH_*` prefix. This magic has been removed — pass values explicitly:
```python
```python test="skip"
# Before (v2) — client_id and client_secret loaded automatically
# from FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID, etc.
auth = GitHubProvider()
@ -278,7 +282,7 @@ transport = StreamableHttpTransport("http://localhost:8000/mcp")
`OpenAPIProvider` no longer accepts a `timeout` parameter. Configure timeout on the httpx2 client directly. The `client` parameter is also now optional — when omitted, a default client is created from the spec's `servers` URL with a 30-second timeout:
```python
```python test="skip"
# Before
provider = OpenAPIProvider(spec, client, timeout=60)
@ -291,7 +295,7 @@ provider = OpenAPIProvider(spec, client)
The FastMCP metadata key in component `meta` dicts changed from `_fastmcp` to `fastmcp`. If you read metadata from tool or resource objects, update the key:
```python
```python test="skip"
# Before
tags = tool.meta.get("_fastmcp", {}).get("tags", [])
@ -309,7 +313,7 @@ Metadata is now always included — the `include_fastmcp_meta` parameter has bee
In v2, `@mcp.tool` transformed your function into a `FunctionTool` object. In v3, decorators return your original function unchanged — which means decorated functions stay callable for testing, reuse, and composition:
```python
```python test="skip"
@mcp.tool
def greet(name: str) -> str:
return f"Hello, {name}!"
@ -321,7 +325,7 @@ If you have code that treats the decorated result as a `FunctionTool` (e.g., acc
**Background tasks require optional dependency**
FastMCP's background task system (SEP-1686) is now behind an optional extra. If your server uses background tasks, install with:
FastMCP's background task system is now behind an optional extra. If your server uses background tasks, install with:
```bash
pip install "fastmcp[tasks]"
@ -335,7 +339,7 @@ These were deprecated in v3. Items marked **Removed in v4** no longer work at al
**mount() prefix → namespace** (Removed in v4)
```python
```python test="skip"
# Removed in v4
main.mount(subserver, prefix="api")
@ -345,7 +349,7 @@ main.mount(subserver, namespace="api")
**import_server() → mount()** (Removed in v4)
```python
```python test="skip"
# Removed in v4
main.import_server(subserver)
@ -382,7 +386,7 @@ server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)])
**add_tool_transformation() → add_transform()** (Removed in v4)
```python
```python test="skip"
# Removed in v4
mcp.add_tool_transformation("name", config)
@ -395,7 +399,7 @@ mcp.add_transform(ToolTransform({"name": config}))
The proxy target is passed positionally in both APIs, so most calls migrate unchanged. If you passed the target by keyword, note that the parameter was renamed from `backend=` to `target=`.
```python
```python test="skip"
# Removed in v4
proxy = FastMCP.as_proxy("http://example.com/mcp")
proxy = FastMCP.as_proxy(backend="http://example.com/mcp") # keyword form
@ -424,12 +428,18 @@ server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)])
### Removed Deprecated Features
- `BearerAuthProvider` → use `JWTVerifier`
- `Context.get_http_request()` → use `get_http_request()` from dependencies
- `from fastmcp import Image` → use `from fastmcp.utilities.types import Image`
- `FastMCP(dependencies=[...])` → use `fastmcp.json` configuration
- `FastMCPProxy(client=...)` → use `client_factory=lambda: ...`
- `output_schema=False` → use `output_schema=None`
A batch of long-deprecated surfaces came out in 2.14. Each fails loudly at import or call time, and each has a direct replacement:
| Removed | Replacement |
|---|---|
| `BearerAuthProvider` | `JWTVerifier` — the same JWT validation under a name that says what it does |
| `Context.get_http_request()` | `get_http_request()` from [dependency injection](/servers/dependency-injection) |
| `from fastmcp import Image` | `from fastmcp.utilities.types import Image` |
| `FastMCP(dependencies=[...])` | a [`fastmcp.json`](/deployment/server-configuration) configuration file |
| `FastMCPProxy(client=...)` | `client_factory=lambda: ...` |
| `output_schema=False` | `output_schema=None` |
Two of these are worth understanding rather than just swapping. `FastMCPProxy` takes a factory instead of a client because a single shared client cannot serve concurrent proxied sessions safely — the factory gives each session its own backend connection. And `output_schema=False` became `output_schema=None` because `False` read as "this tool has a schema, and it is false"; `None` says plainly that there is no schema.
## v2.13.0
@ -437,7 +447,7 @@ server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)])
The OAuth proxy now issues its own JWT tokens. For production, provide explicit keys:
```python
```python test="skip"
auth = GitHubProvider(
client_id=os.environ["GITHUB_CLIENT_ID"],
client_secret=os.environ["GITHUB_CLIENT_SECRET"],

View file

@ -1,37 +1,124 @@
---
title: Upgrading from FastMCP 3
sidebarTitle: "From FastMCP 3.x"
sidebarTitle: "From FastMCP 3"
description: What changes when you upgrade to FastMCP 4, which builds on the MCP Python SDK v2
icon: up
---
FastMCP 4 builds on the MCP Python SDK v2, and that is the source of every change in this guide. The SDK v2 makes two sweeping changes to the protocol layer: it splits the protocol types out of `mcp.types` into a standalone `mcp_types` package, and it renames every protocol field from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, `isError` → `is_error`, and so on).
FastMCP 4 builds on the MCP Python SDK v2, and that is the source of every change in this guide. The SDK v2 makes two sweeping changes to the protocol layer: it moves the protocol types into a standalone `mcp_types` package (still importable as `mcp.types`), and it renames every model field from camelCase to snake_case in Python (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, `isError` → `is_error`, and so on). The wire format does not change: the models keep their camelCase aliases and serialize under them, so this renames the attributes your code reads, not the JSON on the connection.
FastMCP 4 absorbs almost all of this for you. Field access is bridged so your existing reads keep working, and the imports you were taught have a stable home in FastMCP itself. The sections below describe what FastMCP handles for you, the small number of changes you must make in your own code, and the deprecation timeline for the compatibility shims.
FastMCP 4 absorbs almost all of this for you. Field access is bridged so your existing reads keep working, and the imports you were taught have a stable home in FastMCP itself. What the SDK cannot hide is the protocol's own direction: the new sessionless era removes the server's ability to call back into a client mid-request, and background tasks moved out of the core spec into an extension. Those two shape the changes a working server is most likely to feel.
## Environment requirements
The sections below cover what FastMCP handles for you, the changes you must make in your own code, the surfaces removed outright in 4.0, the behavior shifts that compile fine but act differently, and the deprecation timeline for the compatibility shims.
## Install the v4 Prerelease
While FastMCP 4 is in prerelease, pin the beta explicitly. The `fastmcp` package is a thin wrapper that depends on `fastmcp-slim` at the same version, so asking for a prerelease of one means asking for a prerelease of the other. pip infers that on its own:
```bash
pip install "fastmcp==4.0.0b1"
```
uv is stricter: it allows prereleases only for packages you name, and `fastmcp-slim` arrives transitively. Constrain it alongside the requirement in `pyproject.toml`:
```toml
[project]
dependencies = ["fastmcp==4.0.0b1"]
[tool.uv]
constraint-dependencies = ["fastmcp-slim==4.0.0b1"]
```
Then run `uv lock` or `uv sync` normally. Naming the one package keeps the rest of your graph on stable releases, where `--prerelease allow` would opt every dependency into prereleases. The MCP SDK needs no constraint at all now that it ships stable releases — pinning `mcp==2.0.0b2` here would in fact break the resolution, since a prerelease does not satisfy FastMCP's own `mcp>=2.0.0` requirement.
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
You are upgrading an MCP server or client from FastMCP 3.x to FastMCP 4, which is built on the MCP Python SDK v2.
FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-fastmcp-3 — it explains every item below, with the replacement code. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs.
Then search the provided code for each signal below. Most FastMCP 3 servers upgrade untouched, so report only what you actually find.
ENVIRONMENT
- a pydantic pin below 2.12
- a FastAPI pin below 0.133.0, the first release admitting Starlette 1.x (earlier ones cap it, e.g. 0.115.12 requires `starlette<0.47.0`), or any direct Starlette pin below 1.0.1
IMPORTS THAT NO LONGER RESOLVE
- `fastmcp.server.proxy`, `fastmcp.server.openapi`, `FastMCPOpenAPI`
- `fastmcp.experimental.server.openapi`, `fastmcp.experimental.utilities.openapi`
- `fastmcp.experimental.sampling.handlers`
- `fastmcp.server.apps`, `fastmcp.server.app`
- `fastmcp.tools.tool`, `fastmcp.resources.resource`, `fastmcp.prompts.prompt`
- `fastmcp.server.tasks`, `fastmcp.server.sampling`
- `fastmcp.server.auth.authorization`
- `CurrentDocket` or `CurrentWorker` from `fastmcp.dependencies`
- `SkillsProvider`
- `CachableToolResult`, `CachablePromptResult`, and their siblings (the misspelling was corrected with no alias)
- `PromptToolMiddleware`, `ResourceToolMiddleware`
REMOVED SERVER METHODS AND KEYWORDS
- `FastMCP.as_proxy(...)`
- `import_server(...)` ← flag this one loudly: `mount()` is the replacement but NOT an equivalent. `import_server` took a static snapshot and skipped the child's lifespan and middleware; `mount` is a live composition that runs both.
- `mount(prefix=...)`, `mount(as_proxy=...)`
- `add_tool_transformation(...)`, `remove_tool_transformation(...)`
- `remove_tool(...)` ← its replacement raises KeyError where this raised NotFoundError, so check surrounding except clauses
- tool `serializer=`, tool `exclude_args=`
- `StreamableHttpTransport(sse_read_timeout=...)`
- `FASTMCP_DECORATOR_MODE` / `settings.decorator_mode`
- `FastMCP(sampling_handler=...)`, `sampling_handler_behavior=`
REMOVED CONTEXT METHODS
- `ctx.sample(...)`, `ctx.sample_step(...)`, `ctx.list_roots(...)`
- Note for the user: if borrowing the CALLER's model is the whole point of the server, the guide's recommendation is to stay on FastMCP 3.x rather than migrate.
- The client side is NOT affected — `Client(sampling_handler=...)` and `Client(roots=...)` still mean what they meant.
RUNTIME BREAKS THAT STILL COMPILE — the ones most likely to reach production
- `ctx.elicit(...)` anywhere. It is era-gated in 4.0 and raises on modern connections, which is what `Client` now negotiates by default. This is the single most likely runtime failure.
- `ctx.elicit(...)` called without `response_type`
- `except httpx.` around any FastMCP call. FastMCP raises httpx2 exceptions now, but httpx is usually still installed transitively, so the handler imports, type-checks, and silently never matches.
- a custom `httpx.AsyncClient`, `httpx_client_factory=`, or `httpx.Auth` handed to a FastMCP transport, `OAuth`, or `from_openapi`
- `Middleware.on_initialize` hooks, and `ctx.set_state` values read back in a later call — neither survives a modern connection
- middleware assuming `on_message` only sees routable requests
- camelCase field reads (`inputSchema`, `isError`, `mimeType`, `nextCursor`, `structuredContent`, `serverInfo`, and the rest) — these still work but warn, and are scheduled for removal
- clients matching on the resource-not-found error code -32002
- templated resources whose parameters legitimately carry `..` or absolute paths
- an OAuth server (`OAuthProxy` or anything built on it) with `issuer_url` set to something other than `base_url` — this forces a one-time re-authorization of every client
BACKGROUND TASKS
- `@mcp.tool(task=True)` or `TaskConfig` without `mcp.add_extension(TasksExtension())`
- `task=` on a `@mcp.resource` or `@mcp.prompt` decorator (tools only now)
- `client.call_tool(..., task=True)`, `read_resource(task=True)`, `get_prompt(task=True)`
ERRORS
- `McpError(ErrorData(...))` positional construction. Catching and `err.error.code` are unchanged; only construction moved.
For each item found, show the original line, name what changed, and give the corrected code from the guide. Where you could not confirm a replacement in the docs, say so instead of guessing.
</Prompt>
## Environment Requirements
The SDK v2 raises FastMCP's dependency floors, which matters before any of your code runs.
**pydantic >= 2.12 is now the floor.** If your project pins an older pydantic (for example `pydantic==2.11.*`), installing this FastMCP release fails with an unsatisfiable-resolution error from your installer — bump your pin to `>=2.12` first. If you don't pin pydantic at all, installers upgrade it silently as part of the FastMCP upgrade.
**The server extra floors Starlette >= 1.0.** Modern FastAPI (0.11x and later) already runs on Starlette 1.x, so mounting a FastMCP server inside a FastAPI app coexists cleanly — verified with FastAPI 0.138.2. Only very old FastAPI versions pinned below Starlette 1.0 conflict; upgrade FastAPI if your resolver complains about Starlette.
**The server extra floors Starlette >= 1.0.1.** This is the requirement most likely to force an unrelated upgrade, because FastAPI pinned Starlette to a sub-1.0 range for a long time — FastAPI 0.115.12, for example, requires `starlette<0.47.0`. **FastAPI 0.133.0 is the first release that admits Starlette 1.x**, so a project pinned below that gets an unsatisfiable resolution rather than a version bump. Raise your FastAPI pin to `>=0.133.0` before upgrading FastMCP. Mounting a FastMCP server inside a FastAPI app is otherwise unaffected — verified against FastAPI 0.135.2 on Starlette 1.3.1.
## What FastMCP absorbs
## What FastMCP Absorbs
### Legacy camelCase field access keeps working
### camelCase Field Access
Objects that FastMCP hands back to you — the results of `client.list_tools()`, `client.call_tool_mcp()`, `client.read_resource()`, and the parameter objects passed to your sampling and elicitation handlers — are SDK v2 objects with snake_case fields. FastMCP installs a compatibility bridge at import time that routes the old camelCase names to their new snake_case fields, so code written against FastMCP 2.x still reads correctly:
```python
from fastmcp import Client
async with Client("my_mcp_server.py") as client:
tools = await client.list_tools()
schema = tools[0].inputSchema # still works, warns once
async def read_schema():
async with Client("my_mcp_server.py") as client:
tools = await client.list_tools()
return tools[0].inputSchema # still works, warns once
```
Each bridged read emits a `FastMCPDeprecationWarning` pointing you at the snake_case name (`tools[0].input_schema` here). The bridge covers the fields users actually read: `inputSchema`/`outputSchema` on tools, `mimeType` on resources and content, `isError`/`structuredContent` on tool results, `nextCursor` on paginated results, `serverInfo`/`protocolVersion` on the initialize result, the sampling parameter fields (`systemPrompt`, `maxTokens`, `stopSequences`, `modelPreferences`, `toolChoice`), and `requestedSchema` on elicitation parameters.
Each bridged read emits a `FastMCPDeprecationWarning` pointing you at the snake_case name (`tools[0].input_schema` here). The bridge covers the fields users actually read: `inputSchema`/`outputSchema` on tools; `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint` on tool annotations; `mimeType` on resources and content; `isError`/`structuredContent` on tool results; `nextCursor` on paginated results; `serverInfo`/`protocolVersion` on the initialize result; the sampling parameter fields (`systemPrompt`, `maxTokens`, `stopSequences`, `modelPreferences`, `toolChoice`); and `requestedSchema` on elicitation parameters.
The bridge is controlled by the `mcp_camelcase_compat` setting, which defaults to on. Set it to `False` (or the environment variable `FASTMCP_MCP_CAMELCASE_COMPAT=false`) to turn the shims off, in which case only the snake_case names resolve:
@ -43,23 +130,19 @@ fastmcp.settings.mcp_camelcase_compat = False
See [Settings](/more/settings) for the full reference.
### Imports have a stable home
### Protocol Types
The `mcp.types` module no longer exists. FastMCP re-exports the protocol types you're most likely to use — `TextContent`, `ImageContent`, `Tool`, `ErrorData`, `Icon`, `PromptMessage`, `SamplingMessage`, `ToolAnnotations`, and around two dozen others — from `fastmcp.types`. Update your imports to point there:
Every protocol type — `TextContent`, `ImageContent`, `Tool`, `ErrorData`, `Icon`, `PromptMessage`, `SamplingMessage`, `ToolAnnotations`, notification and request wrapper types like `ToolListChangedNotification`, and everything else — now lives in a standalone `mcp_types` package. The SDK re-exports that package as `mcp.types`, so existing imports keep working and stay the preferred spelling:
```python
from fastmcp.types import TextContent, Tool, ToolAnnotations
from mcp.types import TextContent, Tool, ToolAnnotations
```
For protocol types FastMCP does not re-export (notification and request wrapper types like `ToolListChangedNotification` or `ServerNotification`), import them from `mcp_types` directly:
Both names resolve to the same objects, so `from mcp_types import X` is equally valid — useful if you depend on the types without the rest of the SDK. What did change is the fields on those types: they are snake_case now (`input_schema`, not `inputSchema`), which the [compatibility bridge](#legacy-camelcase-field-access-keeps-working) covers for the objects FastMCP hands you.
```python
import mcp_types
`fastmcp.types` still exists, but holds only types FastMCP defines itself (currently just `Textarea`, used to render a multiline textarea in form-based UIs) — it does not re-export protocol types.
notification = mcp_types.ToolListChangedNotification()
```
### `McpError` has an alias
### The `McpError` Alias
`fastmcp.exceptions.McpError` is an alias of the SDK's `MCPError`. Catching errors is unchanged — `except McpError` still catches SDK-raised errors, and reading `err.error.code` still works:
@ -72,7 +155,7 @@ except McpError as err:
print(err.error.code)
```
### Behavior preserved across the SDK boundary
### Preserved Behavior
A few client behaviors that touch the SDK are preserved so you don't have to change anything:
@ -80,17 +163,9 @@ A few client behaviors that touch the SDK are preserved so you don't have to cha
- `client.ping()` returns a `bool`.
- `client.transport.get_session_id()` returns `None` on protocol eras that have no session, rather than raising. (The SDK v2 removed session-id access from its streamable HTTP transport; FastMCP reconstructs it on the transport object.)
## What you must change
## What You Must Change
Three things are on you.
**Your own `mcp.types` imports.** FastMCP can re-export types, but it can't rewrite imports in your code. Any `from mcp.types import X` or `import mcp.types` in your server or client fails at import time with:
```
ModuleNotFoundError: No module named 'mcp.types'
```
The raw message gives no hint toward the fix, so if you see it after upgrading, this is why. Switch to `from fastmcp.types import X` for the common types, or `import mcp_types` for the rest.
Everything above, FastMCP handled for you. What remains lives in your own code, where FastMCP can't reach it — how you construct errors, the custom HTTP clients you hand to a transport, and any place you reach past FastMCP's surfaces into the raw SDK objects. Each surfaces as a clear failure at import or call time, and each is a mechanical fix.
**`McpError` construction.** The v1 pattern of wrapping an `ErrorData` and passing it positionally fails under SDK v2 with:
@ -100,7 +175,7 @@ TypeError: MCPError.__init__() missing 1 required positional argument: 'message'
Note the message prints the class as `MCPError` (uppercase) even though your code wrote `McpError` — the old name is an alias for the SDK's renamed class. Construct the error with keyword arguments instead:
```python
```python test="skip"
from fastmcp.exceptions import McpError
# Before (raises TypeError under SDK v2):
@ -116,7 +191,7 @@ Catching and `err.error.code` are unchanged — only construction moved.
**FastMCP now uses httpx2 exclusively.** FastMCP has replaced `httpx` with [httpx2](https://pypi.org/project/httpx2/), a next-generation httpx fork, across its entire HTTP stack — client transports and every server-side path (auth providers, the OpenAPI integration, the version check). `httpx` is no longer a FastMCP dependency. If you pass a custom client or factory into a FastMCP client transport — `StreamableHttpTransport(httpx_client_factory=...)`, `SSETransport(httpx_client_factory=...)`, `OAuth(httpx_client_factory=...)`, or a custom `httpx.Auth` as `Client(auth=...)` — those objects must now be httpx2. httpx2 is a drop-in fork with the same public API, so the change is an import swap:
```python
```python test="skip"
# Before
import httpx
@ -134,52 +209,247 @@ transport = StreamableHttpTransport(
)
```
The `client` you pass to `FastMCP.from_openapi(client=...)` (and `OpenAPIProvider(client=...)`) is now type-hinted `httpx2.AsyncClient`. FastMCP does not gate on the type, so an existing `httpx.AsyncClient` keeps working at runtime via duck-typing this release — but switching it to `httpx2.AsyncClient` clears the type hint and is the supported path going forward. HTTP made inside your own tools is entirely yours and is unaffected either way.
The `client` you pass to `FastMCP.from_openapi(client=...)` (and `OpenAPIProvider(client=...)`) should now be an `httpx2.AsyncClient`. Existing `httpx.AsyncClient` instances remain temporarily accepted via duck typing, but emit a `FastMCPDeprecationWarning` and will be rejected in a future release. HTTP made inside your own tools is entirely yours and is unaffected.
**The subtlest break is exception handlers, and no type checker will catch it.** `httpx` very likely remains installed in your environment (the Anthropic, OpenAI, and Google SDKs all depend on it), so code that catches old-httpx exceptions around FastMCP calls still imports and still type-checks — it just never matches, because FastMCP now raises `httpx2` exceptions. The handler silently becomes dead code:
```python
import httpx # still installed transitively — this import works
try:
result = await client.call_tool("fetch", {"url": url})
except httpx.ConnectError: # dead code: FastMCP now raises httpx2.ConnectError
return fallback()
async def fetch(client, url):
try:
return await client.call_tool("fetch", {"url": url})
except httpx.ConnectError: # dead code: FastMCP now raises httpx2.ConnectError
return fallback()
```
Grep your codebase for `except httpx.` and move those handlers to `httpx2`. The exception hierarchies match name-for-name, so the fix is an import swap — the hard part is remembering to look. One place you are covered automatically: exceptions raised *inside your tools and resources* (for example, a tool whose own old-httpx call gets a 429) are still mapped to `ToolError`/`ResourceError` by FastMCP's error boundary, which recognizes both libraries' exceptions during the transition.
Two runtime behaviors shift with httpx2, and because the switch is now wholesale they apply to **all** FastMCP HTTP — including server-auth upstream calls, not just the client path. TLS verification uses the operating system's trust store (via `truststore`, honoring `SSL_CERT_FILE`/`SSL_CERT_DIR`) instead of the bundled certifi CA set, so corporate-CA or certifi-pinned setups may verify differently. And the FastMCP HTTP loggers are renamed from `httpx`/`httpcore.*` to `httpx2`/`httpcore2.*` — update any logging filters that select the HTTP stack by logger name.
## Deprecation timeline
## Removed in FastMCP 4
Deprecations that warned throughout the 3.x line are removed in 4.0. Unlike the bridged changes above, these fail immediately at the call site — a `ModuleNotFoundError`, `ImportError`, `AttributeError`, or `TypeError` — so nothing degrades silently. Every one has a direct replacement, and the fix is mechanical.
### Moved Imports
The proxy, OpenAPI, and app integrations moved to their permanent homes, and the internal component classes are no longer re-exported from their old aliases:
| Removed import | Replacement |
| --- | --- |
| `fastmcp.server.proxy` | `fastmcp.server.providers.proxy` |
| `fastmcp.server.openapi` (and `FastMCPOpenAPI`) | `FastMCP` with an `OpenAPIProvider` from `fastmcp.server.providers.openapi` |
| `fastmcp.experimental.server.openapi` | `fastmcp.server.providers.openapi` |
| `fastmcp.experimental.utilities.openapi` | `fastmcp.utilities.openapi` |
| `fastmcp.server.apps`, `fastmcp.server.app` | `fastmcp.apps` (e.g. `AppConfig`) or `fastmcp` (`FastMCPApp`) |
| `Tool` / `ToolResult` from `fastmcp.tools.tool` | `fastmcp.tools` |
| `Resource` from `fastmcp.resources.resource` | `fastmcp.resources` |
| `Prompt` / `Message` from `fastmcp.prompts.prompt` | `fastmcp.prompts` |
| `FunctionTool` / `ParsedFunction` / `tool` from `fastmcp.tools.tool` | `fastmcp.tools.function_tool` |
| `FunctionResource` / `resource` from `fastmcp.resources.resource` | `fastmcp.resources.function_resource` |
| `FunctionPrompt` / `prompt` from `fastmcp.prompts.prompt` | `fastmcp.prompts.function_prompt` |
| `OpenAISamplingHandler` from `fastmcp.experimental.sampling.handlers` | `fastmcp.client.sampling.handlers.openai` |
| `AuthCheck` / `AuthContext` / `require_scopes` / `require_roles` / `restrict_tag` / `run_auth_checks` from `fastmcp.server.auth.authorization` | `fastmcp.server.auth` |
| `run_auth_checks_with_shortfall` / `scope_requirements` from `fastmcp.server.auth.authorization` | `fastmcp.utilities.authorization` |
| `SkillsProvider` | `SkillsDirectoryProvider` from `fastmcp.server.providers.skills` |
| `TaskConfig` from `fastmcp.server.tasks` | `fastmcp.utilities.tasks` |
| `CurrentDocket` / `CurrentWorker` from `fastmcp.dependencies` | `fastmcp_tasks.dependencies` |
| `fastmcp.server.sampling` (and `SamplingTool`) | removed with [server-side sampling](#protocol-version-support) |
Two renames in the same family are worth calling out because they have no compatibility alias. The response-caching wrapper models lost a spelling typo — `CachableToolResult`, `CachablePromptResult`, and their siblings became `CacheableToolResult`, `CacheablePromptResult`, etc. — so an import of the old spelling from `fastmcp.server.middleware.caching` raises `ImportError`. And `PromptToolMiddleware` / `ResourceToolMiddleware` are gone in favor of the `PromptsAsTools` / `ResourcesAsTools` transforms from `fastmcp.server.transforms` (the `ToolInjectionMiddleware` base class is retained).
### Removed Server Methods
These `FastMCP` methods and keywords have warned since 3.0 and are now removed:
| Removed | Replacement |
| --- | --- |
| `FastMCP.as_proxy(sub)` | `create_proxy(sub)` (from `fastmcp.server`) |
| `mcp.import_server(sub)` | `mcp.mount(sub)` |
| `mcp.mount(sub, prefix="x")` | `mcp.mount(sub, namespace="x")` |
| `mcp.mount(sub, as_proxy=True)` | wrap with `create_proxy(sub)`, then `mount` the proxy |
| `mcp.add_tool_transformation(name, cfg)` | `mcp.add_transform(ToolTransform({name: cfg}))` |
| `mcp.remove_tool_transformation(name)` | removed (was a no-op); hide tools with `mcp.disable(keys=[...])` |
| `mcp.remove_tool(name)` | `mcp.local_provider.remove_tool(name)` |
Two of these replacements are not exact behavioral swaps. `create_proxy` takes its target as the first positional argument (`target`), so a keyword call like `as_proxy(backend=server)` becomes `create_proxy(server)` rather than reusing the old keyword. And `local_provider.remove_tool` raises a plain `KeyError` when the tool is missing, where `FastMCP.remove_tool` raised a `NotFoundError` — update any `except NotFoundError` cleanup around a removal.
`mount(as_proxy=True)` used to route the child through a proxy (an MCP-client execution boundary) rather than composing it directly. To keep that boundary, wrap the child in `create_proxy()` and mount the proxy; a plain `mount(child)` composes the child in-process. Either way, the child's lifespan and middleware now run — a direct mount no longer skips them.
`import_server` → `mount` is the one row here that is not a mechanical swap, because the two never had the same semantics. `import_server` took a **one-time static snapshot** — it copied the child's tools, resources, and prompts at call time, with no live link, and did not run the child's lifespan or middleware. `mount` is a **live composition** — it holds a live link to the child and runs the child's lifespan and middleware. After switching, later changes to the child become visible through the parent, the child's lifespan runs with the parent's (entered when the server starts, held until it stops — not per request), and the child's middleware runs on the operations delegated to it. If you depended on the frozen-copy behavior (a stable snapshot, no child lifecycle), there is no drop-in replacement: register the child's components on the parent directly instead of composing the two servers.
### Removed Parameters
Several parameters and settings that warned in 3.x are gone:
- **Tool `serializer=`** is removed from `@tool` / `mcp.tool()`, `Tool.from_function`, `Tool.from_tool`, and the OpenAPI tool. Return a `ToolResult` from your tool for full control over serialization instead.
- **Tool `exclude_args=`** is removed. Hide a parameter from the tool schema by injecting it instead: give it a `Depends(factory)` default (from `fastmcp.dependencies`), where `factory` is a callable returning the value the argument used to carry. An injected parameter never appears in the tool's schema, which is what `exclude_args` was for.
- **The `decorator_mode` setting** (`FASTMCP_DECORATOR_MODE`) and its `"object"` mode are removed. Decorators always return your original function with metadata attached; reach the component object through the server (`await mcp.get_tool("name")`) rather than off the decorated function.
- **`StreamableHttpTransport(sse_read_timeout=...)`** is removed — it was a no-op under the SDK v2 client. Set the read timeout through the public `Client(transport, timeout=...)` (a `timedelta` or float seconds), or reach for a custom `httpx_client_factory` when you need finer control. (`SSETransport` still accepts `sse_read_timeout`.)
- **`ctx.elicit()` now requires `response_type`.** Omitting it (or passing `None`) has warned since 3.2 and now raises `TypeError`. The empty-object schema it produced gave clients nothing to render, and some showed an empty, non-functional form. Pass a type describing what you expect back — `bool` is the right answer for a confirmation:
```python test="skip"
# Before
result = await ctx.elicit("Approve this action?")
# After
result = await ctx.elicit("Approve this action?", response_type=bool)
```
This is the server-authoring API only. Client elicitation handlers still receive `response_type=None` for URL requests and for empty schemas sent by other servers — that contract is unchanged.
### Background Tasks
Background tasks left the core MCP spec during the SDK v2 rebuild and came back as the `io.modelcontextprotocol/tasks` extension (SEP-2663). FastMCP follows the protocol: what was a built-in server feature in 3.x is now a registered extension, and the authoring surface changed on both sides of the connection.
The extension ships in a separate package, so the pin from [Install the v4 Prerelease](#install-the-v4-prerelease) needs one more entry before any of this imports:
```toml
[project]
dependencies = ["fastmcp[tasks]==4.0.0b1"]
[tool.uv]
constraint-dependencies = [
"fastmcp-slim==4.0.0b1",
"fastmcp-tasks==4.0.0b1",
"mcp==2.0.0b2",
"mcp-types==2.0.0b2",
]
```
On the server, `task=True` still marks a tool as capable of running in the background, but it no longer runs anything by itself — the extension does. Register it, or the server refuses to start:
```python
from fastmcp import FastMCP
from fastmcp_tasks import TasksExtension
mcp = FastMCP("MyServer")
mcp.add_extension(TasksExtension())
@mcp.tool(task=True)
async def slow_computation(duration: int) -> str:
"""A long-running operation."""
return "done"
```
Without the registration, a `task=True` tool raises at startup rather than the first time a client calls the tool:
```
RuntimeError: Task-enabled tools (slow_computation) require the tasks extension,
but no extension with identifier 'io.modelcontextprotocol/tasks' is registered.
```
`TaskConfig` moved from `fastmcp.server.tasks` to `fastmcp.utilities.tasks`, and the `CurrentDocket` and `CurrentWorker` dependencies moved to `fastmcp_tasks.dependencies`.
`task=` is now a tool-only keyword. FastMCP 3 accepted it on resource, resource-template, and prompt decorators as well; passing it to `@mcp.resource` or `@mcp.prompt` now raises `TypeError`, and there is no replacement — the extension tasks tool calls only.
The client API changed shape entirely. In 3.x you opted a single call into background execution with `task=True` and got a handle back. In 4.0 `call_tool` handles a tasked call transparently: if the server runs the call in the background, the client polls it to completion and returns the same result a synchronous call would have produced.
```python
import fastmcp_tasks # noqa: F401 — importing anywhere enables client task support
from fastmcp import Client
async def run(server):
async with Client(server) as client:
return await client.call_tool("slow_computation", {"duration": 10})
```
When you want the handle — to do other work while the task runs, check on it, or cancel it — `call_tool_task` returns one immediately:
```python
from fastmcp import Client
from fastmcp_tasks import call_tool_task
async def run(server):
async with Client(server) as client:
task = await call_tool_task(client, "slow_computation", {"duration": 10})
return await task.result()
```
Three things follow from this. `client.call_tool(name, args, task=True)` raises `TypeError`, as do `read_resource(task=True)` and `get_prompt(task=True)` — and those last two have no replacement. Client task support requires `fastmcp_tasks` to be imported somewhere in the process, since that import is what makes a `Client` advertise the capability. And tasks are negotiated only on modern connections, so a `mode="legacy"` client never gets them. See [Background Tasks](/servers/tasks) for the full picture.
## Behavior Changes
These changes compile fine and can surface at runtime. The first is the one most likely to bite a working 3.x server.
**`ctx.elicit()` no longer reaches a default client.** Elicitation is era-gated in 4.0: `ctx.elicit()` works on handshake-era connections (≤ 2025-11-25) and raises on the modern `2026-07-28` protocol, which has no back-channel for a running tool to push a request down. Because `fastmcp.Client` now defaults to `mode="auto"`, an ordinary client negotiates the modern era against a FastMCP server — so a tool that elicited happily in 3.x now fails the call:
```
ToolError: elicitation via server-initiated requests is unavailable on 2026-07-28 connections.
```
The gate is strict in both directions, which is what makes it debuggable: a guard tool that returns an input request on a handshake connection raises the mirror-image error rather than misbehaving quietly. You have three ways forward. Rewrite the tool as a guard tool that *returns* a description of the input it needs, which is the form that works on modern connections. Branch on `ctx.request_context.protocol_version` and keep both paths if you serve both eras. Or keep this server's clients on the handshake era with `Client(server, mode="legacy")`, which leaves `ctx.elicit()` working as written. See [Elicitation](/servers/elicitation#which-approach-to-use) for the two shapes side by side.
**Middleware sees traffic it never saw before.** Dispatch now begins in the SDK's middleware layer, the single point every inbound message passes through, so `on_message`, `on_request`, and `on_notification` observe *every* message a client sends — including `notifications/cancelled`, `notifications/initialized`, and `notifications/progress`, and including requests that fail before reaching a handler, such as an unknown method or a `tools/call` whose params fail validation. In 3.x those never reached your hooks. Middleware that assumed every message it saw was a routable request, or that counted messages to measure tool traffic, needs a guard on the message type. The operation hooks (`on_call_tool`, `on_list_tools`, and the rest) are unaffected: they still fire exactly once per request and still receive typed component results. See [What middleware sees](/servers/middleware#what-middleware-sees).
**Templated resources are path-screened by default.** Every templated resource now has its extracted parameter values checked for path-traversal (`..` segments), absolute paths, and null bytes *before your handler runs*, at the server's read chokepoint. A rejected read returns a non-leaky "resource not found" error. Only a standalone `..` segment counts as traversal, so values that merely contain dots (`file.tar.gz`, `HEAD~3..HEAD`) and dotfiles (`.env`) still pass. If a template legitimately accepts `..`-bearing or absolute values, exempt the parameter with `ResourceSecurity(exempt_params={...})`, disable the check per-component with `security=None`, or set a server-wide default with `FastMCP(resource_security=...)`. See [Resources → Path Security](/servers/resources#path-security).
**Resource-not-found now returns `-32602`.** The wire error code for a missing resource from the core `resources/read` handler changed from `-32002` to `-32602` (`INVALID_PARAMS`, per SEP-2164). The human-readable message ("Resource not found: ...") is unchanged, so this only affects clients that matched on the numeric code — update those to expect `-32602`. (The opt-in `ErrorHandlingMiddleware` keeps its own per-method-prefix code mapping; if you run it with `transform_errors=True` it can still map not-found to a different code, so it is unaffected by this change.)
**An OAuth server whose `issuer_url` differs from its `base_url` re-authorizes its clients once.** `issuer_url` exists so a server's OAuth identity can differ from the URL its endpoints are mounted at — the usual case being a server under `/api` whose discovery lives at the host root. It now supplies the `issuer` in the authorization server metadata, the `iss` claim on every token the server mints, and the RFC 9207 `iss` on authorization responses; `base_url` still supplies `authorization_endpoint`, `token_endpoint`, and the rest, because that is where the routes are actually mounted. Both values previously came from `base_url`, which published an `issuer` contradicting the URL the client had just performed discovery at — a document RFC 8414 §3.3 requires a strict client to reject.
The cost of the correction is the `iss` on tokens already in the wild, so it falls on the providers that mint their own tokens — `OAuthProxy` and everything built on it. Access *and* refresh tokens carry the claim, and the verifier compares it exactly, so clients cannot refresh their way across the upgrade; it is a one-time full re-authorization. Interactive clients re-prompt and recover on their own, while a headless deployment holding a long-lived refresh token needs someone to re-authorize it. Plan the upgrade for a window where that is acceptable. If an identity provider mints SEP-990 ID-JAG assertions for this server, repoint their `aud` at the new issuer too — unless you pin the expected value with `IdentityAssertion(audience=...)`, which overrides the issuer and keeps working untouched.
Servers that leave `issuer_url` unset, or set it to the same value as `base_url`, are unaffected. It defaults to `base_url`, and the metadata and minted `iss` are byte-identical to what 3.x produced.
## Deprecation Timeline
The camelCase bridge is a migration aid, not a permanent fixture. It works today and warns on every bridged read so you can find and update the affected call sites. Plan to migrate your reads to snake_case: the shims will be removed in a future release, after which only the snake_case names resolve — the same state you get today by setting `mcp_camelcase_compat = False`. Turning the setting off is a good way to surface every remaining camelCase read in your code as a hard `AttributeError` before the shims go away.
## SDK deprecation warnings you may see
## SDK Deprecation Warnings
Ordinary use of `ctx.info` (client logging) and `ctx.sample` now emits an SDK-level `MCPDeprecationWarning`:
Ordinary use of `ctx.info` (client logging) emits an SDK-level `MCPDeprecationWarning`:
```
The logging/sampling capability is deprecated as of 2026-07-28 (SEP-2577)
The logging capability is deprecated as of 2026-07-28 (SEP-2577)
```
These warnings come from the MCP SDK, not from FastMCP. For logging they are benign: `ctx.info` keeps working on session-based connections exactly as the protocol table below describes, and the SDK is only signaling the protocol's direction. For sampling, FastMCP additionally emits its own `FastMCPDeprecationWarning`: `ctx.sample` and `ctx.sample_step` are deprecated and slated for removal, so treat that warning as a prompt to migrate to server-side LLM calls rather than as informational.
The warning comes from the MCP SDK, not from FastMCP, and it is benign. `ctx.info` and the rest of the logging methods keep working on every era, including the modern one — a log message is a *notification*, which rides the response stream the caller already opened. The SDK is signaling the protocol's direction for the capability declaration, not the notification itself.
## Protocol version support
## Protocol Version Support
FastMCP servers built on the SDK v2 serve multiple protocol eras from the same server. The SDK negotiates the era each client speaks: the sessionless `2026-07-28` era (which discovers capabilities through `server/discover`) and earlier session-based handshake versions are all handled simultaneously. This formally supersedes FastMCP's earlier "latest protocol only" stance — a single server now works with clients across the protocol transition.
Not every Context feature is available on every era yet. The imperative push APIs that call back into the client mid-execution — `ctx.elicit`, `ctx.sample`, and `ctx.list_roots` — depend on the session-based back-channel of the earlier eras, so on a `2026-07-28` connection they raise a clear, era-aware error rather than reaching the client. Elicitation itself still reaches the user on the modern era, through the guard pattern: a tool *returns* an `InputRequiredResult` describing what it needs, and the client answers with a fresh call (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)). Logging notifications and the request/response features flow on every era.
**`ctx.sample()`, `ctx.sample_step()`, and `ctx.list_roots()` are gone from `Context`**, along with the `sampling_handler=` and `sampling_handler_behavior=` arguments to `FastMCP()`. Touching a removed method raises `AttributeError` on every era, and `FastMCP(sampling_handler=...)` raises a `TypeError` naming the migration, so the break surfaces when you upgrade rather than in production against whichever client happens to negotiate the modern era.
Sampling is the exception that does not come back, and the reason is the protocol rather than an unfinished FastMCP feature. SEP-2577 deprecated server-initiated sampling, so `ctx.sample` and `ctx.sample_step` are **deprecated** and will be removed in a future FastMCP release. Elicitation moved to the guard pattern because the modern protocol still carries elicitation requests; sampling has no equivalent path because the protocol deprecated the pattern itself. The migration is to call an LLM directly from your server rather than borrowing the client's model. See [Sampling](/servers/sampling) for details.
All three *pushed*: the server sent a request down a live back-channel and blocked for the answer, and the sessionless protocol has no such channel. Since `fastmcp.Client` now negotiates the modern protocol by default, a method like that would fail against a default client. What the protocol removed is the pushing, not the asking — sampling, elicitation, and roots all still reach the client through the [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol), where a tool *returns* an `InputRequiredResult` describing what it needs, the client answers, and it calls again with the answer attached.
Migrating differs by capability. For **roots**, the guard pattern is the direct replacement: a server asks once and has what it needs, so the extra round buys the whole answer, and taking the paths as tool arguments is simpler still when the caller can just supply them. For **sampling**, the guard route works the same way, but generation usually belongs in your server, because every round is a full request-response cycle and a generation loop pays that cost repeatedly. [Call an LLM from your server](/servers/sampling) with your own API key and your tool behaves the same for every client, including the many that never implemented sampling; reach for the guard route when the point is specifically to use the caller's model. If borrowing the caller's model *is* your server — you hold no key of your own, and the token bill was never yours to pay — staying on FastMCP 3.x is the honest answer until that changes.
| Context feature | Earlier eras (session-based) | `2026-07-28` (sessionless) |
| --- | --- | --- |
| `ctx.info` / logging notifications | Supported | Supported |
| Tools, resources, prompts, completions | Supported | Supported |
| `ctx.elicit` | Supported | Use the guard pattern (return `InputRequiredResult`) |
| `ctx.sample` / `ctx.sample_step` | Supported (deprecated) | Removed — call an LLM server-side |
| `ctx.list_roots` | Supported | Via the guard pattern (`input_requests` carries roots requests) |
| Tasks (via the FastMCP client) | Supported | Not yet |
| `ctx.elicit` | Supported | Raises — use the guard pattern (return `InputRequiredResult`) |
| `ctx.sample` / `ctx.sample_step` | Method removed — call an LLM server-side | Method removed — call an LLM server-side, or ask via the guard pattern |
| `ctx.list_roots` | Method removed — take paths as tool arguments | Method removed — ask via the guard pattern, or take paths as tool arguments |
| `client.set_logging_level()` | Supported | Raises — `logging/setLevel` needs session state the era lacks |
| `Middleware.on_initialize` | Runs on connect | Never runs — there is no `initialize` handshake |
| Session state (`ctx.set_state` across calls) | Persists for the session | Does not persist — every request is a fresh connection |
| Background tasks (`task=True`) | Runs synchronously — never tasked | Supported via the tasks extension |
If your tools rely on `ctx.elicit` or `ctx.list_roots`, they continue to work against clients on the earlier eras; on the modern era, reach for the guard pattern instead (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)). Sampling is deprecated on every era and will not return on modern connections — migrate those tools to server-side LLM calls.
Several of these bite by default now, because **`fastmcp.Client` defaults to `mode="auto"`** in v4 — an ordinary `Client(server)` negotiates the newest protocol both sides share, which against a FastMCP server is the sessionless `2026-07-28` era. On that era there is no `initialize` handshake, so a `Middleware.on_initialize` hook never runs; each request is a fresh connection, so state written with `ctx.set_state` in one call is not visible in the next; and a tool that calls [`ctx.elicit()`](#behavior-changes) raises. A server that gates access in `on_initialize`, relies on per-session state, or elicits mid-tool must keep its clients on the session-based era. The control is per-client: `Client(server, mode="legacy")`. There is no server-side setting that restricts which protocol versions a server offers, so a server whose behavior depends on the handshake era depends on its callers opting into it — which is only practical when you control them. If you don't, port the behavior instead: a guard tool for elicitation, [session state](/servers/sessions) for what `ctx.set_state` held, and per-request auth checks for what `on_initialize` gated.
The client side is unaffected. `sampling_handler=` and `roots=` mean what they always did — see [client sampling](/clients/sampling) and [client roots](/clients/roots) — and one registration serves both routes, since a handshake-era server's pushed request and a modern server's returned one dispatch to the same handler.
## Upgrade Checklist
Most servers upgrade untouched. Work down this list to find the ones that don't:
1. **Bump your environment.** Raise any pin below `pydantic>=2.12`; upgrade FastAPI if your resolver complains about Starlette `<1.0.1`.
2. **Fix imports that moved out.** `from mcp.types import X` still works, but update any import from the [removed modules](#moved-imports) (`fastmcp.server.proxy`, `fastmcp.server.openapi`, `fastmcp.server.apps`, the `fastmcp.tools.tool` / `resources.resource` / `prompts.prompt` component shims).
3. **Update removed server APIs.** Swap `as_proxy` → `create_proxy`, `import_server` → `mount`, `mount(prefix=)` → `mount(namespace=)`, and the [other removed methods and keywords](#removed-server-methods).
4. **Replace `ctx.sample` and `ctx.list_roots`.** Both are gone from `Context`, as are `FastMCP(sampling_handler=...)` and `sampling_handler_behavior=`. Call an LLM directly from your server for generation; ask for roots through the guard pattern, or take file paths as tool arguments. A server whose purpose is to use the caller's model should stay on FastMCP 3.x rather than migrate.
5. **Find every `ctx.elicit()` call.** It raises on modern connections, which is what a default client now negotiates. Rewrite the tool as a guard tool, branch on `ctx.request_context.protocol_version`, or keep its clients on `mode="legacy"` — see [the era gate](#behavior-changes).
6. **Register the tasks extension.** A `task=True` tool needs `mcp.add_extension(TasksExtension())` or the server won't start. Drop `task=` from resource and prompt decorators, move `TaskConfig` to `fastmcp.utilities.tasks`, and replace client-side `call_tool(..., task=True)` with plain `call_tool` or `call_tool_task`.
7. **Update removed tool parameters.** Replace tool `serializer=` (return a `ToolResult`), `exclude_args=` (use `Depends()`), and `StreamableHttpTransport(sse_read_timeout=)`.
8. **Fix `McpError` construction.** Positional `McpError(ErrorData(...))` becomes keyword `McpError(code=..., message=...)`. Catching is unchanged.
9. **Move httpx to httpx2.** Grep for `except httpx.` and for custom `httpx_client_factory` / `httpx.Auth` objects handed to FastMCP, and swap the import to `httpx2`.
10. **Decide the client era.** `Client` now defaults to `mode="auto"`. If a server relies on `on_initialize`, per-session state, or `ctx.elicit()`, keep its clients on `mode="legacy"`, or port the behavior forward — there is no server-side protocol-version restriction.
11. **Verify behavior changes.** Confirm templated resources that legitimately accept `..` or absolute paths are exempted, guard any middleware that now sees notifications and unroutable requests, update any client that matched the old `-32002` resource-not-found code, and if your server mints its own OAuth tokens (`OAuthProxy` and the providers built on it) under an `issuer_url` that differs from its `base_url`, schedule the [one-time re-authorization](#behavior-changes) its clients now need.
12. **Run with the camelCase bridge off.** Set `mcp_camelcase_compat = False` (or `FASTMCP_MCP_CAMELCASE_COMPAT=false`) in CI to surface every remaining camelCase read as a hard `AttributeError` before the shims are removed.
The executable version of this checklist lives in [`tests/test_upgrade_from_v3.py`](https://github.com/PrefectHQ/fastmcp/blob/main/tests/test_upgrade_from_v3.py): it builds representative 3.x-style servers and asserts they run unchanged, and pins every removed surface to the exact error it now raises.

View file

@ -0,0 +1,623 @@
---
title: Upgrading from the Low-Level SDK v1
sidebarTitle: "From Low-Level SDK v1"
description: Upgrade your MCP server from v1 of the low-level Python SDK's Server class to FastMCP
icon: up
---
If you've been building MCP servers directly on the `mcp` package's `Server` class — writing `list_tools()` and `call_tool()` handlers, hand-crafting JSON Schema dicts, and wiring up transport boilerplate — this guide is for you. FastMCP replaces all of that machinery with a declarative, Pythonic API where your functions *are* the protocol surface.
The core idea: instead of telling the SDK what your tools look like and then separately implementing them, you write ordinary Python functions and let FastMCP derive the protocol layer from your code. Type hints become JSON Schema. Docstrings become descriptions. Return values are serialized automatically. The plumbing you wrote to satisfy the protocol just disappears.
## The SDK v2 Transition
MCP SDK v2 is a substantial, deliberate modernization of the protocol layer. Protocol types moved into a standalone `mcp_types` package, wire fields moved from camelCase to snake_case, and the low-level `Server` was rebuilt so handlers are passed to the constructor as `on_*` callables taking `(ctx, params)` rather than registered with decorators. A v1 server meets that change the moment its environment resolves `mcp` to v2:
```
AttributeError: 'Server' object has no attribute 'list_tools'
```
Often nobody chose that moment. An unpinned `mcp` dependency, a fresh lockfile, or a rebuilt container picks up the new major version. Nothing is wrong with your code, and nothing is wrong with the SDK — major versions are exactly where a change like this belongs. Your build just crossed it earlier than you planned to.
Pinning the SDK back restores the decorator API immediately, with no code changes, and buys you time to choose deliberately:
```bash
pip install "mcp<2"
```
## Two Upgrade Paths
Both directions are reasonable, and the choice is about which code you'd rather maintain.
**Porting the low-level `Server` to SDK v2** keeps you in direct control of the protocol surface, which is the point of the low-level API and the right call for some servers. The work is real: your imports, every handler signature, every handler's return type, and your error construction all move.
**Adopting FastMCP** is what the rest of this page walks through. What makes it less work is not that FastMCP is better — it's that the code most affected by the SDK v2 changes is precisely the code FastMCP doesn't ask you to write. Your `list_tools`/`call_tool` pair, hand-written JSON Schema, and content-block wrappers aren't ported to new signatures; they're deleted, and FastMCP derives all of it from your function signatures instead. FastMCP 4 runs on MCP SDK v2 underneath, so both paths land you on the same modern protocol layer.
<Note>
Already on SDK v2's rebuilt `Server` class, with constructor-registered `on_*` handlers? See [Upgrading from the Low-Level SDK v2](/getting-started/upgrading/from-low-level-sdk-v2) instead — the before-and-after code is different enough to warrant its own guide.
Using FastMCP 1.0 via `from mcp.server.fastmcp import FastMCP`? Your upgrade is a single import — see [Upgrading from MCP SDK v1](/getting-started/upgrading/from-mcp-sdk-v1).
</Note>
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
You are rewriting an MCP server built on v1 of the `mcp` package's low-level `Server` class (`mcp.server.Server` or `mcp.server.lowlevel.server.Server`, with decorator-registered handlers) using FastMCP 4's high-level API.
FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v1 — it explains every item below, with before-and-after code for each handler group. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs.
Then work through the provided code. This is a rewrite, not a patch: most of what you find gets deleted rather than translated.
CONSTRUCTION AND TRANSPORT
- `Server("name")`
- `async with stdio_server() as (r, w): await server.run(r, w, server.create_initialization_options())`
- `SseServerTransport` / `StreamableHTTPSessionManager` and any Starlette wiring around them
- `asyncio.run(main())` boilerplate
- `lifespan=` — carries over directly: pass the same async context manager to `FastMCP(lifespan=...)`, and read what it yields from `ctx.lifespan_context` in any tool. Do not drop it — the tools that depended on it (a DB connection, a client pool) lose their dependency silently if you do.
HANDLERS TO DELETE (each becomes one or more decorated functions)
- `@server.list_tools()` + `@server.call_tool()` — note the `if name == ...` dispatch chain inside call_tool; each branch becomes its own `@mcp.tool`
- `@server.list_resources()` + `@server.list_resource_templates()` + `@server.read_resource()` — note any manual URI parsing, which the `{placeholder}` syntax replaces
- `@server.list_prompts()` + `@server.get_prompt()`
- any other `@server.*()` handler in the file — completion, resource subscribe/unsubscribe, logging level, progress. Look these up in the FastMCP docs rather than assuming a decorator name maps one-to-one.
TYPES THAT DISAPPEAR FROM YOUR CODE
- hand-written `inputSchema` JSON Schema dicts — these come from type hints now
- `types.Tool`, `types.Resource`, `types.ResourceTemplate`, `types.Prompt`, `types.PromptArgument`
- `types.TextContent` wrappers around return values — return plain Python values instead
- `types.ImageContent`, `types.EmbeddedResource`
- `types.PromptMessage`, `types.GetPromptResult`
- Note that in the SDK v2 that FastMCP 4 builds on, `mcp.types` aliases the standalone `mcp_types` package; the import path still works, but the fields are snake_case now.
CONTEXT AND SIDE CHANNELS
- `server.request_context`
- `session.send_log_message(...)`, `session.send_progress_notification(...)`
- direct session use for anything else — a FastMCP `Context` has a `ctx.session` property returning the underlying SDK session, so this still works; prefer a `Context` method where one exists, and note the remaining uses as SDK-coupled
ERRORS
- `raise ValueError(f"Unknown tool: ...")` and other dispatch fallbacks — these become unnecessary
- `McpError` construction and any error-code mapping
For each item found, show the original code, say what it did, and give the FastMCP equivalent. Where several handlers collapse into one decorated function, show the collapse rather than a line-by-line mapping. Call out anything you could not find a documented FastMCP replacement for instead of inventing one.
</Prompt>
## Install
FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
```bash
pip install "fastmcp==4.0.0b1"
# or
uv add "fastmcp==4.0.0b1"
```
An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
FastMCP depends on the `mcp` package, so the SDK stays installed. FastMCP 4 builds on SDK v2, where the protocol types live in a standalone `mcp_types` package that stays importable as `mcp.types`. Most of your `mcp.types` imports disappear entirely in the rewrite below, since FastMCP derives the protocol types from your function signatures.
## Server and Transport
The `Server` class requires you to choose a transport, connect streams, build initialization options, and run an event loop. FastMCP collapses all of that into a constructor and a `run()` call.
<CodeGroup>
```python Before test="skip"
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
server = Server("my-server")
# ... register handlers ...
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options(),
)
asyncio.run(main())
```
```python After
from fastmcp import FastMCP
mcp = FastMCP("my-server")
# ... register tools, resources, prompts ...
if __name__ == "__main__":
mcp.run()
```
</CodeGroup>
Need HTTP instead of stdio? With the `Server` class, you'd wire up Starlette routes and `SseServerTransport` or `StreamableHTTPSessionManager`. With FastMCP:
```python
from fastmcp import FastMCP
mcp = FastMCP("my-server")
if __name__ == "__main__":
mcp.run(transport="http", host="0.0.0.0", port=8000)
```
## Tools
This is where the difference is most dramatic. The `Server` class requires two handlers — one to describe your tools (with hand-written JSON Schema) and another to dispatch calls by name. FastMCP eliminates both by deriving everything from your function signature.
<CodeGroup>
```python Before test="skip"
import mcp.types as types
from mcp.server import Server
server = Server("math")
@server.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="add",
description="Add two numbers",
inputSchema={
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"},
},
"required": ["a", "b"],
},
),
types.Tool(
name="multiply",
description="Multiply two numbers",
inputSchema={
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"},
},
"required": ["a", "b"],
},
),
]
@server.call_tool()
async def call_tool(
name: str, arguments: dict
) -> list[types.TextContent]:
if name == "add":
result = arguments["a"] + arguments["b"]
return [types.TextContent(type="text", text=str(result))]
elif name == "multiply":
result = arguments["a"] * arguments["b"]
return [types.TextContent(type="text", text=str(result))]
raise ValueError(f"Unknown tool: {name}")
```
```python After
from fastmcp import FastMCP
mcp = FastMCP("math")
@mcp.tool
def add(a: float, b: float) -> float:
"""Add two numbers"""
return a + b
@mcp.tool
def multiply(a: float, b: float) -> float:
"""Multiply two numbers"""
return a * b
```
</CodeGroup>
Each `@mcp.tool` function is self-contained: its name becomes the tool name, its docstring becomes the description, its type annotations become the JSON Schema, and its return value is serialized automatically. No routing. No schema dictionaries. No content-type wrappers.
### Type Mapping
When converting your `inputSchema` to Python type hints:
| JSON Schema | Python Type |
|---|---|
| `{"type": "string"}` | `str` |
| `{"type": "number"}` | `float` |
| `{"type": "integer"}` | `int` |
| `{"type": "boolean"}` | `bool` |
| `{"type": "array", "items": {"type": "string"}}` | `list[str]` |
| `{"type": "object"}` | `dict` |
| Optional property (not in `required`) | `param: str \| None = None` |
### Return Values
With the `Server` class, tools return `list[types.TextContent | types.ImageContent | ...]`. In FastMCP, return plain Python values — strings, numbers, dicts, lists, dataclasses, Pydantic models — and serialization is handled for you.
For images or other non-text content, FastMCP provides helpers:
```python
from fastmcp import FastMCP
from fastmcp.utilities.types import Image
mcp = FastMCP("media")
@mcp.tool
def create_chart(data: list[float]) -> Image:
"""Generate a chart from data."""
png_bytes = generate_chart(data) # your logic
return Image(data=png_bytes, format="png")
```
## Resources
The `Server` class uses three handlers for resources: `list_resources()` to enumerate them, `list_resource_templates()` for URI templates, and `read_resource()` to serve content — all with manual routing by URI. FastMCP replaces all three with per-resource decorators.
<CodeGroup>
```python Before test="skip"
import json
import mcp.types as types
from mcp.server import Server
from pydantic import AnyUrl
server = Server("data")
@server.list_resources()
async def list_resources() -> list[types.Resource]:
return [
types.Resource(
uri=AnyUrl("config://app"),
name="app_config",
description="Application configuration",
mimeType="application/json",
),
types.Resource(
uri=AnyUrl("config://features"),
name="feature_flags",
description="Active feature flags",
mimeType="application/json",
),
]
@server.list_resource_templates()
async def list_resource_templates() -> list[types.ResourceTemplate]:
return [
types.ResourceTemplate(
uriTemplate="users://{user_id}/profile",
name="user_profile",
description="User profile by ID",
),
types.ResourceTemplate(
uriTemplate="projects://{project_id}/status",
name="project_status",
description="Project status by ID",
),
]
@server.read_resource()
async def read_resource(uri: AnyUrl) -> str:
uri_str = str(uri)
if uri_str == "config://app":
return json.dumps({"debug": False, "version": "1.0"})
if uri_str == "config://features":
return json.dumps({"dark_mode": True, "beta": False})
if uri_str.startswith("users://"):
user_id = uri_str.split("/")[2]
return json.dumps({"id": user_id, "name": f"User {user_id}"})
if uri_str.startswith("projects://"):
project_id = uri_str.split("/")[2]
return json.dumps({"id": project_id, "status": "active"})
raise ValueError(f"Unknown resource: {uri}")
```
```python After
import json
from fastmcp import FastMCP
mcp = FastMCP("data")
@mcp.resource("config://app", mime_type="application/json")
def app_config() -> str:
"""Application configuration"""
return json.dumps({"debug": False, "version": "1.0"})
@mcp.resource("config://features", mime_type="application/json")
def feature_flags() -> str:
"""Active feature flags"""
return json.dumps({"dark_mode": True, "beta": False})
@mcp.resource("users://{user_id}/profile")
def user_profile(user_id: str) -> str:
"""User profile by ID"""
return json.dumps({"id": user_id, "name": f"User {user_id}"})
@mcp.resource("projects://{project_id}/status")
def project_status(project_id: str) -> str:
"""Project status by ID"""
return json.dumps({"id": project_id, "status": "active"})
```
</CodeGroup>
Static resources and URI templates use the same `@mcp.resource` decorator — FastMCP detects `{placeholders}` in the URI and automatically registers a template. The function parameter `user_id` maps directly to the `{user_id}` placeholder.
## Prompts
Same pattern: the `Server` class uses `list_prompts()` and `get_prompt()` with manual routing. FastMCP uses one decorator per prompt.
<CodeGroup>
```python Before test="skip"
import mcp.types as types
from mcp.server import Server
server = Server("prompts")
@server.list_prompts()
async def list_prompts() -> list[types.Prompt]:
return [
types.Prompt(
name="review_code",
description="Review code for issues",
arguments=[
types.PromptArgument(
name="code",
description="The code to review",
required=True,
),
types.PromptArgument(
name="language",
description="Programming language",
required=False,
),
],
)
]
@server.get_prompt()
async def get_prompt(
name: str, arguments: dict[str, str] | None
) -> types.GetPromptResult:
if name == "review_code":
code = (arguments or {}).get("code", "")
language = (arguments or {}).get("language", "")
lang_note = f" (written in {language})" if language else ""
return types.GetPromptResult(
description="Code review prompt",
messages=[
types.PromptMessage(
role="user",
content=types.TextContent(
type="text",
text=f"Please review this code{lang_note}:\n\n{code}",
),
)
],
)
raise ValueError(f"Unknown prompt: {name}")
```
```python After
from fastmcp import FastMCP
mcp = FastMCP("prompts")
@mcp.prompt
def review_code(code: str, language: str | None = None) -> str:
"""Review code for issues"""
lang_note = f" (written in {language})" if language else ""
return f"Please review this code{lang_note}:\n\n{code}"
```
</CodeGroup>
Returning a `str` from a prompt function automatically wraps it as a user message. For multi-turn prompts, return a `list[Message]`:
```python
from fastmcp import FastMCP
from fastmcp.prompts import Message
mcp = FastMCP("prompts")
@mcp.prompt
def debug_session(error: str) -> list[Message]:
"""Start a debugging conversation"""
return [
Message(f"I'm seeing this error:\n\n{error}"),
Message("I'll help you debug that. Can you share the relevant code?", role="assistant"),
]
```
## Request Context
The `Server` class exposes request context through `server.request_context`, which gives you the raw `ServerSession` for sending notifications. FastMCP replaces this with a typed `Context` object injected into any function that declares it.
<CodeGroup>
```python Before test="skip"
import mcp.types as types
from mcp.server import Server
server = Server("worker")
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "process_data":
ctx = server.request_context
await ctx.session.send_log_message(
level="info", data="Starting processing..."
)
# ... do work ...
await ctx.session.send_log_message(
level="info", data="Done!"
)
return [types.TextContent(type="text", text="Processed")]
```
```python After
from fastmcp import FastMCP, Context
mcp = FastMCP("worker")
@mcp.tool
async def process_data(ctx: Context) -> str:
"""Process data with progress logging"""
await ctx.info("Starting processing...")
# ... do work ...
await ctx.info("Done!")
return "Processed"
```
</CodeGroup>
The `Context` object provides logging (`ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`), progress reporting (`ctx.report_progress()`), resource subscriptions, session state, and more. See [Context](/servers/context) for the full API.
## Errors
Most of the errors a low-level server raises disappear with the dispatch that raised them: the `ValueError(f"Unknown tool: {name}")` fallback is unnecessary once FastMCP routes calls, and an exception from your function body is converted to a tool error for you.
Deliberate protocol errors are the exception, and they need a small rewrite. The v1 pattern wrapped an `ErrorData` and passed it positionally; FastMCP's `McpError` takes the fields directly:
```python test="skip"
from fastmcp.exceptions import McpError
# Before (SDK v1):
# raise McpError(ErrorData(code=-32000, message="Upstream unavailable"))
# After:
raise McpError(code=-32000, message="Upstream unavailable")
```
An optional third argument, `data=`, carries the structured payload `ErrorData` used to hold. Catching is unchanged — `except McpError` still works, and `err.error.code` still reads the code — so only construction sites need touching.
## Complete Example
A full server upgrade, showing how all the pieces fit together:
<CodeGroup>
```python Before expandable test="skip"
import asyncio
import json
import mcp.types as types
from mcp.server import Server
from mcp.server.stdio import stdio_server
from pydantic import AnyUrl
server = Server("demo")
@server.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="greet",
description="Greet someone by name",
inputSchema={
"type": "object",
"properties": {
"name": {"type": "string"},
},
"required": ["name"],
},
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
if name == "greet":
return [types.TextContent(type="text", text=f"Hello, {arguments['name']}!")]
raise ValueError(f"Unknown tool: {name}")
@server.list_resources()
async def list_resources() -> list[types.Resource]:
return [
types.Resource(
uri=AnyUrl("info://version"),
name="version",
description="Server version",
)
]
@server.read_resource()
async def read_resource(uri: AnyUrl) -> str:
if str(uri) == "info://version":
return json.dumps({"version": "1.0.0"})
raise ValueError(f"Unknown resource: {uri}")
@server.list_prompts()
async def list_prompts() -> list[types.Prompt]:
return [
types.Prompt(
name="summarize",
description="Summarize text",
arguments=[
types.PromptArgument(name="text", required=True)
],
)
]
@server.get_prompt()
async def get_prompt(
name: str, arguments: dict[str, str] | None
) -> types.GetPromptResult:
if name == "summarize":
return types.GetPromptResult(
description="Summarize text",
messages=[
types.PromptMessage(
role="user",
content=types.TextContent(
type="text",
text=f"Summarize:\n\n{(arguments or {}).get('text', '')}",
),
)
],
)
raise ValueError(f"Unknown prompt: {name}")
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream, write_stream,
server.create_initialization_options(),
)
asyncio.run(main())
```
```python After
import json
from fastmcp import FastMCP
mcp = FastMCP("demo")
@mcp.tool
def greet(name: str) -> str:
"""Greet someone by name"""
return f"Hello, {name}!"
@mcp.resource("info://version")
def version() -> str:
"""Server version"""
return json.dumps({"version": "1.0.0"})
@mcp.prompt
def summarize(text: str) -> str:
"""Summarize text"""
return f"Summarize:\n\n{text}"
if __name__ == "__main__":
mcp.run()
```
</CodeGroup>
## What You Gain
Deleting the handler machinery is the immediate payoff, but the reason to make this move is what becomes available once your server is a FastMCP server.
[Server composition](/servers/composition) mounts one server inside another, so a surface that grew unwieldy as a single `call_tool` dispatch splits into modules developed and tested independently. [Middleware](/servers/middleware) runs across every request for logging, rate limiting, error handling, and caching — the cross-cutting concerns that, on the low-level `Server`, meant threading the same code through every handler. [Proxy servers](/servers/providers/proxy) put a FastMCP server in front of any existing MCP server, bridging transports and adding auth to a backend you don't control, and the [OpenAPI integration](/integrations/openapi) generates an entire server from an API specification you already have. [Authentication](/servers/auth/authentication) arrives as a single `auth=` provider covering token verification, OAuth, and named providers for GitHub, Google, Auth0, and others.
The change most likely to affect your daily work is [testing](/servers/testing). FastMCP ships a client that connects to a server object in the same Python process, so a test calls your tools directly — no subprocess, no stdio pipes, no transport to stand up.

View file

@ -0,0 +1,622 @@
---
title: Upgrading from the Low-Level SDK v2
sidebarTitle: "From Low-Level SDK v2"
description: Move a server built on v2 of the low-level Python SDK's Server class to FastMCP
icon: up
---
If your server builds on the `mcp` package's low-level `Server` class as SDK v2 rebuilt it — handlers passed to the constructor as `on_list_tools`, `on_call_tool`, and their siblings, each taking `(ctx, params)` and returning a wrapped result object — this guide is for you. FastMCP replaces that machinery with a declarative API where your functions *are* the protocol surface.
The core idea: instead of describing your tools to the SDK and then separately implementing them, you write ordinary Python functions and let FastMCP derive the protocol layer from your code. Type hints become JSON Schema. Docstrings become descriptions. Return values are serialized automatically. The dispatch you wrote to route a call by name, and the schemas you wrote by hand to describe it, both disappear.
Migrating from SDK v2 is the most direct of the four upgrade paths, because you and FastMCP already share a protocol layer. FastMCP 4 is built on SDK v2, so `mcp_types` imports keep working, field names are already snake_case, and the era negotiation you get is the one you have. Almost nothing about the wire changes — the one exception is [argument strictness](#stricter-arguments), covered below.
<Note>
On SDK v1's decorator-registered `Server` — `@server.list_tools()`, `@server.call_tool()` — instead? See [Upgrading from the Low-Level SDK v1](/getting-started/upgrading/from-low-level-sdk-v1), where the before-and-after code matches that API.
Using SDK v2's high-level `MCPServer` class? See [Upgrading from MCP SDK v2](/getting-started/upgrading/from-mcp-sdk-v2) — that migration is mostly renaming.
</Note>
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
You are rewriting an MCP server built on the MCP Python SDK v2's low-level `Server` class (`mcp.server.lowlevel.server.Server`, with `on_*` handlers passed to the constructor) using FastMCP 4's high-level API.
FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v2 — it explains every item below in full, with before-and-after code. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not guess at a FastMCP API you have not confirmed in the docs.
Then work through the provided code looking for each of these. The guide has the replacement for every one:
CONSTRUCTION AND TRANSPORT
- `Server(name, on_list_tools=..., on_call_tool=..., ...)` — the whole constructor, including every handler passed to it
- `server.run(read_stream, write_stream, server.create_initialization_options())` and its `stdio_server()` context manager
- `server.streamable_http_app()` and any Starlette app assembled around it
- `asyncio.run(main())` boilerplate
- `lifespan=` — carries over directly: pass the same async context manager to `FastMCP(lifespan=...)`, and read what it yields from `ctx.lifespan_context` in any tool. Do not drop it — the tools that depended on it (a DB connection, a client pool) lose their dependency silently if you do.
HANDLERS TO DELETE, EACH REPLACED BY ONE DECORATOR (not simply removed)
- `on_list_tools` + `on_call_tool` → one `@mcp.tool` function per branch of the `if params.name == ...` dispatch chain inside `on_call_tool`
- `on_list_resources` + `on_list_resource_templates` + `on_read_resource` → one `@mcp.resource` function per resource/template
- `on_list_prompts` + `on_get_prompt` → one `@mcp.prompt` function per prompt
- `on_completion` → one `@mcp.completion` function. This one is easy to drop by mistake: skipping it does not just remove autocomplete cleanly, it silently stops FastMCP from advertising the completions capability at all, since that capability is only advertised when a handler is registered.
- `on_subscribe_resource` / `on_unsubscribe_resource` / `on_subscriptions_listen` — flag for the user, no single-decorator equivalent
- `on_set_logging_level`, `on_progress`, `on_roots_list_changed`, `on_ping` — flag for the user, these are protocol-level hooks with no direct FastMCP surface
TYPES THAT DISAPPEAR FROM YOUR CODE
- Hand-written `input_schema` / `output_schema` JSON Schema dicts — these come from type hints now
- `types.ListToolsResult`, `types.CallToolResult`, `types.ListResourcesResult`, `types.ListResourceTemplatesResult`, `types.ReadResourceResult`, `types.ListPromptsResult`, `types.GetPromptResult` — result wrappers FastMCP builds for you
- `types.TextContent`, `types.TextResourceContents`, `types.BlobResourceContents` — return plain Python values instead
- `types.ImageContent` / `types.AudioContent` — `fastmcp.utilities.types.Image` / `Audio`
- `types.Tool`, `types.Resource`, `types.ResourceTemplate`, `types.Prompt`, `types.PromptArgument` — declaration types FastMCP derives
- `types.PromptMessage` — `fastmcp.prompts.Message`
- Note which `mcp_types` imports are still needed afterward; protocol types are unchanged in FastMCP, so surviving imports stay as they are.
CONTEXT AND SIDE CHANNELS
- `ctx.session.send_log_message(...)` — `ctx.info()` / `ctx.debug()` / `ctx.warning()` / `ctx.error()` on a `fastmcp.Context` parameter
- `ctx.session.report_progress(...)` — `ctx.report_progress()`
- `ctx.request_id`, `ctx.meta`, `ctx.protocol_version` — these live on `ctx.request_context` in FastMCP (`ctx.request_context.request_id`, and so on); note that `ctx.protocol_version` directly on the Context does not exist
- `ctx.params` — no equivalent, and none is needed: the raw request params were how a low-level handler read the tool's arguments, and those are now the decorated function's typed parameters. `ctx.request_context.params` does NOT exist and raises AttributeError.
- Direct `ctx.session` use for anything else — `Context.session` exists in FastMCP too and returns the same raw SDK session, so this still works; prefer a `Context` method where one exists, and note the remaining uses as SDK-coupled
ERRORS AND AUTH
- `raise ValueError(f"Unknown tool: ...")` dispatch fallbacks — these become unnecessary
- `MCPError` construction and any error-code mapping
- `auth=AuthSettings(...)`, `token_verifier=`, `auth_server_provider=` — one `auth=` provider in FastMCP
- `TransportSecuritySettings`
For each item found, show the original code, say what it did, and give the FastMCP equivalent. Where several handlers collapse into one decorated function, show the collapse rather than a line-by-line mapping. Call out anything you could not find a documented FastMCP replacement for instead of inventing one.
</Prompt>
## Install
FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
```bash
pip install "fastmcp==4.0.0b1"
# or
uv add "fastmcp==4.0.0b1"
```
An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
FastMCP 4 depends on the MCP SDK v2 you are already using, so `mcp_types` stays importable and every protocol type keeps its current name and fields. Most of those imports vanish from your code anyway — FastMCP derives them — but the ones you keep need no changes.
## Server and Transport
The `Server` class asks you to open a transport, connect its streams, build initialization options, and run an event loop. FastMCP collapses that into a constructor and a `run()` call.
<CodeGroup>
```python Before test="skip"
import asyncio
from mcp.server.lowlevel.server import Server
from mcp.server.stdio import stdio_server
server = Server("my-server") # plus every on_* handler
async def main():
async with stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
server.create_initialization_options(),
)
asyncio.run(main())
```
```python After
from fastmcp import FastMCP
mcp = FastMCP("my-server")
# ... register tools, resources, prompts ...
if __name__ == "__main__":
mcp.run()
```
</CodeGroup>
Serving HTTP is the same shape. Where the low-level class hands you a Starlette app from `server.streamable_http_app()` and leaves the hosting to you, FastMCP runs it directly:
```python
from fastmcp import FastMCP
mcp = FastMCP("my-server")
if __name__ == "__main__":
mcp.run(transport="http", host="0.0.0.0", port=8000)
```
`mcp.http_app()` still returns a Starlette app when you need to mount the server inside a larger application.
## Tools
This is where the difference is largest. SDK v2 requires two handlers — one describing your tools with hand-written JSON Schema, one dispatching calls by name — and both are passed to the constructor, so the connection between a tool's declaration and its implementation lives only in your head. FastMCP derives both from the function.
<CodeGroup>
```python Before
import mcp_types as types
from mcp.server.context import ServerRequestContext
from mcp.server.lowlevel.server import Server
async def list_tools(ctx: ServerRequestContext, params) -> types.ListToolsResult:
number = {"type": "number"}
schema = {
"type": "object",
"properties": {"a": number, "b": number},
"required": ["a", "b"],
}
return types.ListToolsResult(
tools=[
types.Tool(name="add", description="Add two numbers", input_schema=schema),
types.Tool(
name="multiply", description="Multiply two numbers", input_schema=schema
),
]
)
async def call_tool(
ctx: ServerRequestContext, params: types.CallToolRequestParams
) -> types.CallToolResult:
arguments = params.arguments or {}
if params.name == "add":
result = arguments["a"] + arguments["b"]
elif params.name == "multiply":
result = arguments["a"] * arguments["b"]
else:
raise ValueError(f"Unknown tool: {params.name}")
return types.CallToolResult(content=[types.TextContent(type="text", text=str(result))])
server = Server("math", on_list_tools=list_tools, on_call_tool=call_tool)
```
```python After
from fastmcp import FastMCP
mcp = FastMCP("math")
@mcp.tool
def add(a: float, b: float) -> float:
"""Add two numbers"""
return a + b
@mcp.tool
def multiply(a: float, b: float) -> float:
"""Multiply two numbers"""
return a * b
```
</CodeGroup>
Each `@mcp.tool` function is self-contained: its name becomes the tool name, its docstring becomes the description, its annotations become the JSON Schema, and its return value is serialized for you. The dispatch chain, the schema dicts, the `CallToolResult` wrapper, the `TextContent` wrapper, and the unknown-tool fallback all go away — a tool that doesn't exist is now the framework's problem, not a branch you maintain.
### Type Mapping
Your hand-written `input_schema` becomes the function's parameters:
| JSON Schema | Python type |
|---|---|
| `{"type": "string"}` | `str` |
| `{"type": "number"}` | `float` |
| `{"type": "integer"}` | `int` |
| `{"type": "boolean"}` | `bool` |
| `{"type": "array", "items": {"type": "string"}}` | `list[str]` |
| `{"type": "object"}` | `dict` |
| A property absent from `required` | `param: str \| None = None` |
Constraints carry over too. A schema with `"minimum"` and `"maximum"` becomes a Pydantic `Field`, and a nested object schema becomes a Pydantic model or dataclass used as the annotation — FastMCP generates the same schema back out of it.
### Return Values
The low-level class requires tools to return a `CallToolResult` wrapping a list of content blocks. FastMCP takes the value itself — strings, numbers, dicts, lists, dataclasses, Pydantic models — and handles both the content block and the structured output. For images and audio, FastMCP provides wrapper types that carry the format:
```python
from fastmcp import FastMCP
from fastmcp.utilities.types import Image
mcp = FastMCP("media")
@mcp.tool
def create_chart(data: list[float]) -> Image:
"""Generate a chart from data."""
png_bytes = render_png(data) # your logic
return Image(data=png_bytes, format="png")
```
When you need full control over the wire result — multiple content blocks, or structured content that differs from the content blocks — return a `ToolResult` from `fastmcp.tools` instead.
### Stricter Arguments
Deriving the schema from your signature also tightens what callers may send, and this is the one behavior change the migration introduces. Your `on_call_tool` handler reads `params.arguments` as a plain dict and never looks at keys it doesn't need, so a call carrying an unexpected key succeeds. FastMCP declares `"additionalProperties": false` on the generated schema and enforces it, so the same call fails:
```python test="skip"
# Against the low-level handler: succeeds, "extra" never read.
# Against FastMCP: raises, "extra" is not a parameter of greet().
await client.call_tool("greet", {"name": "World", "extra": "surprise"})
```
For most servers this is an improvement that costs nothing — a caller sending keys your handler never read was already a bug, and the hand-written schema never advertised that they were allowed. It matters if a client in your fleet attaches metadata alongside real arguments, since those calls start failing the moment you migrate. Accept them explicitly as optional parameters if you need to keep them working.
## Resources
Resources take three handlers on the low-level class: one to list static resources, one to list URI templates, and one to read whichever URI arrives, with routing you write by hand. FastMCP replaces all three with a decorator per resource, and detects templates from the URI itself.
<CodeGroup>
```python Before
import json
import mcp_types as types
from mcp.server.context import ServerRequestContext
from mcp.server.lowlevel.server import Server
async def list_resources(ctx: ServerRequestContext, params) -> types.ListResourcesResult:
return types.ListResourcesResult(
resources=[
types.Resource(
uri="config://app",
name="app_config",
description="Application configuration",
mime_type="application/json",
)
]
)
async def list_resource_templates(
ctx: ServerRequestContext, params
) -> types.ListResourceTemplatesResult:
return types.ListResourceTemplatesResult(
resource_templates=[
types.ResourceTemplate(
uri_template="users://{user_id}/profile",
name="user_profile",
description="User profile by ID",
)
]
)
async def read_resource(
ctx: ServerRequestContext, params: types.ReadResourceRequestParams
) -> types.ReadResourceResult:
uri = str(params.uri)
if uri == "config://app":
text = json.dumps({"debug": False, "version": "1.0"})
elif uri.startswith("users://"):
user_id = uri.split("/")[2]
text = json.dumps({"id": user_id, "name": f"User {user_id}"})
else:
raise ValueError(f"Unknown resource: {uri}")
return types.ReadResourceResult(
contents=[
types.TextResourceContents(
uri=params.uri, mime_type="application/json", text=text
)
]
)
server = Server(
"data",
on_list_resources=list_resources,
on_list_resource_templates=list_resource_templates,
on_read_resource=read_resource,
)
```
```python After
import json
from fastmcp import FastMCP
mcp = FastMCP("data")
@mcp.resource("config://app", mime_type="application/json")
def app_config() -> str:
"""Application configuration"""
return json.dumps({"debug": False, "version": "1.0"})
@mcp.resource("users://{user_id}/profile", mime_type="application/json")
def user_profile(user_id: str) -> str:
"""User profile by ID"""
return json.dumps({"id": user_id, "name": f"User {user_id}"})
```
</CodeGroup>
The URI does the routing. A `{placeholder}` in the URI makes the resource a template, and FastMCP matches the parameter to the function argument of the same name — so the `uri.split("/")[2]` parsing goes away along with the handler that held it. Return a `str` for text content and `bytes` for binary; FastMCP builds the `TextResourceContents` or `BlobResourceContents` wrapper.
Templated resources also gain a protection the low-level version left to you: FastMCP screens extracted parameter values for path traversal, absolute paths, and null bytes before your function runs. See [Path Security](/servers/resources#path-security) if a template legitimately accepts those values.
## Prompts
The same collapse, one more time: `on_list_prompts` declares arguments as `PromptArgument` objects, `on_get_prompt` routes by name and assembles a `GetPromptResult` of `PromptMessage` objects. FastMCP takes a function whose parameters are the arguments.
<CodeGroup>
```python Before
import mcp_types as types
from mcp.server.context import ServerRequestContext
from mcp.server.lowlevel.server import Server
async def list_prompts(ctx: ServerRequestContext, params) -> types.ListPromptsResult:
return types.ListPromptsResult(
prompts=[
types.Prompt(
name="review_code",
description="Review code for issues",
arguments=[
types.PromptArgument(
name="code", description="The code to review", required=True
),
types.PromptArgument(
name="language", description="Programming language", required=False
),
],
)
]
)
async def get_prompt(
ctx: ServerRequestContext, params: types.GetPromptRequestParams
) -> types.GetPromptResult:
if params.name != "review_code":
raise ValueError(f"Unknown prompt: {params.name}")
arguments = params.arguments or {}
language = arguments.get("language", "")
note = f" (written in {language})" if language else ""
text = f"Please review this code{note}:\n\n{arguments.get('code', '')}"
return types.GetPromptResult(
description="Code review prompt",
messages=[
types.PromptMessage(
role="user", content=types.TextContent(type="text", text=text)
)
],
)
server = Server("prompts", on_list_prompts=list_prompts, on_get_prompt=get_prompt)
```
```python After
from fastmcp import FastMCP
mcp = FastMCP("prompts")
@mcp.prompt
def review_code(code: str, language: str | None = None) -> str:
"""Review code for issues"""
note = f" (written in {language})" if language else ""
return f"Please review this code{note}:\n\n{code}"
```
</CodeGroup>
Returning a `str` wraps it as a single user message. Whether an argument is required is read from the signature: `code` has no default, so it's required; `language` defaults to `None`, so it isn't. Multi-turn prompts return a list of `Message` objects, which take their text positionally and default to the user role:
```python
from fastmcp import FastMCP
from fastmcp.prompts import Message
mcp = FastMCP("prompts")
@mcp.prompt
def debug_session(error: str) -> list[Message]:
"""Start a debugging conversation"""
return [
Message(f"I'm seeing this error:\n\n{error}"),
Message("I'll help you debug that. Can you share the relevant code?", role="assistant"),
]
```
## Request Context
The low-level class hands each handler a `ServerRequestContext` carrying the raw `ServerSession`, and you reach through it to send notifications. FastMCP injects a typed `Context` into any function that declares one, and puts the operations you actually want on it directly.
<CodeGroup>
```python Before
import mcp_types as types
from mcp.server.context import ServerRequestContext
from mcp.server.lowlevel.server import Server
async def call_tool(
ctx: ServerRequestContext, params: types.CallToolRequestParams
) -> types.CallToolResult:
if params.name == "process_data":
await ctx.session.send_log_message(level="info", data="Starting processing...")
await ctx.session.report_progress(1, 2)
# ... do work ...
await ctx.session.send_log_message(level="info", data="Done!")
return types.CallToolResult(
content=[types.TextContent(type="text", text="Processed")]
)
raise ValueError(f"Unknown tool: {params.name}")
server = Server("worker", on_call_tool=call_tool)
```
```python After
from fastmcp import FastMCP, Context
mcp = FastMCP("worker")
@mcp.tool
async def process_data(ctx: Context) -> str:
"""Process data with progress logging"""
await ctx.info("Starting processing...")
await ctx.report_progress(1, 2)
# ... do work ...
await ctx.info("Done!")
return "Processed"
```
</CodeGroup>
The `Context` parameter is injected by type annotation and never appears in the tool's schema, so clients see `process_data` as taking no arguments. Beyond logging and progress, it carries resource reads, [session state](/servers/sessions), elicitation, and component visibility — see [Context](/servers/context) for the full surface.
One thing to check as you migrate: `ctx.session` still exists on a FastMCP `Context` as an escape hatch, and it hands back the same raw SDK session your handlers use today. That makes it a working translation for anything with no `Context` equivalent — but it's also the one part of your server that stays coupled to SDK internals, so reach for the `Context` method first and keep the escape hatch for what genuinely has no equivalent.
## Complete Example
Everything above, applied at once:
<CodeGroup>
```python Before expandable
import json
import mcp_types as types
from mcp.server.context import ServerRequestContext
from mcp.server.lowlevel.server import Server
async def list_tools(ctx: ServerRequestContext, params) -> types.ListToolsResult:
return types.ListToolsResult(
tools=[
types.Tool(
name="greet",
description="Greet someone by name",
input_schema={
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"],
},
)
]
)
async def call_tool(
ctx: ServerRequestContext, params: types.CallToolRequestParams
) -> types.CallToolResult:
if params.name == "greet":
name = (params.arguments or {})["name"]
return types.CallToolResult(
content=[types.TextContent(type="text", text=f"Hello, {name}!")]
)
raise ValueError(f"Unknown tool: {params.name}")
async def list_resources(ctx: ServerRequestContext, params) -> types.ListResourcesResult:
return types.ListResourcesResult(
resources=[
types.Resource(
uri="info://version", name="version", description="Server version"
)
]
)
async def read_resource(
ctx: ServerRequestContext, params: types.ReadResourceRequestParams
) -> types.ReadResourceResult:
if str(params.uri) != "info://version":
raise ValueError(f"Unknown resource: {params.uri}")
return types.ReadResourceResult(
contents=[
types.TextResourceContents(
uri=params.uri, text=json.dumps({"version": "1.0.0"})
)
]
)
async def list_prompts(ctx: ServerRequestContext, params) -> types.ListPromptsResult:
return types.ListPromptsResult(
prompts=[
types.Prompt(
name="summarize",
description="Summarize text",
arguments=[types.PromptArgument(name="text", required=True)],
)
]
)
async def get_prompt(
ctx: ServerRequestContext, params: types.GetPromptRequestParams
) -> types.GetPromptResult:
if params.name != "summarize":
raise ValueError(f"Unknown prompt: {params.name}")
text = (params.arguments or {}).get("text", "")
return types.GetPromptResult(
description="Summarize text",
messages=[
types.PromptMessage(
role="user",
content=types.TextContent(type="text", text=f"Summarize:\n\n{text}"),
)
],
)
server = Server(
"demo",
on_list_tools=list_tools,
on_call_tool=call_tool,
on_list_resources=list_resources,
on_read_resource=read_resource,
on_list_prompts=list_prompts,
on_get_prompt=get_prompt,
)
```
```python After
import json
from fastmcp import FastMCP
mcp = FastMCP("demo")
@mcp.tool
def greet(name: str) -> str:
"""Greet someone by name"""
return f"Hello, {name}!"
@mcp.resource("info://version")
def version() -> str:
"""Server version"""
return json.dumps({"version": "1.0.0"})
@mcp.prompt
def summarize(text: str) -> str:
"""Summarize text"""
return f"Summarize:\n\n{text}"
if __name__ == "__main__":
mcp.run()
```
</CodeGroup>
## What You Gain
Deleting the handler machinery is the immediate payoff, but the reason to make this move is what becomes available once your server is a FastMCP server.
[Server composition](/servers/composition) mounts one server inside another, so a surface that grew unwieldy as a single dispatch chain splits into modules developed and tested independently. [Middleware](/servers/middleware) runs across every request for logging, rate limiting, error handling, and caching, with hooks at whichever level of specificity you need — the cross-cutting concerns that, on the low-level class, meant threading the same code through every handler. [Proxy servers](/servers/providers/proxy) put a FastMCP server in front of any existing MCP server, bridging transports and adding auth to a backend you don't control, and the [OpenAPI integration](/integrations/openapi) generates an entire server from an API specification you already have. [Authentication](/servers/auth/authentication) consolidates the SDK's separate token verifier, authorization-server provider, and `AuthSettings` into a single `auth=` provider, with named providers for GitHub, Google, Auth0, Keycloak, and others.
The change most likely to affect your daily work is [testing](/servers/testing). FastMCP ships a client that connects to a server object in the same Python process, so a test calls your tools directly — no subprocess, no stdio pipes, no transport to stand up.

View file

@ -0,0 +1,264 @@
---
title: Upgrading from MCP SDK v1
sidebarTitle: "From MCP SDK v1"
description: Upgrade from FastMCP 1.0, bundled in v1 of the MCP Python SDK, to the standalone FastMCP framework
icon: up
---
If your server starts with `from mcp.server.fastmcp import FastMCP`, you're using FastMCP 1.0 — the version bundled with v1 of the `mcp` package. Upgrading to the standalone FastMCP framework is easy. **For most servers, it's a single import change.**
```python test="skip"
# Before
from mcp.server.fastmcp import FastMCP
# After
from fastmcp import FastMCP
```
That's it. Your `@mcp.tool`, `@mcp.resource`, and `@mcp.prompt` decorators, your `mcp.run()` call, and the rest of your server code all work as-is.
<Tip>
**Why upgrade?** FastMCP 1.0 pioneered the Pythonic MCP server experience, and we're proud it was bundled into the `mcp` package. The standalone FastMCP project has since grown into a full framework for taking MCP servers from prototype to production — with composition, middleware, proxy servers, authentication, and much more. Upgrading gives you access to all of that, plus ongoing updates and fixes.
</Tip>
## The SDK v2 Transition
MCP SDK v2 is a substantial, deliberate modernization of the protocol layer, and part of that work rebuilt the high-level server as `MCPServer` under `mcp.server.mcpserver`. `mcp.server.fastmcp` does not exist there — so a FastMCP 1.0 server meets the change the moment its environment resolves `mcp` to v2:
```
ModuleNotFoundError: No module named 'mcp.server.fastmcp'
```
Often nobody chose that moment. An unpinned `mcp` dependency, a fresh lockfile, or a rebuilt container picks up the new major version and the module your server imports on line one has moved. Nothing is wrong with your code, and nothing is wrong with the SDK — major versions are exactly where a change like this belongs. Your build just crossed it earlier than you planned to.
Pinning the SDK back restores the old module immediately, with no code changes, and buys you time to choose deliberately:
```bash
pip install "mcp<2"
```
## Two Upgrade Paths
From here, both directions are reasonable, and which is less work depends on which API you already write.
**`MCPServer`, the SDK's high-level server**, is a capable, well-designed API and the direct continuation of the SDK's own line. Because it was rebuilt rather than renamed, expect real work: a new class and import, a different decorator call style, and protocol types imported from the standalone `mcp_types` package with snake_case field names.
**FastMCP** is the import change at the top of this page. It is short for a specific, historical reason: FastMCP 1.0 *is* early FastMCP — it was contributed into the `mcp` package, and the standalone project kept developing that same high-level API. The surface you already write against is the surface FastMCP still offers. FastMCP 4 is itself built on MCP SDK v2, so both paths land you on the same modern protocol layer; FastMCP absorbs the adaptation internally rather than asking your code to do it.
The claim is narrower than it may sound. It holds for FastMCP 1.0 servers specifically, because of shared lineage — not because one library is better than the other. Both projects are moving the same direction on the same protocol.
If you have already moved to SDK v2 and write against `MCPServer` today, see [Upgrading from MCP SDK v2](/getting-started/upgrading/from-mcp-sdk-v2). If your server uses the low-level `Server` class rather than the high-level one, see [Upgrading from the Low-Level SDK v1](/getting-started/upgrading/from-low-level-sdk-v1).
## Install
FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
```bash
pip install "fastmcp==4.0.0b1"
# or
uv add "fastmcp==4.0.0b1"
```
An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
FastMCP depends on the `mcp` package, so the SDK stays installed and importable. What changes is which parts of it you reach for. FastMCP 4 builds on SDK v2, where `mcp.server.fastmcp` is gone — anything you imported from it needs a new home, and the sections below cover that. `mcp.types` still resolves (it aliases the standalone `mcp_types` package), though its fields are snake_case now. Update your import, run your server, and if your tools work, you're done.
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
You are upgrading an MCP server from FastMCP 1.0 (bundled in v1 of the `mcp` package) to standalone FastMCP 4.
FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v1 — it explains every item below, with the replacement code. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs.
For most servers the entire upgrade is the first item. Work through the rest looking for signals, and report only what you actually find.
THE IMPORT (every server needs this)
- `from mcp.server.fastmcp import FastMCP` → `from fastmcp import FastMCP`
- `from mcp.server.fastmcp import Context`
- `from mcp.server.fastmcp import Image`
CONSTRUCTOR ARGUMENTS THAT MOVED (all raise TypeError)
- moved to run()/http_app(), and FastMCP names them in the error: host, port, log_level, debug, sse_path, message_path, streamable_http_path, json_response, stateless_http
- moved but rejected with only a generic "unexpected keyword argument", so flag these explicitly: `event_store=` (→ `http_app(event_store=...)`; dropping it silently disables streamable-HTTP resumability), `mount_path=` (→ `http_app(path=...)`), `transport=` (→ `run(transport=...)`), `transport_security=` (→ host/origin settings on `http_app()`), `warn_on_duplicate_tools/_resources/_prompts=` (→ one `on_duplicate=`), `dependencies=` (→ a fastmcp.json file)
- `name`, `instructions`, `website_url`, `icons`, `tools`, `lifespan` carry over unchanged
- note when reporting: FastMCP names the streamable HTTP transport "http", not "streamable-http"
CONTEXT METHODS WITH CHANGED SIGNATURES (compile fine, fail at runtime)
- `ctx.log(level, data)` → `ctx.log(message, level=...)`, message first
- `ctx.info(data)` / `debug` / `warning` / `error` → take a str message, not arbitrary JSON-serializable data
- `ctx.elicit(..., schema=Model)` → `response_type=Model`
- `ctx.read_resource(uri)` → returns a `ResourceResult`; read `.contents` rather than iterating the return value
- `ctx.report_progress`, `ctx.request_id`, `ctx.client_id` are unchanged
AUTHENTICATION (the one case where the single import change is NOT enough)
- `token_verifier=` and `auth_server_provider=` — both raise TypeError on FastMCP 4
- `auth=AuthSettings(...)` — the keyword survives but the value does not: FastMCP's `auth=` takes a FastMCP `AuthProvider`, not the SDK settings object
Report these as a real migration, not a rename: FastMCP consolidates all three into one provider, and ships `JWTVerifier` for tokens you already issue, `RemoteAuthProvider` for delegating to an external authorization server, `OAuthProxy` for wrapping a provider without Dynamic Client Registration, and named providers for GitHub, Google, Auth0, Keycloak, and others. Look up the right one at https://gofastmcp.com/servers/auth/authentication rather than guessing.
PROMPT RETURN VALUES
- prompt functions returning `PromptMessage`, or `TextContent`-wrapped content
- prompt functions returning raw dicts with "role"/"content" keys — FastMCP 1.0 coerced these silently, standalone FastMCP does not
OTHER mcp.* IMPORTS
- anything from `mcp.types` — the import path still works in the SDK v2 that FastMCP 4 builds on, but the fields were renamed from camelCase to snake_case
- `from mcp.server.stdio import stdio_server` and any transport boilerplate around it
- `mcp.types.TextContent` / `ImageContent` used to wrap tool return values — FastMCP has friendlier equivalents, so prefer those over keeping the raw protocol types
DECORATOR RETURN VALUES
- any code reading `.name`, `.description`, or other component attributes off a `@mcp.tool` / `@mcp.resource` / `@mcp.prompt` decorated function. Decorators return the original function now.
For each item found, show the original line, name what changed, and give the corrected code from the guide. If the only change needed is the import, say so plainly rather than manufacturing work.
</Prompt>
## What Might Need Updating
Most servers need nothing beyond the import change. Skim the sections below to see if any apply.
### Constructor Settings
If you passed transport settings like `host` or `port` directly to `FastMCP()`, those now belong on `run()`. This keeps your server definition independent of how it's deployed:
```python test="skip"
from fastmcp import FastMCP
# Before
mcp = FastMCP("my-server", host="0.0.0.0", port=8080)
mcp.run()
# After
mcp = FastMCP("my-server")
mcp.run(transport="http", host="0.0.0.0", port=8080)
```
Nine arguments move this way, and each raises a `TypeError` naming its own replacement, so you can also just run the server and follow the errors: `host`, `port`, `log_level`, `debug`, `sse_path`, `message_path`, `streamable_http_path`, `json_response`, and `stateless_http`.
A second group is rejected with only a generic "unexpected keyword argument" and no hint, which makes these the ones worth reading in advance:
| SDK v1 `FastMCP(...)` | FastMCP 4 |
|---|---|
| `event_store=` | `mcp.http_app(event_store=...)` |
| `mount_path=` | `mcp.http_app(path=...)` |
| `transport=` | `mcp.run(transport=...)` |
| `transport_security=` | `host_origin_protection=`, `allowed_hosts=`, `allowed_origins=` on `http_app()` |
| `warn_on_duplicate_tools=`, `_resources=`, `_prompts=` | a single `on_duplicate=` |
| `dependencies=[...]` | a [`fastmcp.json`](/deployment/server-configuration) configuration file |
| `auth_server_provider=`, `token_verifier=` | a single `auth=` provider — see [Authentication](#authentication) below |
Dropping `event_store=` rather than moving it is the one to watch: it silently disables streamable-HTTP resumability, so a client that reconnects loses the events it missed instead of replaying them.
`name`, `instructions`, `website_url`, `icons`, `tools`, and `lifespan` carry over to the constructor unchanged.
### Authentication
This is the one case where the import change alone won't do. FastMCP 1.0 exposed the SDK's auth plumbing as three separate constructor arguments — `token_verifier=`, `auth_server_provider=`, and `auth=AuthSettings(...)`. The first two raise `TypeError` on FastMCP 4, and while `auth=` survives as a keyword, its value doesn't: FastMCP expects one of its own `AuthProvider` objects rather than the SDK's settings object.
The replacement is a single provider carrying the whole configuration, chosen by what you're actually doing:
| What you were doing | FastMCP provider |
|---|---|
| Validating JWTs you already issue | `JWTVerifier` |
| Delegating to an external authorization server | `RemoteAuthProvider` |
| Wrapping a provider without Dynamic Client Registration | `OAuthProxy` |
| GitHub, Google, Auth0, Keycloak, WorkOS, … | the matching named provider |
```python
from fastmcp import FastMCP
from fastmcp.server.auth import JWTVerifier
mcp = FastMCP("my-server", auth=JWTVerifier(jwks_uri="https://example.com/.well-known/jwks.json"))
```
See [Authentication](/servers/auth/authentication) for the full set and their configuration.
### Context Methods
`from fastmcp import Context` gets you the injected context object, but four of its methods took a different shape in FastMCP 1.0, and a bare import swap leaves calls that compile and then fail:
| SDK v1 | FastMCP 4 |
|---|---|
| `ctx.log(level, data)` | `ctx.log(message, level=...)` — message is first now |
| `ctx.info(data)` and its `debug`/`warning`/`error` siblings | take a `str` message, where v1 accepted any JSON-serializable value |
| `ctx.elicit(message, schema=Model)` | `ctx.elicit(message, response_type=Model)` |
| `ctx.read_resource(uri)` | returns a `ResourceResult`; the payload is under `.contents` rather than being iterable directly |
`ctx.report_progress()`, `ctx.request_id`, and `ctx.client_id` are unchanged.
### Prompts
If your prompt functions return `mcp.types.PromptMessage` objects or raw dicts with `role`/`content` keys, upgrade them to FastMCP's `Message` class. Or just return a plain string — it's automatically wrapped as a user message. FastMCP 1.0 silently coerced dicts into messages; standalone FastMCP requires typed `Message` objects or strings.
```python
from fastmcp import FastMCP
mcp = FastMCP("prompts")
@mcp.prompt
def review(code: str) -> str:
"""Review code for issues"""
return f"Please review this code:\n\n{code}"
```
Multi-turn prompts return a list of messages. `Message` takes the text positionally and defaults to the user role, so only the assistant turns need a `role`:
```python
from fastmcp import FastMCP
from fastmcp.prompts import Message
mcp = FastMCP("prompts")
@mcp.prompt
def debug(error: str) -> list[Message]:
"""Start a debugging session"""
return [
Message(f"I'm seeing this error:\n\n{error}"),
Message("I'll help debug that. Can you share the relevant code?", role="assistant"),
]
```
### Other `mcp.*` Imports
FastMCP 4 builds on MCP SDK v2, which moved the protocol types into a standalone `mcp_types` package and re-exports it as `mcp.types` — so `from mcp.types import X` keeps working. The field names did change, from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, and so on). For everything else SDK v2 changed, see [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3), which covers the same protocol rebuild from the FastMCP side.
Where FastMCP provides its own API for the same thing, it's worth switching over rather than importing the protocol type:
| MCP SDK v1 | FastMCP equivalent |
|---|---|
| `mcp.types.TextContent(type="text", text=str(x))` | Just return `x` from your tool |
| `mcp.types.ImageContent(...)` | `from fastmcp.utilities.types import Image` |
| `mcp.types.PromptMessage(...)` | `from fastmcp.prompts import Message` |
| `mcp.server.fastmcp.Context` | `from fastmcp import Context` |
| `from mcp.server.stdio import stdio_server` | Not needed — `mcp.run()` handles transport |
For protocol types without a FastMCP equivalent, import them from `mcp_types` directly.
### Decorated Functions
In FastMCP 1.0, `@mcp.tool` replaced your function with a `FunctionTool` object. Now decorators return your original function unchanged, so decorated functions stay callable for testing, reuse, and composition:
```python
from fastmcp import FastMCP
mcp = FastMCP("greeter")
@mcp.tool
def greet(name: str) -> str:
"""Greet someone"""
return f"Hello, {name}!"
# This works now — the function is still a regular function
assert greet("World") == "Hello, World!"
```
Code that reads `.name`, `.description`, or other component attributes off the decorated result needs updating. This is uncommon — most servers never touch the tool object. When you do need the component itself, reach it through the server with `await mcp.get_tool("greet")`.
## Verifying the Upgrade
Run your server the way you always have. To confirm every component came across, inspect the server with the FastMCP CLI:
```bash
fastmcp inspect my_server.py
```
The output lists every tool, resource, template, and prompt your server exposes, so a component that failed to register shows up here rather than at the first client call.
## Looking Ahead
The MCP ecosystem is evolving fast. Part of FastMCP's job is to absorb that complexity on your behalf — as the protocol and its tooling grow, we do the work so your server code doesn't have to change. The SDK v1 to v2 transition is the clearest example so far: an entire protocol layer was rewritten underneath FastMCP 4, and the servers on this page cross it with one line.

View file

@ -0,0 +1,328 @@
---
title: Upgrading from MCP SDK v2
sidebarTitle: "From MCP SDK v2"
description: Move a server built on the MCP Python SDK v2's MCPServer class to FastMCP
icon: up
---
If your server starts with `from mcp.server.mcpserver import MCPServer`, you're using the high-level server API introduced in v2 of the `mcp` package. Moving to FastMCP is a mechanical migration: the two APIs share a lineage, so most of your code carries over with a rename.
```python
# Before
from mcp.server.mcpserver import MCPServer
server = MCPServer("my-server")
@server.tool()
def greet(name: str) -> str:
"""Greet someone by name"""
return f"Hello, {name}!"
# After
from fastmcp import FastMCP
mcp = FastMCP("my-server")
@mcp.tool
def greet(name: str) -> str:
"""Greet someone by name"""
return f"Hello, {name}!"
```
That resemblance is not a coincidence. `MCPServer` is the SDK's successor to FastMCP 1.0, the high-level server that shipped inside SDK v1; FastMCP is the standalone framework that grew from the same starting point. Both derive the protocol layer from your function signatures — type hints become JSON Schema, docstrings become descriptions, return values are serialized for you. What separates them is scope: `MCPServer` is the SDK's ergonomic surface over the protocol, while FastMCP builds on that same SDK v2 and adds the machinery a server needs in production — composition, middleware, proxying, authentication providers, tool transformation, a client, and a testing story.
<Note>
Building on the low-level `Server` class instead? See [Upgrading from the Low-Level SDK v2](/getting-started/upgrading/from-low-level-sdk-v2). Still on SDK v1's `mcp.server.fastmcp.FastMCP`? Your upgrade is a single import — see [Upgrading from MCP SDK v1](/getting-started/upgrading/from-mcp-sdk-v1).
</Note>
<Prompt description="Copy this prompt into any LLM along with your server code to get automated upgrade guidance.">
You are migrating an MCP server from the MCP Python SDK v2's high-level `MCPServer` class (`mcp.server.mcpserver`) to FastMCP 4. The two APIs are close relatives, so most of this is mechanical renaming.
FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v2 — it carries the full mapping table and before-and-after code for everything below. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs.
Then work through the provided code looking for each of these.
IMPORTS AND CONSTRUCTION
- `MCPServer`, and `Context`, `Image`, `Audio`, `Message` imported from `mcp.server.mcpserver`
- `mcp_types` imports — these are UNCHANGED. FastMCP 4 builds on the same SDK v2, so leave them alone and say so.
DECORATORS
- `@server.tool()`, `@server.prompt()` — FastMCP takes a bare `@mcp.tool` / `@mcp.prompt` (and still accepts the called form)
- `@server.resource(...)`, `@server.completion()`, `@server.custom_route(...)`
TRANSPORT
- `run(transport="streamable-http")` — FastMCP names this transport "http"
- `streamable_http_app()`, `sse_app()`
CONSTRUCTOR ARGUMENTS THAT DO NOT CARRY OVER
- `debug=`, `log_level=`
- `warn_on_duplicate_tools=` / `_resources=` / `_prompts=`
- `dependencies=`
- `title=`, `description=`
- `token_verifier=`, `auth_server_provider=`, `auth=AuthSettings(...)` — FastMCP consolidates all three into one `auth=` provider
- `cache_hints=`
- `extensions=`
- `tools=[...]` (rare — the SDK's `Tool` type is not exported): FastMCP takes plain callables, so pass the underlying functions
These raise TypeError, most naming their replacement. `name`, `version`, `instructions`, `icons`, `website_url`, `lifespan`, `resource_security`, and `request_state_security` carry over unchanged.
CONTEXT — these ten properties do NOT exist on FastMCP's Context and raise AttributeError if you only swap the import:
- `ctx.mcp_server` → `ctx.fastmcp`
- `ctx.headers` → `get_http_headers()` from `fastmcp.server.dependencies` (a function, not a property)
- `ctx.protocol_version` → `ctx.request_context.protocol_version`
- `ctx.client_capabilities` → read it off `ctx.session` / `ctx.request_context`
- `ctx.notify_tools_changed()`, `notify_resources_changed()`, `notify_prompts_changed()`, `notify_resource_updated()` → `ctx.send_notification(...)` with the matching `mcp_types` notification. FastMCP emits the list-changed ones for you when components change visibility through `ctx.enable_components` / `ctx.disable_components`.
- `ctx.elicit_url` → not the same thing as `ctx.elicit` (that one is form elicitation, with a different signature and wire behavior). The URL flow survives on the raw session as `ctx.session.elicit_url(...)` — use that rather than deleting an OAuth or payment handoff.
- `ctx.close_standalone_sse_stream` → no public FastMCP equivalent, and NOT on `ctx.request_context`. Flag it for the user.
These four exist on both but with DIFFERENT signatures, so a bare import swap compiles and then fails at runtime:
- `ctx.log(level, data)` → `ctx.log(message, level=...)` — the first positional argument is now the message, not the level
- `ctx.info(data)` / `debug` / `warning` / `error` → these take `message` as a string, where the SDK accepted any JSON-serializable `data`
- `ctx.elicit(message, schema=Model)` → `ctx.elicit(message, response_type=Model)` — the keyword was renamed
- `ctx.read_resource(uri)` → still takes a URI, but returns a `ResourceResult` whose payload is under `.contents`, where the SDK returned an iterable of content objects directly. Code that iterates or indexes the return value needs updating.
Genuinely unchanged: `report_progress`, `request_id`, `client_id`, `input_responses`, `request_state`, `session`, and `request_context`.
RESOLVERS — the one part that is not a rename, so check for it first
- any `Annotated[T, Resolve(fn)]` parameter, and the resolvers behind it
- resolvers returning `Elicit[...]`, `Sample`, or `ListRoots`
FastMCP has no resolver injection, but the underlying requests survive in a different shape: on a modern connection `Elicit`, `Sample`, and `ListRoots` all ride the guard pattern, where the tool returns an `InputRequiredResult` and the client answers on the next call. Do not tell the user these capabilities are simply unavailable. Flag every resolver with the guide's per-capability reasoning (server-side LLM call is usually better than guard-routed sampling; roots are often simplest as ordinary tool arguments) rather than picking a rewrite yourself. Also note that a resolved parameter is hidden from the tool's input schema, so replacing it with an ordinary argument changes the schema clients see.
For each item found, show the original code, name what changed, and give the FastMCP equivalent from the guide. Call out anything you could not find a documented replacement for instead of inventing one.
</Prompt>
## Install
FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
```bash
pip install "fastmcp==4.0.0b1"
# or
uv add "fastmcp==4.0.0b1"
```
An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
FastMCP 4 depends on the MCP SDK v2, so nothing you already import from `mcp_types` moves. That is the practical benefit of migrating at this version rather than an earlier one: you and FastMCP are on the same protocol layer, with the same snake_case field names and the same type package, so the migration touches only the server API.
## The Mechanical Part
Most of the work is renaming. This table covers the surfaces a typical `MCPServer` server touches:
| MCP SDK v2 | FastMCP |
|---|---|
| `from mcp.server.mcpserver import MCPServer` | `from fastmcp import FastMCP` |
| `from mcp.server.mcpserver import Context` | `from fastmcp import Context` |
| `from mcp.server.mcpserver import Image, Audio` | `from fastmcp.utilities.types import Image, Audio` |
| `from mcp.server.mcpserver.prompts.base import Message` | `from fastmcp.prompts import Message` |
| `@server.tool()` | `@mcp.tool` |
| `@server.prompt()` | `@mcp.prompt` |
| `@server.resource("uri://x")` | `@mcp.resource("uri://x")` |
| `@server.completion()` | `@mcp.completion` |
| `@server.custom_route(path, methods)` | `@mcp.custom_route(path, methods)` |
| `server.run(transport="streamable-http")` | `mcp.run(transport="http")` |
| `server.streamable_http_app()` | `mcp.http_app()` |
| `server.sse_app()` | `mcp.http_app(transport="sse")` |
| `ctx.mcp_server` | `ctx.fastmcp` |
| `ctx.headers` | `get_http_headers()` from `fastmcp.server.dependencies` |
| `ctx.protocol_version` | `ctx.request_context.protocol_version` |
| `ctx.client_capabilities` | read it off `ctx.session` |
| `from mcp_types import X` | unchanged |
Two of these are worth a sentence each. The decorators lose their parentheses: `MCPServer` required `@server.tool()` and raised a `TypeError` telling you so if you wrote `@server.tool`, while FastMCP accepts both forms, so `@mcp.tool` is the idiomatic spelling and `@mcp.tool()` keeps working if you'd rather not touch every line. And the streamable HTTP transport is named `"http"` in FastMCP rather than `"streamable-http"` — the transport is the same, and `mcp.run()` still defaults to stdio.
Here is a complete server before and after. Nothing in the logic changes:
<CodeGroup>
```python Before
import json
from mcp.server.mcpserver import MCPServer, Context
server = MCPServer("demo")
@server.tool()
def greet(name: str) -> str:
"""Greet someone by name"""
return f"Hello, {name}!"
@server.tool()
async def process(items: list[str], ctx: Context) -> str:
"""Process a batch of items"""
for i, item in enumerate(items):
await ctx.report_progress(i, len(items))
return f"Processed {len(items)} items"
@server.resource("config://app", mime_type="application/json")
def app_config() -> str:
"""Application configuration"""
return json.dumps({"debug": False})
@server.resource("users://{user_id}/profile")
def profile(user_id: str) -> str:
"""User profile by ID"""
return json.dumps({"id": user_id})
@server.prompt()
def summarize(text: str) -> str:
"""Summarize text"""
return f"Summarize:\n\n{text}"
if __name__ == "__main__":
server.run(transport="streamable-http")
```
```python After
import json
from fastmcp import FastMCP, Context
mcp = FastMCP("demo")
@mcp.tool
def greet(name: str) -> str:
"""Greet someone by name"""
return f"Hello, {name}!"
@mcp.tool
async def process(items: list[str], ctx: Context) -> str:
"""Process a batch of items"""
for i, item in enumerate(items):
await ctx.report_progress(i, len(items))
return f"Processed {len(items)} items"
@mcp.resource("config://app", mime_type="application/json")
def app_config() -> str:
"""Application configuration"""
return json.dumps({"debug": False})
@mcp.resource("users://{user_id}/profile")
def profile(user_id: str) -> str:
"""User profile by ID"""
return json.dumps({"id": user_id})
@mcp.prompt
def summarize(text: str) -> str:
"""Summarize text"""
return f"Summarize:\n\n{text}"
if __name__ == "__main__":
mcp.run(transport="http")
```
</CodeGroup>
## Constructor Arguments
`FastMCP()` describes your server's identity and behavior; how it gets deployed is decided when you serve it. Several `MCPServer` constructor arguments move accordingly, and each raises a `TypeError` naming its replacement rather than being silently ignored.
`name`, `version`, `instructions`, `icons`, `website_url`, `lifespan`, `resource_security`, and `request_state_security` all mean what they meant before. The rest map like this:
| `MCPServer(...)` | FastMCP |
|---|---|
| `debug=True` | `FASTMCP_DEBUG` environment variable |
| `log_level="DEBUG"` | `run_http_async(log_level=...)` or `FASTMCP_LOG_LEVEL` |
| `warn_on_duplicate_tools`, `_resources`, `_prompts` | a single `on_duplicate=` |
| `dependencies=[...]` | a [`fastmcp.json`](/deployment/server-configuration) configuration file |
| `title=`, `description=` | `instructions=` |
| `tools=[Tool, ...]` | `tools=[callable, ...]`, or FastMCP's own `Tool` |
| `resources=[Resource, ...]` | no constructor keyword — register with `@mcp.resource` or `mcp.add_resource()` |
| `subscriptions=<SubscriptionBus>` | no equivalent — see below |
| `token_verifier=`, `auth_server_provider=`, `auth=AuthSettings(...)` | a single `auth=` provider |
| `cache_hints={...}` | `cache_ttl=`, `cache_scope=` |
| `extensions=[...]` | `mcp.add_extension(...)` |
| `middleware=[ServerMiddleware, ...]` | `middleware=[Middleware, ...]` — same keyword, different class |
`middleware=` is the row most likely to be mistaken for a rename. Both constructors take a `middleware=` sequence, but an `MCPServer` wants the SDK's `ServerMiddleware` — one hook wrapping every raw JSON-RPC message — while FastMCP wants its own `Middleware`, which adds typed per-operation hooks (`on_call_tool`, `on_list_tools`, and the rest) on top of the same message-level pass. Keeping the keyword and swapping the base class is the migration; see [Middleware](/servers/middleware).
Authentication is the largest of these, and it consolidates rather than moves. `MCPServer` exposes the SDK's raw auth plumbing — a token verifier, an authorization-server provider, and an `AuthSettings` object, configured separately. FastMCP takes one `auth=` provider that carries the whole configuration, and ships providers for the common cases: `JWTVerifier` for validating tokens you already issue, `RemoteAuthProvider` for delegating to an external authorization server, `OAuthProxy` for wrapping a provider that lacks Dynamic Client Registration, and named providers for GitHub, Google, Auth0, Keycloak, WorkOS, and others. See [Authentication](/servers/auth/authentication).
Two rows are worth reading before you delete the argument. `resources=` has no constructor equivalent, so pre-built `Resource` objects need registering through `@mcp.resource` or `mcp.add_resource()` instead — dropping the keyword silently drops the resources with it. And `subscriptions=`, which an `MCPServer` uses to plug in an external pub/sub bus so resource-update notifications reach clients across replicas, has no FastMCP equivalent at all. A multi-replica deployment that relies on it should confirm it can live without cross-replica subscription fan-out before migrating, because a mechanical rename removes that behavior without any error to warn you.
### Serving HTTP
Renaming `streamable_http_app()` to `http_app()` is only mechanical for a call with no arguments. The keywords were renamed and regrouped, so an existing call carries arguments `http_app()` does not accept:
| SDK v2 | FastMCP |
|---|---|
| `streamable_http_app(streamable_http_path=...)` | `http_app(path=...)` |
| `sse_app(sse_path=...)` | `http_app(path=..., transport="sse")` |
| `sse_app(message_path=...)` | no equivalent |
| `transport_security=TransportSecuritySettings(...)` | `host_origin_protection=`, `allowed_hosts=`, `allowed_origins=` |
| `host=...` | pass to `mcp.run(host=...)` instead |
`json_response`, `stateless_http`, `event_store`, and `retry_interval` keep their names. See [Deploying HTTP servers](/deployment/http) for the host and origin settings.
### Stricter Arguments
One behavior change survives the rename and is worth knowing before you migrate. `MCPServer` binds the arguments it recognizes and ignores the rest, so a call carrying an unexpected key succeeds. FastMCP declares `"additionalProperties": false` on every generated schema and enforces it, so the same call fails:
```python test="skip"
# Against MCPServer: succeeds, "extra" ignored.
# Against FastMCP: raises, "extra" is not a parameter of greet().
await client.call_tool("greet", {"name": "World", "extra": "surprise"})
```
For most servers this is an improvement that costs nothing — a caller sending keys your tool never reads was already a bug. It matters if a client in your fleet passes extra metadata alongside real arguments, since those calls start failing the moment you migrate. Accept the extras explicitly as optional parameters if you need to keep them working.
## Asking for Input
This is the one part of the migration that is not a rename, so read it before you start if your tools use resolvers.
`MCPServer` asks the client for things through dependency-injection resolvers. A tool parameter annotated `Annotated[T, Resolve(fn)]` is filled by running `fn` before the tool body, and the resolver can return a request marker — `Elicit[T]` to ask the user, `Sample` to borrow the client's model, `ListRoots` to fetch its roots — which the framework turns into the right wire interaction for whichever protocol era the connection negotiated:
```python
from typing import Annotated
from pydantic import BaseModel
from mcp.server.mcpserver import MCPServer, Resolve, Elicit
server = MCPServer("booking")
class Destination(BaseModel):
destination: str
def ask_destination() -> Elicit[Destination]:
return Elicit("Where would you like to fly?", Destination)
@server.tool()
def book_flight(dest: Annotated[Destination, Resolve(ask_destination)]) -> str:
"""Book a flight"""
return f"Booked to {dest.destination}"
```
FastMCP has no equivalent annotation, and it makes the protocol era explicit instead of hiding it. Which replacement you want depends on which era your clients speak.
On **handshake-era connections** (≤ 2025-11-25), a running tool asks the user directly with `ctx.elicit()`, and the call blocks until the answer arrives. Where the resolver returned a value or aborted the call, `ctx.elicit()` hands you the outcome to branch on, so declining and cancelling become cases your tool answers for itself:
```python
from fastmcp import FastMCP, Context
mcp = FastMCP("booking")
@mcp.tool
async def book_flight(ctx: Context) -> str:
"""Book a flight"""
result = await ctx.elicit("Where would you like to fly?", response_type=str)
if result.action == "accept":
return f"Booked to {result.data}"
return "Booking cancelled"
```
On the **modern protocol** (2026-07-28), server-initiated requests are gone from the wire, so a tool asks by *returning* a description of what it needs. The client answers and calls the tool again with the answer attached, and the tool re-runs from the top. This is the [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol), and it reads the answers off `ctx.input_responses`.
The two are era-gated in both directions: `ctx.elicit()` raises on a modern connection, and a guard result raises on a handshake one. A server that must serve both branches on `ctx.request_context.protocol_version`. See [Elicitation](/servers/elicitation#which-approach-to-use) for both shapes side by side.
Resolvers that return `Sample` or `ListRoots` have no *injected* equivalent — FastMCP has no `ctx.sample()` or `ctx.list_roots()` — but the underlying request survives, so this is a change of shape rather than a loss of capability. On a modern connection both ride the same guard pattern as elicitation: the tool returns an `InputRequiredResult` describing the sampling or roots request, and the client answers on the next call.
Which shape you want differs by capability. For **roots**, the guard route is the natural replacement, since one round buys the whole answer — and taking the paths as ordinary tool arguments is simpler still whenever the caller can supply them. For **generation**, prefer [calling an LLM from your server](/servers/sampling) with your own API key: your tool then behaves identically for every client, including the many that never implemented sampling, and you avoid paying a full request-response cycle per generation step. Reach for the guard route when using the *caller's* model is specifically the point.
One schema detail is easy to miss during the rewrite. A resolved parameter never appears in the tool's input schema — `book_flight` above advertises no arguments at all. When you replace a resolver with an explicit tool argument, the schema the client sees gains a field, which is usually what you want but is a visible change to your tool's contract.
## What You Gain
The migration is worth doing for what sits on the other side of it. FastMCP is a framework rather than a protocol surface, and these are the capabilities that most often motivate the move:
[Server composition](/servers/composition) mounts one server inside another, so a large surface splits into modules that are developed and tested independently. [Middleware](/servers/middleware) runs across every request for logging, rate limiting, error handling, and caching, with hooks at whichever level of specificity you need. [Proxy servers](/servers/providers/proxy) put a FastMCP server in front of any existing MCP server, bridging transports and adding auth to a backend you don't control. The [OpenAPI integration](/integrations/openapi) generates a whole server from an existing API specification. [Tool transformation](/servers/transforms/transforms) rewrites the tools a server exposes — renaming, hiding, and reshaping arguments — without touching the code that defines them.
FastMCP also ships a [client](/clients/client), which `MCPServer` has no counterpart for. It speaks every transport, drives both protocol eras, and connects to a server object in-process — so [testing](/servers/testing) a server means calling its tools in the same Python process, with no subprocess and no network.

View file

@ -1,26 +1,11 @@
---
title: "Welcome to FastMCP"
title: "FastMCP: The Framework for MCP"
sidebarTitle: "Welcome!"
description: The fast, Pythonic way to build MCP servers, clients, and applications.
description: FastMCP is the standard framework for building Model Context Protocol (MCP) servers, clients, and interactive applications.
icon: hand-wave
mode: center
---
{/* <img
src="/assets/brand/f-watercolor-waves-4.png"
alt="'F' logo on a watercolor background"
noZoom
className="rounded-2xl block dark:hidden"
/>
<img
src="/assets/brand/f-watercolor-waves-4-dark.png"
alt="'F' logo on a watercolor background"
noZoom
className="rounded-2xl hidden dark:block"
/>
*/}
<video
autoPlay
muted
@ -38,97 +23,112 @@ mode: center
src="/assets/brand/f-watercolor-waves-4-dark-animated.mp4"
></video>
**FastMCP is a full framework for building [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) applications.** It gives you one coherent API for servers, clients, and interactive apps. Use it to expose Python functions as MCP tools, connect to local or remote MCP servers, and return interactive interfaces directly from your tools. FastMCP manages schema generation, validation, transport, authentication, and protocol compatibility around your application code.
**FastMCP is the standard framework for building MCP applications.** The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) connects LLMs to tools and data. FastMCP gives you everything you need to go from prototype to production — build servers that expose capabilities, connect clients to any MCP service, and give your tools interactive UIs:
A FastMCP server starts with ordinary Python:
```python {1}
from fastmcp import FastMCP
mcp = FastMCP("Demo 🚀")
@mcp.tool
def add(a: int, b: int) -> int:
"""Add two numbers"""
"""Add two numbers."""
return a + b
if __name__ == "__main__":
mcp.run()
```
## Move fast and make things
## Move Fast and Make Things
An effective MCP application needs more than a function registry. Models need accurate schemas, callers need validated results, clients need compatible transports, and production servers need authentication and predictable lifecycle management.
The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) lets you give agents access to your tools and data. But building an effective MCP application is harder than it looks.
FastMCP treats those as framework responsibilities. Declare a Python function and FastMCP derives its schema, validates its inputs and outputs, and exposes it through MCP. Connect a client to a URL and FastMCP handles protocol negotiation, authentication, and connection lifecycle. Your application remains ordinary Python while FastMCP keeps the MCP boundary correct.
FastMCP handles all of it. Declare a tool with a Python function, and the schema, validation, and documentation are generated automatically. Connect to a server with a URL, and transport negotiation, authentication, and protocol lifecycle are managed for you. You focus on your logic, and the MCP part just works: **with FastMCP, best practices are built in.**
**That's why FastMCP is the standard framework for working with MCP.** FastMCP created the high-level Python API incorporated into the official MCP Python SDK in 2024. The actively maintained standalone project is now downloaded more than a million times a day, and some version of FastMCP powers 70% of MCP servers across all languages.
**That's why FastMCP is the standard framework for working with MCP.** FastMCP 1.0 was incorporated into the official MCP Python SDK in 2024. Today, the actively maintained standalone project is downloaded a million times a day, and some version of FastMCP powers 70% of MCP servers across all languages.
## Servers, clients, and apps
FastMCP has three pillars:
FastMCP covers the full MCP application lifecycle through three complementary pillars:
<CardGroup cols={3}>
<Card title="Servers" img="/assets/images/servers-card.png" href="/servers/server">
Expose tools, resources, and prompts to LLMs.
Expose Python functions, data, and instructions as MCP tools, resources, and prompts.
</Card>
<Card title="Apps" img="/assets/images/apps-card.png" href="/apps/overview">
Give your tools interactive UIs rendered directly in the conversation.
Give MCP tools interactive user interfaces rendered directly in the conversation.
</Card>
<Card title="Clients" img="/assets/images/clients-card.png" href="/clients/client">
Connect to any MCP server — local or remote, programmatic or CLI.
Connect to any MCP server through Python, the command line, or another MCP application.
</Card>
</CardGroup>
**[Servers](/servers/server)** wrap your Python functions into MCP-compliant tools, resources, and prompts. **[Clients](/clients/client)** connect to any server with full protocol support. And **[Apps](/apps/overview)** give your tools interactive UIs rendered directly in the conversation.
**[Servers](/servers/server)** turn your application logic into MCP capabilities with generated schemas and validation. **[Clients](/clients/client)** connect to local or remote MCP servers with full protocol support. **[Apps](/apps/overview)** let tools return forms, tables, charts, and other interactive interfaces alongside ordinary MCP results.
Ready to build? Start with the [installation guide](/getting-started/installation) or jump straight to the [quickstart](/getting-started/quickstart).
The three pillars share one model: FastMCP owns the protocol machinery while your code defines what the application does.
**Building in TypeScript?** [FastMCP for TypeScript](https://github.com/PrefectHQ/fastmcp-ts) is the official counterpart, built and maintained by the same team. Its servers, clients, and apps follow the same concepts, so what you learn here carries over.
<CardGroup cols={2}>
<Card title="Install FastMCP" icon="download" href="/getting-started/installation">
Add FastMCP to your project with `uv add fastmcp`, verify the package, and find the right upgrade guide.
</Card>
<Card title="Build your first server" icon="rocket-launch" href="/getting-started/quickstart">
Create a tool, run its server, call it from a client, and add an interactive UI.
</Card>
</CardGroup>
FastMCP is made with 💙 by [Prefect](https://www.prefect.io/).
## Run FastMCP in production with Horizon
<Tip>
**This documentation reflects FastMCP's `main` branch**, so it may describe features that have not reached a stable release. Version badges identify when features were introduced.
</Tip>
FastMCP is the standard way to build MCP servers. **[Prefect Horizon](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_welcome&utm_content=welcome_body)** is the enterprise MCP gateway for running them safely.
## Scale MCP with Horizon
Built by the FastMCP team, Horizon packages the best practices we've learned shipping the world's most popular MCP framework.
FastMCP handles the MCP application layer. **[Prefect Horizon](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_welcome&utm_content=welcome_body)** is the enterprise MCP gateway for scaling servers and tools across teams, with centralized governance over how they are deployed, discovered, secured, and used.
Deploy FastMCP servers from GitHub with branch previews and instant rollback. Create a private registry of every MCP your company uses. Secure access with SSO and tool-level RBAC. Get audit logs, observability, and governance across your MCP stack. Remix approved tools into purpose-built endpoints for teams and agents.
Horizon applies the operational patterns developed while maintaining FastMCP: deploy servers from GitHub with branch previews and instant rollback, organize them in a private registry, protect access with SSO and tool-level RBAC, and observe activity through audit logs and telemetry.
Horizon can also combine approved tools into purpose-built MCP endpoints for different teams and agents, while keeping access policy and governance centralized.
Start with FastMCP. [Scale with Horizon →](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_welcome&utm_content=welcome_cta)
<Tip>
**This documentation reflects FastMCP's `main` branch**, meaning it always reflects the latest development version. Features are generally marked with version badges (e.g. `New in version: 3.0.0`) to indicate when they were introduced. Note that this may include features that are not yet released.
</Tip>
## LLM-friendly docs
## LLM-Friendly Docs
FastMCP documentation is designed for developers and coding agents. Every page is available as Markdown, the complete documentation is published in `llms.txt` formats, and the documentation itself is exposed through an MCP server.
The FastMCP documentation is available in multiple LLM-friendly formats:
### MCP server
### MCP Server
The FastMCP docs are accessible via MCP! The server URL is `https://gofastmcp.com/mcp`.
In fact, you can use FastMCP to search the FastMCP docs:
Point any MCP-compatible agent at `https://gofastmcp.com/mcp` to let it search the documentation as it works. You can also connect with FastMCP's Python client directly:
```python
import asyncio
from fastmcp import Client
async def main():
async def main() -> None:
async with Client("https://gofastmcp.com/mcp") as client:
result = await client.call_tool(
name="search_fast_mcp",
arguments={"query": "deploy a FastMCP server"}
arguments={"query": "deploy a FastMCP server"},
)
print(result)
print(result)
asyncio.run(main())
```
### Text Formats
### Markdown formats
The docs are also available in [llms.txt format](https://llmstxt.org/):
- [llms.txt](https://gofastmcp.com/llms.txt) - A sitemap listing all documentation pages
- [llms-full.txt](https://gofastmcp.com/llms-full.txt) - The entire documentation in one file (may exceed context windows)
The documentation is also available in [`llms.txt`](https://llmstxt.org/) formats:
Any page can be accessed as markdown by appending `.md` to the URL. For example, this page becomes `https://gofastmcp.com/getting-started/welcome.md`.
- [`llms.txt`](https://gofastmcp.com/llms.txt) lists every documentation page.
- [`llms-full.txt`](https://gofastmcp.com/llms-full.txt) contains the complete documentation in one file and may exceed some context windows.
You can also copy any page as markdown by pressing "Cmd+C" (or "Ctrl+C" on Windows) on your keyboard.
Append `.md` to any documentation URL to retrieve that page as Markdown. For example, this page is available at `https://gofastmcp.com/getting-started/welcome.md`. You can also copy the current page as Markdown by pressing `Cmd+C` or `Ctrl+C`.

View file

@ -0,0 +1,236 @@
---
title: "What's New in FastMCP 4"
sidebarTitle: "What's New"
description: FastMCP 4 makes stateful MCP applications work on the sessionless protocol while one server serves every protocol era.
icon: sparkles
---
FastMCP 4 makes stateful MCP applications work on MCP's sessionless protocol. Tools can ask follow-up questions across requests, preserve authenticated user state, and move long-running work into background tasks without sticky sessions or a continuously connected client.
The protocol changed completely underneath those APIs. Your application usually does not: one FastMCP server negotiates both protocol eras per connection, and most FastMCP 3 servers upgrade unchanged.
That is the theme of version 4: stateless transport without stateless application code. The release also makes protocol extensions a first-class surface, adds enterprise identity for agents acting on behalf of users, and strengthens production defaults across caching, routing, and security.
<Note>
FastMCP 4 is in **beta**. Pin an exact version and expect sharp edges. See [Install the v4 prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
</Note>
## Protocol compatibility
A protocol migration usually forces a choice between breaking clients that have not moved yet and holding the server back with them. FastMCP 4 serves both eras from one deployment, negotiating the best mutual version for each connection. Modern clients get the sessionless protocol while handshake-era clients continue working unchanged.
Statelessness changes how that deployment scales. Each modern request carries everything needed to answer it, so any replica behind an ordinary load balancer can serve any request and session affinity stops being a requirement.
The client default follows the same rule. `Client(url)` probes for the modern protocol and falls back to the handshake when necessary. Pin `mode="legacy"` only when your application specifically needs the session back-channel.
```python
from fastmcp import Client
# Negotiate the best mutual protocol
client = Client("https://example.com/mcp")
# Require the handshake-era protocol
legacy = Client("https://example.com/mcp", mode="legacy")
```
Once connected, `client.protocol_version`, `client.server_info`, `client.server_capabilities`, and `client.instructions` expose the same interface whichever era was negotiated. Application code that inspects a server does not need a protocol-version branch. See [Protocol negotiation](/clients/client#protocol-negotiation).
On modern connections, FastMCP also attaches the method, target name, and opted-in argument values as HTTP headers. Gateways and load balancers can route requests without parsing JSON-RPC bodies. See [Gateway routing headers](/deployment/http#gateway-routing-headers).
## Stateful applications
The modern protocol removes transport-level sessions, but applications still need conversations, user state, and long-running work. FastMCP moves those concerns into explicit application primitives that survive fresh connections. Shared stores and request-state keys extend them across replicas and worker restarts.
### Interactive tools
Many useful tools need more than one exchange. A booking tool asks for a destination, then a date, then confirmation. A destructive operation asks the user to approve it before continuing.
On the modern protocol, the tool returns a description of the input it needs. That result completes the request normally. The client fulfils the request and calls the tool again with the answer attached; the tool runs from the top, reads `ctx.input_responses`, and either asks another question or returns its final result.
Each request completes while the user responds. Single-process servers use an automatic process-local key to protect the state carried between rounds; load-balanced deployments configure one shared key so any replica can validate and resume the next round:
```python
import os
from fastmcp import Context, FastMCP
from mcp.server.request_state import RequestStateSecurity
from mcp.types import ElicitRequest, ElicitRequestFormParams, InputRequiredResult
mcp = FastMCP(
"Booking",
request_state_security=RequestStateSecurity(
keys=[os.environ["REQUEST_STATE_KEY"].encode()]
),
)
@mcp.tool
async def book_flight(ctx: Context) -> str | InputRequiredResult:
answers = ctx.input_responses
if answers is None:
params = ElicitRequestFormParams(
message="Where would you like to fly?",
requested_schema={
"type": "object",
"properties": {"destination": {"type": "string"}},
"required": ["destination"],
},
)
return InputRequiredResult(
result_type="input_required",
input_requests={
"destination": ElicitRequest(
method="elicitation/create",
params=params,
)
},
)
response = answers["destination"]
if response.action != "accept" or response.content is None:
return "Booking cancelled."
destination = response.content["destination"]
return f"Booked a flight to {destination}."
```
Every replica must receive the same `REQUEST_STATE_KEY`, containing at least 32 bytes of secret key material. A FastMCP client drives the loop through its existing elicitation handler, so client code receives the terminal result without managing the intermediate rounds. See [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol).
### Session state
Application state follows the same explicit model. FastMCP stores state server-side and binds it to the authenticated user, so a session handle is inert in another user's hands.
Most tools want one state bucket per user. Declare a `UserSession` parameter and FastMCP injects it like `Context`: it never appears in the tool schema, and the caller passes nothing because their authenticated identity selects the bucket.
```python
from fastmcp import FastMCP
from fastmcp.server.sessions import UserSession
mcp = FastMCP("Assistant")
@mcp.tool
async def remember(fact: str, session: UserSession) -> str:
facts = await session.get("facts", default=[])
facts.append(fact)
await session.set("facts", facts)
return f"Remembered {len(facts)} facts."
```
`UserSession` requires [authentication](/servers/auth/authentication), since an unauthenticated request has no user to key on. When one user needs several independent buckets, such as separate carts or conversations, `SessionId` exposes the handle as an explicit string argument.
The default in-memory state store is process-local. To preserve state across restarts or share it among replicas, pass a shared persistent `session_state_store`. See [Session state](/servers/sessions).
### Background work
Long-running tools create a different kind of state problem: holding a request open for several minutes invites timeouts and leaves the user unable to tell whether work is progressing. Background tasks accept the call and return a handle immediately, then let the client poll while work proceeds asynchronously.
FastMCP implements the `io.modelcontextprotocol/tasks` extension in the optional `fastmcp-tasks` package. The authoring API remains `@mcp.tool(task=True)`, backed by [Docket](https://github.com/chrisguidry/docket):
```python
import asyncio
from fastmcp import FastMCP
from fastmcp_tasks import TasksExtension
mcp = FastMCP("MyServer")
mcp.add_extension(TasksExtension())
@mcp.tool(task=True)
async def slow_computation(duration: int) -> str:
"""Run a long computation."""
await asyncio.sleep(duration)
return f"Completed in {duration} seconds"
```
`fastmcp.Client` handles the task handle and polling cycle, so `client.call_tool(...)` returns the same way whether the tool ran inline or in the background. See [Background tasks](/servers/tasks).
`TasksExtension()` uses an in-memory, single-process backend by default. Configure a Redis or Valkey backend for durable work that survives restarts and runs across separate workers.
## Extensible protocol
Background tasks are built on a general extension surface. An MCP extension advertises a capability under a reverse-DNS identifier and can add behavior negotiated between a server and client.
### Server extensions
`FastMCP.add_extension()` lets an extension advertise capabilities, add request methods, intercept `tools/call`, and own lifespan behavior with access to the component registry, `Context`, and authentication. Client extensions use the matching `Client(extensions=...)` interface.
Cross-cutting protocol behavior can therefore live in a supported plugin instead of requiring changes to FastMCP core. `TasksExtension` is a complete example of the interface. See [Server extensions](/servers/extensions).
### Argument completion
FastMCP 4 also lets servers answer MCP argument-completion requests. A completion handler sees the prompt or resource-template argument, its partial value, and values already supplied, so suggestions can depend on earlier choices.
```python
from fastmcp import FastMCP
from mcp.types import PromptReference
mcp = FastMCP("Docs")
@mcp.prompt
def write_poem(theme: str) -> str:
return f"Write a poem about {theme}"
@mcp.completion
def complete(ref, argument, context):
if isinstance(ref, PromptReference) and argument.name == "theme":
options = ["nature", "love", "adventure"]
return [option for option in options if option.startswith(argument.value)]
return None
```
Registering the handler advertises the completion capability during negotiation, so clients only send requests to servers that support them. See [Argument completion](/servers/completions).
## Enterprise identity
Interactive OAuth authorization assumes a person can complete a browser flow. Internal agents often act for employees without a person waiting at a keyboard, while the server still needs the employee's identity for authorization and audit.
Identity assertion carries that identity through the agent. A corporate identity provider signs an assertion, the agent presents it, and the server exchanges it for a short-lived token without an interactive login or consent screen. FastMCP performs signature verification, binding checks, replay rejection, and scoped token issuance through the authentication providers you already use.
```python
from fastmcp import FastMCP
from fastmcp.server.auth import IdentityAssertion, OAuthProxy
auth = OAuthProxy(
# Existing upstream configuration
identity_assertion=IdentityAssertion(
trusted_issuers=["https://login.acme-corp.com"]
),
)
mcp = FastMCP("Internal API", auth=auth)
```
The asserted subject enters the normal authentication context, so tools read it through `get_access_token()` like any other identity. See [Identity assertion](/servers/auth/oauth-proxy#identity-assertion-sep-990).
Authorization gained a provider-neutral role check as well. `require_roles` accepts an extraction function for providers that store roles and groups under different claims, while [scope step-up challenges](/servers/authorization#signaling-scope-shortfalls) tell a client exactly which scopes to request.
For clients with no user behind them, such as backend services and scheduled jobs, `ClientCredentialsOAuthProvider` implements the OAuth 2.0 client-credentials grant with no browser or redirect. See [Machine-to-machine authentication](/clients/auth/client-credentials).
## Production defaults
A server can now attach freshness hints to its results, and a caching client can reuse those results without another round trip. Set a default time-to-live and scope on the server:
```python
from fastmcp import FastMCP
mcp = FastMCP("Weather", cache_ttl=300, cache_scope="public")
```
`KeyValueResponseCacheStore` can place the client cache in Redis or another key-value store so a fleet of clients or proxies shares fills. See [Response caching](/clients/client#response-caching).
Resource templates now reject path traversal, absolute paths, and null bytes in their parameters before the handler runs. The protection is enabled by default and applies to mounted and proxied templates. See [Path security](/servers/resources#path-security).
OAuth defaults also distinguish native clients from web applications during Dynamic Client Registration, and missing scopes now produce an `InsufficientScopeError` that names the scopes required to continue. See [Application type](/servers/auth/oauth-proxy#application-type-web-vs-native) and [scope shortfalls](/servers/authorization#signaling-scope-shortfalls).
## Upgrade note
The sessionless protocol has no live connection for a server to call back into during execution. FastMCP 4 therefore removes `ctx.sample()`, `ctx.sample_step()`, and `ctx.list_roots()` from every protocol era so incompatible code fails immediately during an upgrade.
For generation, call an LLM directly from the server when your application owns the model. When borrowing the caller's model is the point, return an `InputRequiredResult` carrying a sampling request and read the answer on the next round. Roots use the same return-and-resume pattern. See [Sampling](/servers/sampling) and [the guard pattern](/servers/elicitation#sampling-and-roots).
`ctx.elicit()` remains available on handshake-era connections; modern connections use the multi-round pattern described above. Code that constructs MCP protocol models directly must also use snake_case Python field names with SDK v2.
[Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) covers these changes and every other compatibility break.

View file

@ -69,9 +69,11 @@ You'll also need to authenticate with Anthropic. You can do this by setting the
export ANTHROPIC_API_KEY="your-api-key"
```
Here is an example of how to call your server from Python. Note that you'll need to replace `https://your-server-url.com` with the actual URL of your server. In addition, we use `/mcp/` as the endpoint because we deployed a streamable-HTTP server with the default path; you may need to use a different endpoint if you customized your server's deployment. **At this time you must also include the `extra_headers` parameter with the `anthropic-beta` header.**
Here is an example of how to call your server from Python. Note that you'll need to replace `https://your-server-url.com` with the actual URL of your server. In addition, we use `/mcp/` as the endpoint because we deployed a streamable-HTTP server with the default path; you may need to use a different endpoint if you customized your server's deployment.
```python {5, 13-22}
The connector is in beta, so the call goes through `client.beta.messages` with the `mcp-client-2025-11-20` flag. Each entry in `mcp_servers` also needs a matching `mcp_toolset` entry in `tools` that references it by name; declaring the server without the toolset is rejected as a validation error.
```python {5, 14-23}
import anthropic
from rich import print
@ -81,8 +83,9 @@ url = 'https://your-server-url.com'
client = anthropic.Anthropic()
response = client.beta.messages.create(
model="claude-sonnet-4-20250514",
model="claude-sonnet-5",
max_tokens=1000,
betas=["mcp-client-2025-11-20"],
messages=[{"role": "user", "content": "Roll a few dice!"}],
mcp_servers=[
{
@ -91,9 +94,7 @@ response = client.beta.messages.create(
"name": "dice-server",
}
],
extra_headers={
"anthropic-beta": "mcp-client-2025-04-04"
}
tools=[{"type": "mcp_toolset", "mcp_server_name": "dice-server"}],
)
print(response.content)
@ -193,7 +194,7 @@ Error code: 400 - {
To authenticate the client, you can pass the token using the `authorization_token` parameter in your MCP server configuration:
```python {8, 21}
```python {8, 22}
import anthropic
from rich import print
@ -206,8 +207,9 @@ access_token = 'your-access-token'
client = anthropic.Anthropic()
response = client.beta.messages.create(
model="claude-sonnet-4-20250514",
model="claude-sonnet-5",
max_tokens=1000,
betas=["mcp-client-2025-11-20"],
messages=[{"role": "user", "content": "Roll a few dice!"}],
mcp_servers=[
{
@ -217,9 +219,7 @@ response = client.beta.messages.create(
"authorization_token": access_token
}
],
extra_headers={
"anthropic-beta": "mcp-client-2025-04-04"
}
tools=[{"type": "mcp_toolset", "mcp_server_name": "dice-server"}],
)
print(response.content)

View file

@ -16,7 +16,7 @@ FastMCP supports two Auth0 integration paths:
## Auth for MCP (DCR)
<VersionBadge version="3.3.0" />
<VersionBadge version="4.0.0" />
This path uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern. Auth0 acts as the authorization server; FastMCP is the resource server.

View file

@ -81,7 +81,8 @@ auth = OAuth(additional_client_metadata={"token_endpoint_auth_method": "none"})
async def main():
async with Client("http://127.0.0.1:8000/mcp", auth=auth) as client:
assert await client.ping()
tools = await client.list_tools()
print(f"Authenticated. Server exposes {len(tools)} tools.")
if __name__ == "__main__":
asyncio.run(main())

View file

@ -95,7 +95,7 @@ The connector must be explicitly enabled in each chat session through Developer
Use `annotations=ToolAnnotations(readOnlyHint=True)` to skip confirmation prompts for read-only tools:
```python
from fastmcp.types import ToolAnnotations
from mcp.types import ToolAnnotations
@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
def get_status() -> str:

View file

@ -83,6 +83,8 @@ mcp = FastMCP(name="My Descope Protected Server", auth=auth_provider)
### Scope discovery and validation
<VersionBadge version="4.0.0" />
When both `scopes_supported` and `required_scopes` are omitted, `DescopeProvider` discovers `scopes_supported` lazily from the OpenID configuration and advertises them to MCP clients. Provider construction remains network-free, and a transient discovery failure is retried on a later metadata request.
Set both options when clients should request a broader set of scopes than the server requires on every token:
@ -116,7 +118,8 @@ import asyncio
async def main():
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
assert await client.ping()
tools = await client.list_tools()
print(f"Authenticated. Server exposes {len(tools)} tools.")
if __name__ == "__main__":
asyncio.run(main())

View file

@ -69,7 +69,7 @@ from fastmcp.server.auth.providers.github import GitHubProvider
# The GitHubProvider handles GitHub's token format and validation
auth_provider = GitHubProvider(
client_id="Ov23liAbcDefGhiJkLmN", # Your GitHub OAuth App Client ID
client_secret="github_pat_...", # Your GitHub OAuth App Client Secret
client_secret="your-github-client-secret", # Your GitHub OAuth App Client Secret
base_url="http://localhost:8000", # Must match your OAuth App configuration
# redirect_path="/auth/callback" # Default value, customize if needed
)
@ -151,7 +151,7 @@ from cryptography.fernet import Fernet
# Production setup with encrypted persistent token storage
auth_provider = GitHubProvider(
client_id="Ov23liAbcDefGhiJkLmN",
client_secret="github_pat_...",
client_secret="your-github-client-secret",
base_url="https://your-production-domain.com",
# Production token management

View file

@ -70,7 +70,7 @@ An object containing environment variables to set when launching the server. All
This format is widely adopted across the MCP ecosystem:
- **Claude Desktop**: Uses `~/.claude/claude_desktop_config.json`
- **Claude Desktop**: Uses `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows
- **Cursor**: Uses `~/.cursor/mcp.json`
- **VS Code**: Uses workspace `.vscode/mcp.json`
- **Other clients**: Many MCP-compatible applications follow this standard
@ -457,7 +457,7 @@ The generated configuration works with any MCP-compatible application:
<Note>
**Prefer [`fastmcp install claude-desktop`](/integrations/claude-desktop)** for automatic installation. Use MCP JSON for advanced configuration needs.
</Note>
Copy the `mcpServers` object into `~/.claude/claude_desktop_config.json`
Copy the `mcpServers` object into Claude Desktop's config file (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows)
### Cursor
<Note>

View file

@ -452,4 +452,21 @@ FastMCP handles array parameters according to OpenAPI specifications:
### Headers
Header parameters are automatically converted to strings and included in the HTTP request.
Header parameters are automatically converted to strings and included in the HTTP request.
### Composed Request Bodies
A request body becomes a flat set of tool arguments, which is the shape LLM tool-calling APIs fill in most reliably. Schemas composed with `allOf` are resolved first, following `$ref` members, so fields inherited from a parent schema appear alongside the ones a schema declares itself.
Schemas that use a `discriminator` are flattened the same way. FastMCP merges in the fields of every subtype named in the discriminator's `mapping`, marks them optional, and names the accepted values on the discriminator's own description. Given a `Pet` body discriminated by `petType` and mapped onto `Cat` and `Dog`, the tool takes the discriminator plus whichever fields that variant uses:
```python
await client.call_tool("create_pet", {
"petType": "cat",
"meowVolume": 11,
})
```
The discriminator stays required; every variant field is optional, because only one variant applies to any given call.
This trades local strictness for a schema models complete accurately. The generated schema permits any combination of variant fields, so sending `packSize` with `petType: "cat"` passes FastMCP's validation and is rejected by the API itself, exactly as it would be for any other HTTP client. Where two variants declare the same field differently, the declarations are combined with `anyOf` so that neither variant's constraints are advertised as applying to both.

View file

@ -300,10 +300,12 @@ For advanced configuration options and custom middleware extensions, see [Advanc
See the [example server](https://github.com/permitio/permit-fastmcp/blob/main/permit_fastmcp/example_server/example.py) for a full implementation with JWT-based authentication. For additional examples and usage patterns, see [Example Server](https://github.com/permitio/permit-fastmcp/blob/main/permit_fastmcp/example_server/):
```python
import os
import datetime
import jwt
from fastmcp import FastMCP, Context
from permit_fastmcp.middleware.middleware import PermitMcpMiddleware
import jwt
import datetime
# Configure JWT identity extraction
os.environ["PERMIT_MCP_IDENTITY_MODE"] = "jwt"

View file

@ -101,7 +101,8 @@ import asyncio
async def main():
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
assert await client.ping()
tools = await client.list_tools()
print(f"Authenticated. Server exposes {len(tools)} tools.")
if __name__ == "__main__":
asyncio.run(main())

77
docs/language-dropdown.js Normal file
View file

@ -0,0 +1,77 @@
// Language dropdown: a small Python/TypeScript switcher injected into the
// sidebar footer, next to Mintlify's theme selector. Selecting the other
// language navigates to that project's docs site; selecting the current
// language is a no-op. Styling lives in css/language-dropdown.css.
(function () {
if (typeof window === "undefined") return;
var CURRENT_LANGUAGE = "python";
// TODO: fastmcp-ts has no public docs site URL discoverable in either repo
// yet. Until it exists, point at the repo README (the same cross-link the
// welcome page uses), then replace with the real docs URL.
var TYPESCRIPT_DOCS_URL = "https://github.com/PrefectHQ/fastmcp-ts";
var PYTHON_DOCS_URL = "https://gofastmcp.com";
var URLS = { python: PYTHON_DOCS_URL, typescript: TYPESCRIPT_DOCS_URL };
function findThemeSelector() {
// Mintlify's sidebar-footer DOM is not a stable public API, so probe a
// few markers (almond theme first) and give up quietly if none match.
return (
document.querySelector("[data-theme-preference-switch]") ||
document.querySelector('[role="group"][aria-label="Theme preference"]')
);
}
function buildDropdown() {
var label = document.createElement("label");
label.id = "language-switch";
var select = document.createElement("select");
select.setAttribute("aria-label", "Switch documentation language");
[
["python", "Python"],
["typescript", "TypeScript"],
].forEach(function (entry) {
var option = document.createElement("option");
option.value = entry[0];
option.textContent = entry[1];
if (entry[0] === CURRENT_LANGUAGE) option.selected = true;
select.appendChild(option);
});
select.addEventListener("change", function () {
if (select.value === CURRENT_LANGUAGE) return;
window.location.href = URLS[select.value];
});
label.appendChild(select);
return label;
}
function addDropdown() {
if (document.getElementById("language-switch")) return;
var theme = findThemeSelector();
if (!theme || !theme.parentElement) return;
// Insert after the theme pill; margin-left:auto floats it right.
theme.parentElement.insertBefore(buildDropdown(), theme.nextSibling);
}
function run() {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", addDropdown);
} else {
addDropdown();
}
}
run();
// Mintlify re-renders the sidebar on client-side navigation; re-inject when
// the dropdown disappears.
new MutationObserver(function () {
if (!document.getElementById("language-switch")) addDropdown();
}).observe(document.body, { subtree: true, childList: true });
})();

View file

@ -1,9 +1,124 @@
---
title: FAQ
description: Answers to common questions about installing and using FastMCP
description: Direct answers to the questions that come up most often about FastMCP 4, the protocol eras, and installation
icon: circle-question
---
## Do I need to change my server code for FastMCP 4?
Most servers run untouched. The defining change in FastMCP 4 is its engine — the MCP Python SDK v2 — and FastMCP absorbs nearly all of it for you, including the wire-wide rename from camelCase to snake_case, which is bridged so your existing reads keep working.
Most of what does reach your code fails loudly at import or call time, and the fix is mechanical: `McpError(ErrorData(...))` becomes `McpError(code=..., message=...)`, custom `httpx` clients handed to a transport become `httpx2`, and `ctx.sample()` and `ctx.list_roots()` are gone.
One change is silent, so go looking for it: an `except httpx.ConnectError:` around a FastMCP call still imports and still type-checks, because `httpx` usually remains installed through some other dependency — but FastMCP now raises the `httpx2` exception, so the handler simply stops matching and your fallback quietly never runs. Grep for `except httpx.` and move those to `httpx2`. [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) covers each one and ends with a checklist.
## Why does my client connect with a different protocol version than before?
`fastmcp.Client` defaults to `mode="auto"` as of FastMCP 4, so it negotiates the newest era both sides speak rather than pinning the handshake. Over streamable HTTP or stdio to a FastMCP server that means the sessionless `2026-07-28` protocol, where FastMCP 3 connected at `2025-11-25`.
Two transports are exceptions: SSE predates the sessionless era and cannot carry it, and a multi-server `MCPConfigTransport` mounts each backend behind a legacy-era composite. Under `mode="auto"` the client recognizes both and settles on the handshake without probing, so seeing `2025-11-25` there is correct rather than a negotiation failure. Pinning a modern version explicitly on either skips that substitution and asks the transport for something it cannot serve, so leave them on auto or legacy.
The client probes `server/discover` and adopts the modern protocol when the server answers, falling back to the `initialize` handshake for anything that is not positive evidence of a modern peer — so a mixed fleet of servers still connects. Pin the old behavior per client with `Client(url, mode="legacy")`. See [Protocol negotiation](/clients/client#protocol-negotiation).
## What are the two protocol eras, and which one does my server speak?
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.
You do not need to pin it just because you registered a `sampling_handler`, `roots=`, or an `elicitation_handler`. None of the three require the handshake on their own: `mode="auto"` reaches whichever era the connection negotiates, and on a modern connection a tool can still exercise any of them through the guard pattern — it manually returns an `InputRequiredResult` embedding the request, and the same handler you already registered answers it. [Roots](/clients/roots) and [elicitation](/clients/elicitation) document this pattern directly; [sampling](/clients/sampling) works through the identical mechanism, though calling an LLM directly from the server is the recommended path there rather than a round trip for it.
Pinning is per client, not a deployment setting: `Client(url, mode="legacy")`. The trade runs the other way as well — [background tasks](/clients/tasks) are modern-only, so a legacy client never triggers one and a task-enabled tool simply runs synchronously.
## Why did my `ctx.sample()` code stop working?
`ctx.sample()` and `ctx.sample_step()` are not part of FastMCP 4. Calling either raises `AttributeError` on every protocol era, and `FastMCP(sampling_handler=...)` raises `TypeError` naming the migration.
Sampling was a server-to-client *request*: the server sent `sampling/createMessage` and blocked until an answer came back down the session. The modern protocol has no server-to-client request direction at all, so the pushed form has nowhere to go.
The asking survives in a different shape. A tool can return an `InputRequiredResult` carrying a `CreateMessageRequest`; the client answers it through the same `sampling_handler` it already registers, and your tool runs again with the completion. Reach for that when using *the caller's* model is the point. Otherwise put generation in your server — hold a provider API key and call the model directly, which has the side benefit that your tool behaves identically for every client, including the many that never implemented sampling. [Sampling](/servers/sampling) shows both.
## What happened to `ctx.list_roots()`?
Removed, for the same reason as sampling: `roots/list` was a server-to-client request, and the modern protocol has no channel to send one.
Take the paths you need as ordinary tool arguments. The agent already knows which directory it is working in, and an explicit argument is visible in the tool's schema instead of hidden in a protocol round-trip. When the caller genuinely has to be asked mid-run, the [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol) carries a roots request in its `input_requests` map alongside elicitation, and `fastmcp.Client` answers it from the `roots=` you already configured.
## Why does `ctx.info()` still work when sampling doesn't?
Because logging is a *notification* and sampling was a *request*. A notification is fire-and-forget: your server emits it down the response stream the caller already opened, and nothing has to be held open on the server's behalf. A request needs an answer to come back the other way, which requires a live connection the server can reach into.
The modern protocol kept every server notification — `notifications/message`, `notifications/progress`, and the list-changed family — and removed the server-to-client request direction entirely. So `ctx.info()`, `ctx.debug()`, and `ctx.report_progress()` reach the client mid-call on every era, while sampling and roots have no era-agnostic form and were dropped. [Sampling](/servers/sampling#the-removed-methods) works through the distinction in full.
You may see an `MCPDeprecationWarning` from the SDK about the logging capability being deprecated as of `2026-07-28`. It refers to the capability declaration, not to the notification, and delivery is unaffected.
## Why can't I call `client.set_logging_level()` anymore?
On a modern connection it raises, because `logging/setLevel` is not in the `2026-07-28` protocol. The method asked the server to remember a level for the rest of the session, and a sessionless protocol has nowhere to keep that.
The messages themselves are unaffected — the server still sends whatever its own configuration allows. Filter on the receiving side in your `log_handler`, which sees each message's `level` field. See [Client Logging](/clients/logging). On a handshake-era connection (`Client(url, mode="legacy")`) the call works as before.
Receiving-side filtering only narrows what already arrives. A server that sets `FastMCP(client_log_level="error")` drops anything below that threshold before it reaches the wire, and a modern client has no way to ask for the missing levels — the server operator has to lower `client_log_level` for them to be sent at all.
## What replaces elicitation on the modern protocol?
The guard pattern. Rather than pausing mid-execution to ask, a tool *returns* an `InputRequiredResult` describing what it needs. That round completes normally, the client collects the answer, and it calls the tool again with the answer attached. Any state you carry between rounds is sealed by the framework before it reaches the wire, so the client holds an opaque token it cannot read or forge.
`ctx.elicit()` still works on handshake-era connections and raises on modern ones, so a server that must serve both eras needs both paths. `fastmcp.Client` drives whichever the connection negotiated with no extra wiring on your side. See [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol).
## Why doesn't my middleware's `on_initialize` hook run?
Because the modern protocol has no `initialize` request. The hook fires on handshake-era connections and never on modern ones, and since `Client` now defaults to `mode="auto"`, that is the common case against a FastMCP 4 server.
Work that must happen once per process belongs in the server [lifespan](/servers/lifespan). Per-request work such as an auth check belongs in `on_request` or a specific operation hook, both of which run on every era — on a modern connection `on_request` sees `server/discover` where a handshake connection sees `initialize`. See [Middleware](/servers/middleware).
## Why doesn't state I set in one tool call show up in the next?
On a modern connection every request is a fresh connection, so `ctx.set_state` lives only for the duration of the call that wrote it. The same code persists state across calls on a handshake-era connection, which is why it appears to break the moment a client negotiates `2026-07-28`.
[Session state](/servers/sessions) is the durable answer, following MCP's own decision to move session semantics up into the application. Declare a `UserSession` parameter and FastMCP injects one bucket of stored state keyed to the authenticated user, with nothing to pass around. Declare a `SessionId` argument when a single user needs several independent sessions, and the caller mints an id with `create_session` and supplies it on each call — register `mcp.add_provider(SessionProvider())` first, since `create_session` doesn't exist until a `SessionProvider` contributes it. Both store server-side and key to the authenticated caller's identity, so a handle is inert in anyone else's hands.
That isolation comes from authentication, not from the id. On an unauthenticated server there is no principal to key on, so every session shares one anonymous namespace and a `SessionId` becomes a bearer capability — anyone holding it can read and write that state. Treat unauthenticated sessions as single-tenant or trusted-network only; `UserSession` sidesteps the question by requiring an authenticated principal outright.
## How do I run background tasks now?
The same way, plus one registration. `@mcp.tool(task=True)` is still the authoring surface and [Docket](https://github.com/chrisguidry/docket) still runs the work. What changed is underneath: tasks left the core MCP spec and returned as the `io.modelcontextprotocol/tasks` extension (SEP-2663), which FastMCP implements in the optional `fastmcp-tasks` package.
Install `fastmcp[tasks]` and register the extension on your server. A `task=True` tool on a server with no tasks extension refuses to start and names the fix, so a missing registration is impossible to ship by accident.
```python
from fastmcp import FastMCP
from fastmcp_tasks import TasksExtension
mcp = FastMCP("MyServer")
mcp.add_extension(TasksExtension())
@mcp.tool(task=True)
async def slow_computation(duration: int) -> str:
return "done"
```
Tasks are modern-only: the capability is negotiated over `2026-07-28`, so a `mode="legacy"` client never triggers one. A tool marked `task=True` (equivalently `mode="optional"`) then just runs synchronously. A tool that sets `TaskConfig(mode="required")` has no synchronous form to fall back to, so the call fails with a missing-required-capability error instead. See [Background Tasks](/servers/tasks).
## `import fastmcp` stopped working after I upgraded with pip
This can happen when you upgrade to FastMCP 3.3 or later from FastMCP 3.2 or earlier with `pip`. The quick fix is `pip install --force-reinstall fastmcp`. See [Troubleshooting](/getting-started/installation#troubleshooting) for the clean-reinstall fallback and an explanation of why it happens.

View file

@ -4,7 +4,7 @@ description: Configure FastMCP behavior through environment variables or a .env
icon: gear
---
FastMCP uses [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) for configuration. Every setting is available as an environment variable with a `FASTMCP_` prefix. Settings are loaded from environment variables and from a `.env` file (see the [Tasks (Docket)](#tasks-docket) section for a caveat about nested settings in `.env` files).
FastMCP uses [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) for configuration. Every setting is available as an environment variable with a `FASTMCP_` prefix. Settings are loaded from environment variables and from a `.env` file.
```bash
# Set via environment
@ -23,7 +23,7 @@ You can change which `.env` file is loaded by setting the `FASTMCP_ENV_FILE` env
|---|---|---|---|
| `FASTMCP_LOG_LEVEL` | `Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]` | `INFO` | Log level for FastMCP's own logging output. Case-insensitive. |
| `FASTMCP_LOG_ENABLED` | `bool` | `true` | Enable or disable FastMCP logging entirely. |
| `FASTMCP_CLIENT_LOG_LEVEL` | `Literal["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"]` | None | Default minimum log level for messages sent to MCP clients via `context.log()`. When set, messages below this level are suppressed. Individual clients can override this per-session using the MCP `logging/setLevel` request. |
| `FASTMCP_CLIENT_LOG_LEVEL` | `Literal["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"]` | None | Default minimum log level for messages sent to MCP clients via `context.log()`. When set, messages below this level are suppressed. Handshake-era clients can override this per-session using the MCP `logging/setLevel` request; the modern protocol has no session to hold that level, so clients on it filter by level in their own log handler instead. |
| `FASTMCP_ENABLE_RICH_LOGGING` | `bool` | `true` | Use rich formatting for log output. Set to `false` for plain Python logging. |
| `FASTMCP_ENABLE_RICH_TRACEBACKS` | `bool` | `true` | Use rich tracebacks for errors. |
| `FASTMCP_DEPRECATION_WARNINGS` | `bool` | `true` | Show deprecation warnings. |
@ -63,7 +63,7 @@ These control how the server listens when running with an HTTP transport.
|---|---|---|---|
| `FASTMCP_CLIENT_INIT_TIMEOUT` | `float \| None` | None | Timeout in seconds for the client initialization handshake. Set to `0` or leave unset to disable. |
| `FASTMCP_CLIENT_DISCONNECT_TIMEOUT` | `float` | `5` | Maximum time in seconds to wait for a clean disconnect before giving up. |
| `FASTMCP_CLIENT_TASK_POLL_INTERVAL` | `float` | `0.5` | Ceiling in seconds for the fallback poll backoff while waiting on a [background task](/servers/tasks). Applies **only** when the server does not advertise its own `pollInterval`: in that case `Task.wait()` starts polling fast (~20ms) and doubles up to this ceiling rather than polling at a fixed cadence. When the server advertises a `pollInterval`, that interval is honored exactly and this setting is ignored. |
| `FASTMCP_TASKS_CLIENT_POLL_INTERVAL` | `float` | `0.5` | Ceiling in seconds for the fallback poll backoff while waiting on a [background task](/servers/tasks). Requires the `fastmcp-tasks` package. Applies **only** when the server does not advertise its own `pollInterval`: in that case `Task.wait()` starts polling fast (~20ms) and doubles up to this ceiling rather than polling at a fixed cadence. When the server advertises a `pollInterval`, that interval is honored exactly and this setting is ignored. |
| `FASTMCP_CLIENT_RAISE_FIRST_EXCEPTIONGROUP_ERROR` | `bool` | `true` | When an `ExceptionGroup` is raised, re-raise the first error directly instead of the group. Simplifies debugging but may mask secondary errors. |
## CLI & Display
@ -77,25 +77,11 @@ These control how the server listens when running with an HTTP transport.
| Environment Variable | Type | Default | Description |
|---|---|---|---|
| `FASTMCP_ENABLE_TELEMETRY` | `bool` | `true` | Whether FastMCP's native [OpenTelemetry instrumentation](/servers/telemetry) is active. Enabled by default; FastMCP uses only the OpenTelemetry API, so span creation is a no-op with negligible overhead unless an OpenTelemetry SDK and exporter are configured. Set to `false` to turn instrumentation off entirely, in which case no FastMCP spans are created even when an SDK is configured. |
| `FASTMCP_TELEMETRY_MODE` | `Literal["native", "propagation_only", "off"]` | `native` | Controls FastMCP's native [OpenTelemetry instrumentation](/servers/telemetry). `native` emits FastMCP's MCP spans and propagates trace context; because FastMCP uses only the OpenTelemetry API, this costs almost nothing unless an SDK and exporter are configured. `propagation_only` keeps `_meta` trace propagation and still parents downstream spans from the incoming context, but emits none of FastMCP's own spans, so another instrumentation layer can own the MCP span hierarchy. `off` is a full pass-through: no spans, and no trace context extracted or attached. |
## Tasks (Docket)
These configure the [Docket](https://github.com/prefecthq/docket) task queue used by [server tasks](/servers/tasks). All use the `FASTMCP_DOCKET_` prefix.
<Warning>
When setting Docket values in a `.env` file, use a **double** underscore: `FASTMCP_DOCKET__URL` (not `FASTMCP_DOCKET_URL`). This is because `.env` values are resolved through the parent `Settings` class, which uses `__` as its nested delimiter. As regular environment variables (e.g., `export`), the single-underscore form `FASTMCP_DOCKET_URL` works fine.
</Warning>
| Environment Variable | Type | Default | Description |
|---|---|---|---|
| `FASTMCP_DOCKET_NAME` | `str` | `fastmcp` | Queue name. Servers and workers sharing the same name and backend URL share a task queue. |
| `FASTMCP_DOCKET_URL` | `str` | `memory://` | Backend URL. Use `memory://` for single-process or `redis://host:port/db` for distributed workers. |
| `FASTMCP_DOCKET_WORKER_NAME` | `str \| None` | None | Worker name. Auto-generated if unset. |
| `FASTMCP_DOCKET_CONCURRENCY` | `int` | `10` | Maximum concurrent tasks per worker. |
| `FASTMCP_DOCKET_REDELIVERY_TIMEOUT` | `timedelta` | `300s` | If a worker doesn't complete a task within this time, it's redelivered to another worker. |
| `FASTMCP_DOCKET_RECONNECTION_DELAY` | `timedelta` | `5s` | Delay between reconnection attempts when the worker loses its backend connection. |
| `FASTMCP_DOCKET_MINIMUM_CHECK_INTERVAL` | `timedelta` | `50ms` | How frequently the worker polls for new tasks. Lower values reduce latency at the cost of more CPU usage. |
Task settings (the `FASTMCP_DOCKET_` and `FASTMCP_TASKS_` variables) live in the optional `fastmcp-tasks` package. See [server tasks](/servers/tasks) for configuration, including `FASTMCP_TASKS_ENCRYPTION_KEY` for [encrypting task snapshots at rest](/servers/tasks#credentials-at-rest).
## Security

View file

@ -31,10 +31,33 @@
}
]
},
{
"group": "fastmcp.server",
"pages": [
"python-sdk/fastmcp-server-caching",
"python-sdk/fastmcp-server-completions",
"python-sdk/fastmcp-server-context",
"python-sdk/fastmcp-server-dependencies",
"python-sdk/fastmcp-server-elicitation",
"python-sdk/fastmcp-server-event_store",
"python-sdk/fastmcp-server-extensions",
"python-sdk/fastmcp-server-http",
"python-sdk/fastmcp-server-lifespan",
"python-sdk/fastmcp-server-low_level",
"python-sdk/fastmcp-server-mixins",
"python-sdk/fastmcp-server-providers",
"python-sdk/fastmcp-server-server",
"python-sdk/fastmcp-server-session_scoped_event_store",
"python-sdk/fastmcp-server-sessions",
"python-sdk/fastmcp-server-telemetry",
"python-sdk/fastmcp-server-transforms"
]
},
{
"group": "fastmcp.utilities",
"pages": [
"python-sdk/fastmcp-utilities-__init__",
"python-sdk/fastmcp-utilities-asgi_transport",
"python-sdk/fastmcp-utilities-async_utils",
"python-sdk/fastmcp-utilities-auth",
"python-sdk/fastmcp-utilities-authorization",
@ -78,6 +101,7 @@
"python-sdk/fastmcp-utilities-mime",
"python-sdk/fastmcp-utilities-openapi",
"python-sdk/fastmcp-utilities-pagination",
"python-sdk/fastmcp-utilities-prefab",
"python-sdk/fastmcp-utilities-skills",
"python-sdk/fastmcp-utilities-tasks",
"python-sdk/fastmcp-utilities-tests",

View file

@ -35,7 +35,7 @@ Usage::
## Classes
### `FastMCPApp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L145" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FastMCPApp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L149" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A Provider that represents an MCP application.
@ -48,19 +48,19 @@ can find them by original name even when transforms have been applied.
**Methods:**
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L169" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L173" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: F) -> F
```
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L181" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L185" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: str | None = None) -> Callable[[F], F]
```
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L192" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L196" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: str | AnyFunction | None = None) -> Any
@ -83,19 +83,19 @@ Supports multiple calling patterns::
def save(name: str): ...
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L259" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L266" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
ui(self, name_or_fn: F) -> F
```
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L274" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L281" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
ui(self, name_or_fn: str | None = None) -> Callable[[F], F]
```
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L288" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `ui` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L295" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
ui(self, name_or_fn: str | AnyFunction | None = None) -> Any
@ -119,7 +119,7 @@ Supports multiple calling patterns::
def dashboard() -> Component: ...
#### `add_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L363" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L373" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_tool(self, tool: Tool | Callable[..., Any]) -> Tool
@ -130,13 +130,13 @@ Add a tool to this app programmatically.
The tool is tagged with this app's name for routing.
#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L419" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L432" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
lifespan(self) -> AsyncIterator[None]
```
#### `run` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L427" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `run` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/app.py#L440" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
run(self, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, **kwargs: Any) -> None

View file

@ -15,7 +15,7 @@ UI metadata for clients that support interactive app rendering.
## Functions
### `app_config_to_meta_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L180" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `app_config_to_meta_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L181" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
app_config_to_meta_dict(app: AppConfig | dict[str, Any]) -> dict[str, Any]
@ -25,9 +25,32 @@ app_config_to_meta_dict(app: AppConfig | dict[str, Any]) -> dict[str, Any]
Convert an AppConfig or dict to the wire-format dict for ``meta["ui"]``.
### `is_model_visible` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L188" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
is_model_visible(component: FastMCPComponent) -> bool
```
Whether a component may be shown to, or invoked by, the model.
Visibility is a declaration, and the MCP Apps spec puts the filtering on
the host — so ``tools/list`` carries app-only tools and the host keeps
them from the model. That division only works where a host stands between
the server and the model.
It does not hold for surfaces a server drives itself. A search result or
a code-mode catalog reaches the model as ordinary tool output, and a
call-tool proxy invokes on a name the model supplies; nothing downstream
can filter either. Those surfaces have to apply the declaration here.
A component with no ``visibility`` is visible: the field marks the
exception, and the spec's default is both audiences.
## Classes
### `ResourceCSP` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L20" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ResourceCSP` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L21" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Content Security Policy for MCP App resources.
@ -37,7 +60,7 @@ load resources from. Hosts use these declarations to build the
``Content-Security-Policy`` header for the sandboxed iframe.
### `ResourcePermissions` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L56" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ResourcePermissions` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L57" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Iframe sandbox permissions for MCP App resources.
@ -48,7 +71,7 @@ iframe. Hosts MAY honour these; apps should use JS feature detection
as a fallback.
### `AppConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L84" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `AppConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L85" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Configuration for MCP App tools and resources.
@ -63,7 +86,7 @@ values appear on the wire. Aliases match the MCP Apps wire format
(camelCase).
### `PrefabAppConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L124" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `PrefabAppConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L125" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
App configuration for Prefab tools with sensible defaults.
@ -83,7 +106,7 @@ Example::
**Methods:**
#### `model_post_init` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L140" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `model_post_init` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/apps/config.py#L141" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
model_post_init(self, __context: Any) -> None

View file

@ -12,6 +12,7 @@ This module re-exports dependency injection symbols to provide a clean,
centralized import location for all dependency-related functionality.
DI features (Depends, CurrentContext, CurrentFastMCP) work without pydocket
using the uncalled-for DI engine. Only task-related dependencies (CurrentDocket,
CurrentWorker) and background task execution require fastmcp[tasks].
using the uncalled-for DI engine. The docket-specific dependencies
(``CurrentDocket``, ``CurrentWorker``) live in the ``fastmcp-tasks`` package
(``fastmcp_tasks.dependencies``).

View file

@ -10,7 +10,7 @@ Custom exceptions for FastMCP.
## Functions
### `to_mcp_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L98" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `to_mcp_error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L117" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_mcp_error(exc: Exception) -> MCPError
@ -38,71 +38,61 @@ explicit code chosen upstream survives translation.
## Classes
### `FastMCPDeprecationWarning` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Deprecation warning for FastMCP APIs.
Subclass of DeprecationWarning so that standard warning filters
still apply, but FastMCP can selectively enable its own warnings
without affecting other libraries in the process.
### `FastMCPError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L43" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `FastMCPError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L38" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Base error for FastMCP.
### `ValidationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L51" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ValidationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L46" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Error in validating parameters or return values.
### `ResourceError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L55" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ResourceError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L50" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Error in resource operations.
### `ToolError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L59" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ToolError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Error in tool operations.
### `PromptError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L63" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `PromptError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L58" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Error in prompt operations.
### `InvalidSignature` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L67" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `InvalidSignature` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L62" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Invalid signature for use with FastMCP.
### `ClientError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L71" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ClientError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L66" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Error in client operations.
### `NotFoundError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L75" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `NotFoundError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L70" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Object not found.
### `DisabledError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L79" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `DisabledError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L74" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Object is disabled.
### `ResourceSecurityError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L83" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ResourceSecurityError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L78" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A templated resource parameter failed path-security screening.
@ -114,8 +104,21 @@ for a resource that does not exist, and never reveals which parameter
or policy tripped.
### `AuthorizationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L94" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `AuthorizationError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L89" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Error when authorization check fails.
### `InsufficientScopeError` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/exceptions.py#L93" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Authorization failed because the token is missing required OAuth scopes.
Unlike a bare ``AuthorizationError``, this carries the specific scopes the
caller must obtain. A component-level scope shortfall can then be signalled
as a spec-correct ``insufficient_scope`` step-up (SEP-2350 / RFC 6750 §3),
naming exactly what to re-authorize for instead of an opaque denial. The
named scopes are only the *unmet* ones, so an existing grant is accumulated
rather than replaced when the caller re-authorizes.

View file

@ -32,7 +32,7 @@ Example configuration:
## Functions
### `infer_transport_type_from_url` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `infer_transport_type_from_url` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L55" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
infer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse']
@ -42,7 +42,7 @@ infer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse']
Infer the appropriate transport type from the given URL.
### `update_config_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L362" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `update_config_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L376" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
update_config_file(file_path: Path, server_name: str, server_config: CanonicalMCPServerTypes) -> None
@ -57,7 +57,7 @@ worry about transforming server objects here.
## Classes
### `StdioMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L168" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `StdioMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L180" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
MCP server configuration for stdio transport.
@ -67,19 +67,19 @@ This is the canonical configuration format for MCP servers using stdio transport
**Methods:**
#### `to_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L201" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `to_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L213" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_transport(self) -> StdioTransport
to_transport(self) -> StdioTransport | FastMCPTransport
```
### `TransformingStdioMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L213" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `TransformingStdioMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L225" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A Stdio server with tool transforms.
### `RemoteMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L217" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `RemoteMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L229" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
MCP server configuration for HTTP/SSE transport.
@ -89,19 +89,19 @@ This is the canonical configuration format for MCP servers using remote transpor
**Methods:**
#### `to_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L253" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `to_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L265" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_transport(self) -> StreamableHttpTransport | SSETransport
to_transport(self) -> StreamableHttpTransport | SSETransport | FastMCPTransport
```
### `TransformingRemoteMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L280" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `TransformingRemoteMCPServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L294" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A Remote server with tool transforms.
### `MCPConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L291" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `MCPConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L305" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A configuration object for MCP Servers that conforms to the canonical MCP configuration format
@ -113,7 +113,7 @@ For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class.
**Methods:**
#### `wrap_servers_at_root` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L305" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `wrap_servers_at_root` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L319" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any]
@ -122,7 +122,7 @@ wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any]
If there's no mcpServers key but there are server configs at root, wrap them.
#### `add_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L318" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L332" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_server(self, name: str, server: MCPServerTypes) -> None
@ -131,7 +131,7 @@ add_server(self, name: str, server: MCPServerTypes) -> None
Add or update a server in the configuration.
#### `from_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L323" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L337" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_dict(cls, config: dict[str, Any]) -> Self
@ -140,7 +140,7 @@ from_dict(cls, config: dict[str, Any]) -> Self
Parse MCP configuration from dictionary format.
#### `to_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L327" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `to_dict` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L341" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
to_dict(self) -> dict[str, Any]
@ -149,7 +149,7 @@ to_dict(self) -> dict[str, Any]
Convert MCPConfig to dictionary format, preserving all fields.
#### `write_to_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L331" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `write_to_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L345" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
write_to_file(self, file_path: Path) -> None
@ -158,7 +158,7 @@ write_to_file(self, file_path: Path) -> None
Write configuration to JSON file.
#### `from_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L337" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `from_file` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L351" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_file(cls, file_path: Path) -> Self
@ -167,7 +167,7 @@ from_file(cls, file_path: Path) -> Self
Load configuration from JSON file.
### `CanonicalMCPConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L347" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `CanonicalMCPConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L361" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Canonical MCP configuration format.
@ -178,7 +178,7 @@ The format is designed to be client-agnostic and extensible for future use cases
**Methods:**
#### `add_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L357" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
#### `add_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/mcp_config.py#L371" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_server(self, name: str, server: CanonicalMCPServerTypes) -> None

View file

@ -0,0 +1,48 @@
---
title: caching
sidebarTitle: caching
---
# `fastmcp.server.caching`
Server-level cache hints for FastMCP (SEP-2549).
A FastMCP server opts every SDK-cacheable result it emits into client-side
caching by setting `cache_ttl` (seconds) and, optionally, `cache_scope` on the
`FastMCP` constructor. The hint is uniform by construction: one server-level
value applies to `tools/list`, `prompts/list`, `resources/list`,
`resources/templates/list`, `resources/read`, and `server/discover` alike — no
per-component surface and no aggregation.
FastMCP does not hand-set the wire fields. It passes the hint through to the SDK
low-level `Server(cache_hints=...)`, whose runner fills `ttlMs`/`cacheScope` on
every cacheable result via `apply_cache_hint`, leaving any field a handler set
explicitly untouched. Honoring is modern-only and opt-in on the client: a hinted
server is inert unless the client passes `cache=` and negotiates `2026-07-28`.
## Functions
### `build_cache_hints` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/caching.py#L29" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
build_cache_hints(cache_ttl: int | None, cache_scope: CacheScope | None) -> dict[CacheableMethod, CacheHint] | None
```
Build the per-method `CacheHint` map for the SDK low-level server.
`cache_ttl` is in seconds and is converted to the wire's milliseconds. When
`cache_ttl` is `None` the server emits no hint, so its wire output is
identical to a server that never set one; a `cache_scope` given without a
`cache_ttl` is meaningless (the client gates caching on the presence of a
TTL) and is rejected rather than silently ignored.
Returns `None` when no hint is set, or a map applying the same hint to every
SDK-cacheable method otherwise.
**Raises:**
- `ValueError`: If `cache_ttl` is not positive, or if `cache_scope` is set
without `cache_ttl`.

View file

@ -0,0 +1,41 @@
---
title: completions
sidebarTitle: completions
---
# `fastmcp.server.completions`
Server-side argument completion for FastMCP.
A completion request names a reference — a specific prompt or resource
template — and the argument being completed, plus a context of the argument
values already supplied. The server answers with candidate string values.
FastMCP surfaces this as a single server-level handler registered with
``@mcp.completion``, mirroring the MCP SDK's own ``completion/complete`` shape
and FastMCP's client-side ``Client.complete()``. The handler receives the
reference, the argument, and the optional context, and returns candidates for
whichever reference/argument pair it recognizes.
## Functions
### `normalize_completion` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/completions.py#L54" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
normalize_completion(result: CompletionValues) -> mcp_types.Completion
```
Coerce a handler's return value into a wire ``Completion``.
A returned ``str`` is rejected: it is almost always a mistake (the value
would iterate into one-character candidates), so it raises rather than
silently producing surprising output.
The MCP contract caps a completion at 100 values, so a longer result is
truncated to the first 100 with ``has_more`` set — a handler that returns
thousands of matches emits a conforming response rather than an oversized
one that strict clients reject.

View file

@ -0,0 +1,711 @@
---
title: context
sidebarTitle: context
---
# `fastmcp.server.context`
## Functions
### `set_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L97" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_transport(transport: TransportType) -> Token[TransportType | None]
```
Set the current transport type. Returns token for reset.
### `reset_transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L104" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
reset_transport(token: Token[TransportType | None]) -> None
```
Reset transport to previous value.
### `set_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L134" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_context(context: Context) -> Generator[Context, None, None]
```
## Classes
### `LogData` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L110" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Data object for passing log arguments to client-side handlers.
This provides an interface to match the Python standard library logging,
for compatibility with structured logging.
### `Context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L143" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Context object providing access to MCP capabilities.
This provides a cleaner interface to MCP's RequestContext functionality.
It gets injected into tool and resource functions that request it via type hints.
To use context in a tool function, add a parameter with the Context type annotation:
```python
@server.tool
async def my_tool(x: int, ctx: Context) -> str:
# Log messages to the client
await ctx.info(f"Processing {x}")
await ctx.debug("Debug info")
await ctx.warning("Warning message")
await ctx.error("Error message")
# Report progress
await ctx.report_progress(50, 100, "Processing")
# Access resources
data = await ctx.read_resource("resource://data")
# Get request info
request_id = ctx.request_id
client_id = ctx.client_id
# Manage state across the session (persists across requests)
await ctx.set_state("key", "value")
value = await ctx.get_state("key")
# Store non-serializable values for the current request only
await ctx.set_state("client", http_client, serializable=False)
return str(x)
```
State Management:
Context provides session-scoped state that persists across requests within
the same MCP session. State is automatically keyed by session, ensuring
isolation between different clients.
State set during `on_initialize` middleware will persist to subsequent tool
calls when using the same session object (STDIO, SSE, single-server HTTP).
For distributed/serverless HTTP deployments where different machines handle
the init and tool calls, state is isolated by the mcp-session-id header.
The context parameter name can be anything as long as it's annotated with Context.
The context is optional - tools that don't need it can omit the parameter.
**Methods:**
#### `is_background_task` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L224" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
is_background_task(self) -> bool
```
True when this context is running in a background task (Docket worker).
When True, certain operations like elicit() will use task-aware
implementations that can pause the task and wait for client input.
#### `task_id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L242" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
task_id(self) -> str | None
```
Get the background task ID if running in a background task.
Returns None if not running in a background task context.
#### `origin_request_id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L250" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
origin_request_id(self) -> str | None
```
Get the request ID that originated this execution, if available.
In foreground request mode, this is the current request_id.
In background task mode, this is the request_id captured when the task
was submitted, if one was available.
#### `fastmcp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L262" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
fastmcp(self) -> FastMCP
```
Get the FastMCP instance.
#### `request_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L312" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
request_context(self) -> FastMCPRequestContext | None
```
Access to the underlying request context.
Returns None when the MCP session has not been established yet.
Returns the FastMCPRequestContext wrapper once the MCP session is available.
For HTTP request access in middleware, use `get_http_request()` from fastmcp.server.dependencies,
which works whether or not the MCP session is available.
Example in middleware:
```python
async def on_request(self, context, call_next):
ctx = context.fastmcp_context
if ctx.request_context:
# MCP session available - can access session_id, request_id, etc.
session_id = ctx.session_id
else:
# MCP session not available yet - use HTTP helpers
from fastmcp.server.dependencies import get_http_request
request = get_http_request()
return await call_next(context)
```
#### `client_extension_settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L337" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
client_extension_settings(self, identifier: str) -> dict[str, Any] | None
```
This request's per-request opt-in settings for an MCP extension.
SEP-2133 extensions negotiate per request: the client repeats its
extension capabilities in each request's ``_meta`` under
``io.modelcontextprotocol/clientCapabilities`` → ``extensions`` →
``identifier``. Returns the declared settings dict (possibly empty) when
the extension was opted in for this request, or ``None`` when it was
not (or there is no active request). This bridges an extension's
``tools/call`` interceptor — which receives a FastMCP ``Context`` — to
the request's declared client capabilities.
#### `input_responses` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L378" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
input_responses(self) -> mcp_types.InputResponses | None
```
Client responses to a prior `InputRequiredResult.input_requests`.
The multi-round-trip guard channel (SEP-2322). A guard tool inspects
this to decide what to do on each round: `None` on the initial round
(nothing has been asked yet, or the client retried without responses),
so the tool returns an `InputRequiredResult` to ask; present on a later
round, so the tool reads the answers and proceeds. It is a mapping whose
keys match the `input_requests` map the tool minted; each value is the
client's result for that request (an `ElicitResult`, `CreateMessageResult`,
or `ListRootsResult`).
In a background task there is no wire request, so this falls back to the
responses the in-task guard loop delivered (see the tasks extension).
#### `request_state` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L399" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
request_state(self) -> str | None
```
Opaque state echoed from a prior `InputRequiredResult.request_state`.
The multi-round-trip guard channel (SEP-2322): whatever a tool put in
`InputRequiredResult.request_state` on an earlier round is handed back
here (as plaintext — the framework seals it on the wire and unseals it
before the tool runs, so tampering is rejected before this is read).
`None` on the initial round. Use it to carry a small amount of computed
state across rounds without re-deriving it.
In a background task there is no wire request, so this falls back to the
state the in-task guard loop re-injected (see the tasks extension).
#### `lifespan_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L418" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
lifespan_context(self) -> dict[str, Any]
```
Access the server's lifespan context.
Returns the context dict yielded by *this* server's lifespan function.
For a mounted child this is the child's own lifespan, not the parent's
— the MCP session always belongs to the parent, so reading from the
request context would return the parent's. We read directly from the
server's cached lifespan result instead, which is set by the
per-server ``_lifespan_manager`` regardless of mount position.
Returns an empty dict if no lifespan was configured.
Example:
```python
@server.tool
def my_tool(ctx: Context) -> str:
db = ctx.lifespan_context.get("db")
if db:
return db.query("SELECT 1")
return "No database connection"
```
#### `report_progress` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L453" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None
```
Report progress for the current operation.
Works in both foreground (MCP progress notifications) and background
(Docket task execution) contexts.
**Args:**
- `progress`: Current progress value e.g. 24
- `total`: Optional total value e.g. 100
- `message`: Optional status message describing current progress
#### `list_resources` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L552" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resources(self) -> list[SDKResource]
```
List all available resources from the server.
**Returns:**
- List of Resource objects available on the server
#### `list_prompts` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L563" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_prompts(self) -> list[SDKPrompt]
```
List all available prompts from the server.
**Returns:**
- List of Prompt objects available on the server
#### `get_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L574" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult
```
Get a prompt by name with optional arguments.
**Args:**
- `name`: The name of the prompt to get
- `arguments`: Optional arguments to pass to the prompt
**Returns:**
- The prompt result
#### `read_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L593" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(self, uri: str | AnyUrl) -> ResourceResult
```
Read a resource by URI.
**Args:**
- `uri`: Resource URI to read
**Returns:**
- ResourceResult with contents
#### `log` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L609" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
log(self, message: str, level: LoggingLevel | None = None, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
```
Send a log message to the client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
**Args:**
- `message`: Log message
- `level`: Optional log level. One of "debug", "info", "notice", "warning", "error", "critical",
"alert", or "emergency". Default is "info".
- `logger_name`: Optional logger name
- `extra`: Optional mapping for additional arguments
#### `transport` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L650" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
transport(self) -> TransportType | None
```
Get the current transport type.
Returns the transport type used to run this server: "stdio", "sse",
or "streamable-http". Returns None if called outside of a server context.
#### `client_supports_extension` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L658" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
client_supports_extension(self, extension_id: str) -> bool
```
Check whether the connected client supports a given MCP extension.
Inspects the ``extensions`` extra field on ``ClientCapabilities``
sent by the client during initialization.
Reads the client's advertised capabilities from the session, which is
available in request mode and in background-task mode (where the
snapshot session preserves the client's initialize params). Returns
``False`` when no session is available (e.g., a distributed worker with
no live session, or outside any context) or when the client did not
advertise the extension.
Example::
from fastmcp.apps.config import UI_EXTENSION_ID
@mcp.tool
async def my_tool(ctx: Context) -> str:
if ctx.client_supports_extension(UI_EXTENSION_ID):
return "UI-capable client"
return "text-only client"
#### `client_id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L688" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
client_id(self) -> str | None
```
Get the client ID if available.
#### `request_id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L696" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
request_id(self) -> str
```
Get the unique ID for this request.
Raises RuntimeError if MCP request context is not available.
#### `session_id` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L709" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
session_id(self) -> str
```
Get the MCP session ID for ALL transports.
Returns the session ID that can be used as a key for session-based
data storage (e.g., Redis) to share data between tool calls within
the same client session.
**Returns:**
- The session ID for StreamableHTTP transports, or a generated ID
- for other transports.
#### `session` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L794" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
session(self) -> ServerSession
```
Access to the underlying session for advanced usage.
In request mode: Returns the session from the active request context.
In background task mode: Returns the session stored at Context creation.
Raises RuntimeError if no session is available.
#### `debug` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L820" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
debug(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
```
Send a `DEBUG`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
#### `info` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L836" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
info(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
```
Send a `INFO`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
#### `warning` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L852" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
warning(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
```
Send a `WARNING`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
#### `error` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L868" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
error(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
```
Send a `ERROR`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
#### `send_notification` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L884" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
send_notification(self, notification: mcp_types.ServerNotification) -> None
```
Send a notification to the client immediately.
**Args:**
- `notification`: An MCP notification instance (e.g., ToolListChangedNotification())
#### `close_sse_stream` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L904" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
close_sse_stream(self) -> None
```
Close the current response stream to trigger client reconnection.
When using StreamableHTTP transport with an EventStore configured, this
method gracefully closes the HTTP connection for the current request.
The client will automatically reconnect (after `retry_interval` milliseconds)
and resume receiving events from where it left off via the EventStore.
This is useful for long-running operations to avoid load balancer timeouts.
Instead of holding a connection open for minutes, you can periodically close
and let the client reconnect.
#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L958" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: type[T]) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation
```
The accepted elicitation will contain the response data
#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L969" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: list[str]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation
```
When response_type is a list of strings, the accepted elicitation will
contain the selected string response
#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L981" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: dict[str, dict[str, str]]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation
```
When response_type is a dict mapping keys to title dicts, the accepted
elicitation will contain the selected key
#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L993" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: list[list[str]]) -> 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
#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1005" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: list[dict[str, dict[str, str]]]) -> 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
#### `elicit` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1017" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
elicit(self, message: str, response_type: type[T] | list[str] | dict[str, dict[str, str]] | list[list[str]] | list[dict[str, dict[str, str]]]) -> AcceptedElicitation[T] | AcceptedElicitation[dict[str, Any]] | AcceptedElicitation[str] | AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
```
Send an elicitation request to the client and await the response.
Call this method at any time to request additional information from
the user through the client. The client must support elicitation,
or the request will error.
Note that the MCP protocol only supports simple object schemas with
primitive types. You can provide a dataclass, TypedDict, or BaseModel to
comply. If you provide a primitive type, an object schema with a single
"value" field will be generated for the MCP interaction and
automatically deconstructed into the primitive type upon response.
``response_type`` is required. Pass ``bool`` when all you need is a
confirmation; an empty schema leaves some clients rendering an empty,
non-functional form.
**Args:**
- `message`: A human-readable message explaining what information is needed
- `response_type`: The type of the response, which should be a primitive
type or dataclass or BaseModel. If it is a primitive type, an
object schema with a single "value" field will be generated.
- `response_title`: Optional label to display for the wrapped ``value``
field when ``response_type`` is a scalar, Literal, Enum, or one
of the dict/list shorthand forms. Overrides the auto-generated
"Value" label. Raises ``TypeError`` if passed with a BaseModel,
dataclass, or ``None`` response type (use ``Field(title=...)``
on the model instead).
- `response_description`: Optional description to attach to the wrapped
``value`` field. Same scope rules as ``response_title``.
#### `set_state` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1110" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_state(self, key: str, value: Any) -> None
```
Set a value in the state store.
By default, values are stored in the session-scoped state store and
persist across requests within the same MCP session. Values must be
JSON-serializable (dicts, lists, strings, numbers, etc.).
For non-serializable values (e.g., HTTP clients, database connections),
pass ``serializable=False``. These values are stored in a request-scoped
dict and only live for the current MCP request (tool call, resource
read, or prompt render). They will not be available in subsequent
requests.
The key is automatically prefixed with the session identifier.
#### `get_state` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1164" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_state(self, key: str) -> Any
```
Get a value from the state store.
Checks request-scoped state first (set with ``serializable=False``),
then falls back to the session-scoped state store.
Returns None if the key is not found.
#### `delete_state` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1178" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
delete_state(self, key: str) -> None
```
Delete a value from the state store.
Removes from both request-scoped and session-scoped stores.
#### `enable_components` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1199" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
enable_components(self) -> None
```
Enable components matching criteria for this session only.
Session rules override global transforms. Rules accumulate - each call
adds a new rule to the session. Later marks override earlier ones
(Visibility transform semantics).
Sends notifications to this session only: ToolListChangedNotification,
ResourceListChangedNotification, and PromptListChangedNotification.
**Args:**
- `names`: Component names or URIs to match.
- `keys`: Component keys to match (e.g., {"tool\:my_tool@v1"}).
- `version`: Component version spec to match.
- `tags`: Tags to match (component must have at least one).
- `components`: Component types to match (e.g., {"tool", "prompt"}).
- `match_all`: If True, matches all components regardless of other criteria.
#### `disable_components` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1237" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
disable_components(self) -> None
```
Disable components matching criteria for this session only.
Session rules override global transforms. Rules accumulate - each call
adds a new rule to the session. Later marks override earlier ones
(Visibility transform semantics).
Sends notifications to this session only: ToolListChangedNotification,
ResourceListChangedNotification, and PromptListChangedNotification.
**Args:**
- `names`: Component names or URIs to match.
- `keys`: Component keys to match (e.g., {"tool\:my_tool@v1"}).
- `version`: Component version spec to match.
- `tags`: Tags to match (component must have at least one).
- `components`: Component types to match (e.g., {"tool", "prompt"}).
- `match_all`: If True, matches all components regardless of other criteria.
#### `reset_visibility` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/context.py#L1275" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
reset_visibility(self) -> None
```
Clear all session visibility rules.
Use this to reset session visibility back to global defaults.
Sends notifications to this session only: ToolListChangedNotification,
ResourceListChangedNotification, and PromptListChangedNotification.

View file

@ -0,0 +1,614 @@
---
title: dependencies
sidebarTitle: dependencies
---
# `fastmcp.server.dependencies`
Dependency injection for FastMCP.
DI features (Depends, CurrentContext, CurrentFastMCP) work without pydocket
using the uncalled-for DI engine. The docket-specific dependencies
(``CurrentDocket``, ``CurrentWorker``) and background task execution live in the
``fastmcp-tasks`` package.
## Functions
### `bind_request_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L104" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
bind_request_context(ctx: ServerRequestContext) -> Generator[FastMCPRequestContext, None, None]
```
Bind a ``FastMCPRequestContext`` for the duration of a handler.
Constructs the wrapper from the SDK's per-request context and sets/resets
the ``fastmcp_request_ctx`` ContextVar. Every request adapter and the
initialize middleware enters this so ``Context`` and dependency helpers can
read the active request from the ContextVar.
### `extract_version_spec` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L131" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
extract_version_spec(meta: dict[str, Any] | None) -> str | None
```
Extract the FastMCP component version from a lifted ``_meta`` block.
### `set_background_context_factory` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L185" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_background_context_factory(factory: Callable[[], Awaitable[Context | None]] | None) -> None
```
Install (or clear) the background-task ``Context`` factory.
The factory returns an already-entered ``Context`` (so ``_current_context``
is set for cleanup) when called inside a worker, or ``None`` when there is
no task context. Passing ``None`` restores core's no-worker-fallback
behavior.
### `set_worker_server_resolver` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L207" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_worker_server_resolver(resolver: Callable[[], FastMCP | None] | None) -> None
```
Install (or clear) the worker-server resolver used by ``get_server()``.
### `is_docket_available` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L244" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
is_docket_available() -> bool
```
Check if a compatible pydocket (>= 0.19.0) is installed and importable.
Three things have to be true for fastmcp's task features to work:
1. pydocket distribution metadata is discoverable
2. its version is at least ``_MIN_DOCKET_VERSION`` (older versions are
missing symbols like ``docket.dependencies.current_execution``,
which fastmcp imports on the request hot path)
3. the package actually imports — guards against broken/partial
installs where metadata exists but ``import docket`` blows up
Any of those failing means we treat docket as unavailable and fall back
to the no-tasks code paths instead of crashing deep inside a request.
### `transform_context_annotations` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L276" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
transform_context_annotations(fn: Callable[..., Any]) -> Callable[..., Any]
```
Transform injected-by-type params into Dependency-defaulted params.
Transforms ALL params typed as Context (into ``= CurrentContext()``) and as
UserSession (into ``= CurrentSession()``) to use Docket's DI system, unless
they already have a Dependency-based default.
This unifies the legacy type annotation DI with Docket's Depends() system,
allowing both patterns to work through a single resolution path.
Note: Only POSITIONAL_OR_KEYWORD parameters are reordered (params with defaults
after those without). KEYWORD_ONLY parameters keep their position since Python
allows them to have defaults in any order.
**Args:**
- `fn`: Function to transform
**Returns:**
- Function with modified signature (same function object, updated __signature__)
### `get_context` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L442" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_context() -> Context
```
Get the current FastMCP Context instance directly.
### `get_server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L452" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_server() -> FastMCP
```
Get the current FastMCP server instance directly.
In a background-task worker the tasks extension's resolver is consulted
first, so a mounted-child task resolves to the child server rather than the
root that started the worker (#3571).
**Returns:**
- The active FastMCP server
**Raises:**
- `RuntimeError`: If no server in context
### `get_session` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L480" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_session(session_id: str) -> Session
```
Resolve and validate a `Session` for an explicit `session_id`.
Pair with a `session_id: SessionId` tool argument (the agent obtains an id
from `create_session` and passes it back). For a single per-user bucket with
nothing for the agent to pass, inject `session: UserSession` instead.
State is keyed by `(principal, session_id)`: the authenticated principal is
the isolation wall and `session_id` organizes sessions within it. The id must
have been minted by `create_session` under the current principal; an id that
was never created, or created under a different principal, raises
`InvalidSession` rather than resolving to a fresh empty bucket (the specific
reason is logged at debug level, never returned to the caller).
Like `get_server()`, this resolves through the task-aware server, so it needs
no foreground context — it works from a `task=True` tool's Docket worker as
well as a normal request.
### `get_http_request` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L515" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_http_request() -> Request
```
Get the current HTTP request.
Tries MCP SDK's request_ctx first, then falls back to FastMCP's HTTP context.
### `get_http_headers` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L536" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_http_headers(include_all: bool = False, include: set[str] | None = None) -> dict[str, str]
```
Extract headers from the current HTTP request if available.
Never raises an exception, even if there is no active HTTP request (in which case
an empty dict is returned).
By default, strips problematic headers like `content-length` and `authorization`
that cause issues if forwarded to downstream services. If `include_all` is True,
all headers are returned.
The `include` parameter allows specific headers to be included even if they would
normally be excluded. This is useful for proxy transports that need to forward
authorization headers to upstream MCP servers.
### `get_access_token` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L600" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_access_token() -> AccessToken | None
```
Get the FastMCP access token from the current context.
This function first tries to get the token from the current HTTP request's scope,
which is more reliable for long-lived connections where the SDK's auth_context_var
may become stale after token refresh. Falls back to the SDK's context var if no
request is available.
**Returns:**
- The access token if an authenticated user is available, None otherwise.
### `without_injected_parameters` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L659" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any]
```
Create a wrapper function without injected parameters.
Returns a wrapper that excludes Context and Docket dependency parameters,
making it safe to use with Pydantic TypeAdapter for schema generation and
validation. The wrapper internally handles all dependency resolution and
Context injection when called.
Handles:
- Legacy Context injection (always works)
- Depends() injection (always works - uses docket or vendored DI engine)
**Args:**
- `fn`: Original function with Context and/or dependencies
- `run_in_thread`: For sync ``fn``, whether to dispatch the call to a worker
thread after resolving dependencies. Defaults to True. Set to False
to call ``fn`` inline on the event loop thread — required for
thread-affinity libraries (e.g. Windows COM). Ignored for async fns.
**Returns:**
- Async wrapper function without injected parameters
### `resolve_dependencies` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L820" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
resolve_dependencies(fn: Callable[..., Any], arguments: dict[str, Any]) -> AsyncGenerator[dict[str, Any], None]
```
Resolve dependencies for a FastMCP function.
This function:
1. Filters out any dependency parameter names from user arguments (security)
2. Resolves Depends() parameters via the DI system
The filtering prevents external callers from overriding injected parameters by
providing values for dependency parameter names. This is a security feature.
Note: Context injection is handled via transform_context_annotations() which
converts `ctx: Context` to `ctx: Context = Depends(get_context)` at registration
time, so all injection goes through the unified DI system.
**Args:**
- `fn`: The function to resolve dependencies for
- `arguments`: User arguments (may contain keys that match dependency names,
which will be filtered out)
### `CurrentContext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L945" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
CurrentContext() -> Context
```
Get the current FastMCP Context instance.
This dependency provides access to the active FastMCP Context for the
current MCP operation (tool/resource/prompt call).
**Returns:**
- A dependency that resolves to the active Context instance
**Raises:**
- `RuntimeError`: If no active context found (during resolution)
### `OptionalCurrentContext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L970" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
OptionalCurrentContext() -> Context | None
```
Get the current FastMCP Context, or None when no context is active.
### `CurrentFastMCP` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L990" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
CurrentFastMCP() -> FastMCP
```
Get the current FastMCP server instance.
This dependency provides access to the active FastMCP server.
**Returns:**
- A dependency that resolves to the active FastMCP server
**Raises:**
- `RuntimeError`: If no server in context (during resolution)
### `CurrentRequest` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1030" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
CurrentRequest() -> Request
```
Get the current HTTP request.
This dependency provides access to the Starlette Request object for the
current HTTP request. Only available when running over HTTP transports
(SSE or Streamable HTTP).
**Returns:**
- A dependency that resolves to the active Starlette Request
**Raises:**
- `RuntimeError`: If no HTTP request in context (e.g., STDIO transport)
### `CurrentHeaders` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1071" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
CurrentHeaders() -> dict[str, str]
```
Get the current HTTP request headers.
This dependency provides access to the HTTP headers for the current request,
including the authorization header. Returns an empty dictionary when no HTTP
request is available, making it safe to use in code that might run over any
transport.
**Returns:**
- A dependency that resolves to a dictionary of header name -> value
### `CurrentAccessToken` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1289" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
CurrentAccessToken() -> AccessToken
```
Get the current access token for the authenticated user.
This dependency provides access to the AccessToken for the current
authenticated request. Raises an error if no authentication is present.
**Returns:**
- A dependency that resolves to the active AccessToken
**Raises:**
- `RuntimeError`: If no authenticated user (use get_access_token() for optional)
### `TokenClaim` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1346" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
TokenClaim(name: str) -> str
```
Get a specific claim from the access token.
This dependency extracts a single claim value from the current access token.
It's useful for getting user identifiers, roles, or other token claims
without needing the full token object.
**Args:**
- `name`: The name of the claim to extract (e.g., "oid", "sub", "email")
**Returns:**
- A dependency that resolves to the claim value as a string
**Raises:**
- `RuntimeError`: If no access token is available or claim is missing
## Classes
### `FastMCPRequestContext` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L55" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
FastMCP-owned wrapper around the SDK's per-request context.
The SDK v2 runner hands each handler a fresh ``ServerRequestContext`` as an
argument rather than exposing it through a ContextVar. FastMCP owns this
ContextVar (``fastmcp_request_ctx``) and each request adapter binds a
``FastMCPRequestContext`` at the top of the handler (and the initialize
middleware binds it too).
A wrapper rather than the raw context because the SDK's
``ServerRequestContext.meta`` is a bare ``RequestParamsMeta`` TypedDict that
only carries ``progress_token`` — it does not carry ``_meta.fastmcp`` or the
distributed-trace parent. Those live in the raw params dict under ``_meta``,
which this wrapper lifts once so downstream consumers have a stable surface.
### `ProgressLike` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1099" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Protocol for progress tracking interface.
Defines the common interface between InMemoryProgress (server context)
and Docket's Progress (worker context).
**Methods:**
#### `current` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1107" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
current(self) -> int | None
```
Current progress value.
#### `total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1112" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
total(self) -> int
```
Total/target progress value.
#### `message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1117" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
message(self) -> str | None
```
Current progress message.
#### `set_total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1121" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_total(self, total: int) -> None
```
Set the total/target value for progress tracking.
#### `increment` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1125" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
increment(self, amount: int = 1) -> None
```
Atomically increment the current progress value.
#### `set_message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1129" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_message(self, message: str | None) -> None
```
Update the progress status message.
### `InMemoryProgress` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1134" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
In-memory progress tracker for immediate tool execution.
Provides the same interface as Docket's Progress but stores state in memory
instead of Redis. Useful for testing and immediate execution where
progress doesn't need to be observable across processes.
**Methods:**
#### `current` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1159" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
current(self) -> int | None
```
#### `total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1163" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
total(self) -> int
```
#### `message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1167" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
message(self) -> str | None
```
#### `set_total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1170" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_total(self, total: int) -> None
```
Set the total/target value for progress tracking.
#### `increment` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1176" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
increment(self, amount: int = 1) -> None
```
Atomically increment the current progress value.
#### `set_message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1185" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_message(self, message: str | None) -> None
```
Update the progress status message.
### `Progress` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1190" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Progress dependency that works in both server and worker contexts.
In a Docket worker, delegates to the execution's Redis-backed progress
(observable across processes). Otherwise, uses in-memory tracking.
The shared default instance acts as a stateless factory — ``__aenter__``
creates a fresh ``Progress`` per invocation so concurrent tasks never
share mutable state.
**Methods:**
#### `current` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1231" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
current(self) -> int | None
```
Current progress value.
#### `total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1237" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
total(self) -> int
```
Total/target progress value.
#### `message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1243" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
message(self) -> str | None
```
Current progress message.
#### `set_total` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1248" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_total(self, total: int) -> None
```
Set the total/target value for progress tracking.
#### `increment` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1253" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
increment(self, amount: int = 1) -> None
```
Atomically increment the current progress value.
#### `set_message` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/dependencies.py#L1258" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_message(self, message: str | None) -> None
```
Update the progress status message.

View file

@ -0,0 +1,152 @@
---
title: elicitation
sidebarTitle: elicitation
---
# `fastmcp.server.elicitation`
## Functions
### `parse_elicit_response_type` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L143" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
parse_elicit_response_type(response_type: Any, response_title: str | None = None, response_description: str | None = None) -> ElicitConfig
```
Parse response_type into schema and handling configuration.
A response type is required; ``None`` raises ``TypeError``. Supports
multiple syntaxes:
- dict: `{"low": {"title": "..."}}` -> single-select titled enum
- list patterns:
- `[["a", "b"]]` -> multi-select untitled
- `[{"low": {...}}]` -> multi-select titled
- `["a", "b"]` -> single-select untitled
- `list\[X]` type annotation: multi-select with type
- Scalar types (bool, int, float, str, Literal, Enum): single value
- Other types (dataclass, BaseModel): use directly
The ``response_title`` and ``response_description`` arguments customize the
label and description of the wrapped ``value`` property for the scalar/dict/list
shorthand forms. They are only valid when FastMCP is wrapping the response
type; passing them with a full BaseModel/dataclass raises ``TypeError``,
because in those cases the user already controls field metadata via
``Field(title=..., description=...)``.
### `handle_elicit_accept` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L310" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
handle_elicit_accept(config: ElicitConfig, content: Any) -> AcceptedElicitation[Any]
```
Handle an accepted elicitation response.
**Args:**
- `config`: The elicitation configuration from parse_elicit_response_type
- `content`: The response content from the client
**Returns:**
- AcceptedElicitation with the extracted/validated data
### `get_elicitation_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L369" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_elicitation_schema(response_type: type[T]) -> dict[str, Any]
```
Get the schema for an elicitation response.
**Args:**
- `response_type`: The type of the response
### `validate_elicitation_json_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L395" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
validate_elicitation_json_schema(schema: dict[str, Any]) -> None
```
Validate that a JSON schema follows MCP elicitation requirements.
This ensures the schema is compatible with MCP elicitation requirements:
- Must be an object schema
- Must only contain primitive field types (string, number, integer, boolean)
- Must be flat (no nested objects or arrays of objects)
- Allows const fields (for Literal types) and enum fields (for Enum types)
- Only primitive types and their nullable variants are allowed
**Args:**
- `schema`: The JSON schema to validate
**Raises:**
- `TypeError`: If the schema doesn't meet MCP elicitation requirements
## Classes
### `ElicitationJsonSchema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L36" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Custom JSON schema generator for MCP elicitation that always inlines enums.
MCP elicitation requires inline enum schemas without $ref/$defs references.
This generator ensures enums are always generated inline for compatibility.
Optionally adds enumNames for better UI display when available.
**Methods:**
#### `generate_inner` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L44" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
generate_inner(self, schema: core_schema.CoreSchema) -> JsonSchemaValue
```
Override to prevent ref generation for enums and handle list schemas.
#### `list_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L57" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_schema(self, schema: core_schema.ListSchema) -> JsonSchemaValue
```
Generate schema for list types, detecting enum items for multi-select.
#### `enum_schema` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L94" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue
```
Generate inline enum schema.
Always generates enum pattern: `{"enum": [value, ...]}`
Titled enums are handled separately via dict-based syntax in ctx.elicit().
### `AcceptedElicitation` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L105" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Result when user accepts the elicitation.
### `ScalarElicitationType` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L113" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
### `ElicitConfig` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/elicitation.py#L118" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Configuration for an elicitation request.
**Attributes:**
- `schema`: The JSON schema to send to the client
- `response_type`: The type to validate responses with (None for raw schemas)
- `is_raw`: True if schema was built directly (extract "value" from response)

View file

@ -0,0 +1,78 @@
---
title: event_store
sidebarTitle: event_store
---
# `fastmcp.server.event_store`
EventStore implementation backed by AsyncKeyValue.
This module provides an EventStore implementation that enables SSE polling/resumability
for Streamable HTTP transports. Events are stored using the key_value package's
AsyncKeyValue protocol, allowing users to configure any compatible backend
(in-memory, Redis, etc.) following the same pattern as ResponseCachingMiddleware.
## Classes
### `EventEntry` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L38" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Stored event entry.
### `StreamEventList` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L46" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
List of event IDs for a stream.
### `EventStore` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L52" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
EventStore implementation backed by AsyncKeyValue.
Enables SSE polling/resumability by storing events that can be replayed
when clients reconnect. Works with any AsyncKeyValue backend (memory, Redis, etc.)
following the same pattern as ResponseCachingMiddleware and OAuthProxy.
**Args:**
- `storage`: AsyncKeyValue backend. Defaults to MemoryStore.
- `max_events_per_stream`: Maximum events to retain per stream. Default 100.
- `ttl`: Event TTL in seconds. Default 3600 (1 hour). Set to None for no expiration.
**Methods:**
#### `store_event` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L120" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId
```
Store an event and return its ID.
**Args:**
- `stream_id`: ID of the stream the event belongs to
- `message`: The JSON-RPC message to store, or None for priming events
**Returns:**
- The generated event ID for the stored event
#### `replay_events_after` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/event_store.py#L166" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
replay_events_after(self, last_event_id: EventId, send_callback: EventCallback) -> StreamId | None
```
Replay events that occurred after the specified event ID.
**Args:**
- `last_event_id`: The ID of the last event the client received
- `send_callback`: A callback function to send events to the client
**Returns:**
- The stream ID of the replayed events, or None if the event ID was not found

View file

@ -0,0 +1,194 @@
---
title: extensions
sidebarTitle: extensions
---
# `fastmcp.server.extensions`
FastMCP-native server extension API (SEP-2133).
An MCP extension is an opt-in, capability-negotiated bundle of protocol
behaviour identified by a reverse-DNS string (e.g. `io.modelcontextprotocol/tasks`).
Unlike the SDK's `mcp.server.extension.Extension`, a FastMCP `ServerExtension`
is bound to its `FastMCP` instance at registration, so its request handlers and
its `tools/call` interceptor can reach the component registry, `Context`, and
auth scope that the SDK's model withholds.
An extension contributes any subset of four things:
- **A negotiated capability.** `settings()` is spliced into
`ServerCapabilities.extensions[identifier]` (see `LowLevelServer.get_capabilities`).
- **New request methods.** `methods()` returns `MethodBinding`s, each wired onto
the low-level server via `add_request_handler` when the extension is registered.
- **A `tools/call` interceptor.** `intercept_tool_call()` is the last gate before
a tool body runs — it composes *after* the FastMCP middleware chain and *before*
component execution, so it can observe, short-circuit, or pass a call through.
- **A lifespan.** `lifespan()` is entered with the server's lifespan and exited on
shutdown — the hook the SDK's `Extension` lacks, needed to start backends/workers.
The base class follows the SDK's httpx-style shape: every contribution method has
a default, so a subclass overrides only what it needs.
## Functions
### `read_client_extension_settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L235" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_client_extension_settings(ctx: ServerRequestContext[Any, Any], identifier: str) -> dict[str, Any] | None
```
Read a client's per-request extension opt-in from the request `_meta`.
SEP-2133 extensions negotiate per request: the client repeats its extension
capabilities in each request's `_meta` under
`io.modelcontextprotocol/clientCapabilities` → `extensions` → `identifier`.
Returns the declared settings dict (possibly empty) when the extension was
opted in for this request, or `None` when it was not.
### `build_method_handler` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L249" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
build_method_handler(binding: MethodBinding) -> ExtensionRequestHandler
```
Wrap a `MethodBinding` into a low-level request handler.
The adapter enforces `protocol_versions` gating (rejecting other versions as
`METHOD_NOT_FOUND`, since `add_request_handler` registers unconditionally)
and binds the FastMCP request context so the handler can use `get_context()`,
auth, and other request-scoped dependencies.
### `wrap_tool_call_interceptor` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L278" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
wrap_tool_call_interceptor(extension: ServerExtension, call_next: Callable[[Any], Awaitable[Any]]) -> Callable[[Any], Awaitable[Any]]
```
Fold one extension's `intercept_tool_call` around a middleware `call_next`.
The returned wrapper is a FastMCP `CallNext`: it hands the extension the
validated `tools/call` params, the FastMCP `Context`, and a zero-arg
continuation that runs the rest of the chain and, finally, the tool body.
## Classes
### `MethodBinding` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L77" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
A new request method an extension serves, e.g. `tasks/get`.
`params_type` validates incoming params before `handler` runs; it should
subclass `RequestParams` so `_meta` parses uniformly. `protocol_versions`,
when set, restricts the method to those wire versions — a request at any
other version is rejected as `METHOD_NOT_FOUND`, mirroring the spec's
`(method, version)` boundary. `None` (the default) admits every version.
Extension methods are additive: `method` must not name a spec-defined
request method (`tools/call`, `completion/complete`, ...). Binding one would
silently shadow the server's own handler. Both constraints are enforced at
construction.
### `ServerExtension` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L111" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Base class for an opt-in FastMCP server extension (SEP-2133).
Subclass, set `identifier`, and override the contribution methods that
apply. Every method has a default, so a minimal extension overrides only
`identifier` and one contribution. `identifier` is validated at
subclass-definition time when set as a class attribute, and again at
registration (which covers per-instance identifiers assigned in `__init__`).
Register an instance with `FastMCP.add_extension(...)`, which binds the
extension to the server so `self.server`, `intercept_tool_call`, and method
handlers can reach FastMCP-level constructs.
**Methods:**
#### `server` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L149" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
server(self) -> FastMCP
```
The FastMCP server this extension is registered on.
Handlers, interceptors, and lifespan code reach the component registry,
`Context`, and auth scope through here. Raises if the extension has not
been registered with `FastMCP.add_extension()`.
#### `settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L165" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
settings(self) -> dict[str, Any]
```
Per-extension settings advertised at `capabilities.extensions[identifier]`.
An empty dict (the default) advertises the extension with no settings.
#### `methods` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L172" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
methods(self) -> Sequence[MethodBinding]
```
New request methods this extension serves (additive).
#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L176" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
lifespan(self) -> AbstractAsyncContextManager[None]
```
A context manager entered with the server's lifespan, exited on shutdown.
Default: a no-op. Override to start and stop resources an extension owns
(a task-queue backend and worker, say). Entered once per runtime tree, at
the root — a mounted child defers to the root, as the shared Docket does.
#### `intercept_tool_call` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L185" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
intercept_tool_call(self, params: CallToolRequestParams, context: Context, call_next: ToolCallContinuation) -> ToolCallOutcome
```
Wrap `tools/call`. Default: pass through unchanged.
Runs after the FastMCP middleware chain and before the tool body, so it
is the last gate before execution. Override to observe the call, to
short-circuit (return a result without awaiting `call_next`), or to pass
it through (`return await call_next()`). `params` is the validated
`tools/call` params; `context` is the FastMCP `Context`, from which the
tool being called (`context.fastmcp.get_tool(params.name)`), auth scope,
and the server are reachable. Multiple extensions nest with the
first-registered outermost.
#### `client_settings` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/extensions.py#L204" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
client_settings(self, ctx: ServerRequestContext[Any, Any]) -> dict[str, Any] | None
```
This extension's per-request opt-in settings declared by the client.
Reads the request's `_meta` client-capabilities block. Returns the
declared settings dict (possibly empty) when the client opted this
extension in for the request, or `None` when it did not. Convenience for
`read_client_extension_settings(ctx, self.identifier)`.

View file

@ -0,0 +1,144 @@
---
title: http
sidebarTitle: http
---
# `fastmcp.server.http`
## Functions
### `set_http_request` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L355" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
set_http_request(request: Request) -> Generator[Request, None, None]
```
### `create_base_app` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L388" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_base_app(routes: list[BaseRoute], middleware: list[Middleware], debug: bool = False, lifespan: Callable | None = None) -> StarletteWithLifespan
```
Create a base Starlette app with common middleware and routes.
**Args:**
- `routes`: List of routes to include in the app
- `middleware`: List of middleware to include in the app
- `debug`: Whether to enable debug mode
- `lifespan`: Optional lifespan manager for the app
**Returns:**
- A Starlette application
### `create_sse_app` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L416" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_sse_app(server: FastMCP[LifespanResultT], message_path: str, sse_path: str, auth: AuthProvider | None = None, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None) -> StarletteWithLifespan
```
Return an instance of the SSE server app.
**Args:**
- `server`: The FastMCP server instance
- `message_path`: Path for SSE messages
- `sse_path`: Path for SSE connections
- `auth`: Optional authentication provider (AuthProvider)
- `debug`: Whether to enable debug mode
- `routes`: Optional list of custom routes
- `middleware`: Optional list of middleware
Returns:
A Starlette application with RequestContextMiddleware
### `create_streamable_http_app` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L545" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_streamable_http_app(server: FastMCP[LifespanResultT], streamable_http_path: str, event_store: EventStore | None = None, retry_interval: int | None = None, auth: AuthProvider | None = None, json_response: bool = False, stateless_http: bool = False, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None, host_origin_protection: HostOriginProtection = False, allowed_hosts: Sequence[str] | None = None, allowed_origins: Sequence[str] | None = None, session_idle_timeout: float | None = None) -> StarletteWithLifespan
```
Return an instance of the StreamableHTTP server app.
**Args:**
- `server`: The FastMCP server instance
- `streamable_http_path`: Path for StreamableHTTP connections
- `event_store`: Optional event store for SSE polling/resumability
- `retry_interval`: Optional retry interval in milliseconds for SSE polling.
Controls how quickly clients should reconnect after server-initiated
disconnections. Requires event_store to be set. Defaults to SDK default.
- `auth`: Optional authentication provider (AuthProvider)
- `json_response`: Whether to use JSON response format
- `stateless_http`: Whether to use stateless mode (new transport per request)
- `debug`: Whether to enable debug mode
- `routes`: Optional list of custom routes
- `middleware`: Optional list of middleware
- `host_origin_protection`: Whether to validate Host and Origin headers
before requests reach the MCP endpoint. Defaults to False for
compatibility. "auto" protects localhost-bound servers and explicit
host/origin allowlists.
- `allowed_hosts`: Additional hostnames that may appear in the Host header.
- `allowed_origins`: Additional browser origins trusted by the request guard.
Configure CORS separately when browser JavaScript must read
cross-origin responses.
- `session_idle_timeout`: Maximum time in seconds a session may remain idle
before it is terminated. The deadline is pushed forward on every
request. When None, sessions never expire from inactivity. Not
supported in stateless mode.
**Returns:**
- A Starlette application with StreamableHTTP support
## Classes
### `FastMCPStreamableHTTPSessionManager` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L43" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Session manager that scopes resumability storage per transport session.
**Methods:**
#### `event_store` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L68" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
event_store(self) -> EventStore | None
```
#### `event_store` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L76" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
event_store(self, event_store: EventStore | None) -> None
```
### `StreamableHTTPASGIApp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L80" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
ASGI application wrapper for Streamable HTTP server transport.
### `HostOriginGuardMiddleware` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L227" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Validate Host and Origin headers before requests reach MCP sessions.
### `StarletteWithLifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L348" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
**Methods:**
#### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L350" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
lifespan(self) -> Lifespan[Starlette]
```
### `RequestContextMiddleware` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/http.py#L363" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Middleware that stores each request in a ContextVar and sets transport type.

View file

@ -0,0 +1,101 @@
---
title: lifespan
sidebarTitle: lifespan
---
# `fastmcp.server.lifespan`
Composable lifespans for FastMCP servers.
This module provides a `@lifespan` decorator for creating composable server lifespans
that can be combined using the `|` operator.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.lifespan import lifespan
@lifespan
async def db_lifespan(server):
conn = await connect_db()
yield {"db": conn}
await conn.close()
@lifespan
async def cache_lifespan(server):
cache = await connect_cache()
yield {"cache": cache}
await cache.close()
mcp = FastMCP("server", lifespan=db_lifespan | cache_lifespan)
```
To compose with existing `@asynccontextmanager` lifespans, wrap them explicitly:
```python
from contextlib import asynccontextmanager
from fastmcp.server.lifespan import lifespan, ContextManagerLifespan
@asynccontextmanager
async def legacy_lifespan(server):
yield {"legacy": True}
@lifespan
async def new_lifespan(server):
yield {"new": True}
# Wrap the legacy lifespan explicitly
combined = ContextManagerLifespan(legacy_lifespan) | new_lifespan
```
## Functions
### `lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/lifespan.py#L172" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
lifespan(fn: LifespanFn) -> Lifespan
```
Decorator to create a composable lifespan.
Use this decorator on an async generator function to make it composable
with other lifespans using the `|` operator.
**Args:**
- `fn`: An async generator function that takes a FastMCP server and yields
a dict for the lifespan context.
**Returns:**
- A composable Lifespan wrapper.
## Classes
### `Lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/lifespan.py#L61" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Composable lifespan wrapper.
Wraps an async generator function and enables composition via the `|` operator.
The wrapped function should yield a dict that becomes part of the lifespan context.
### `ContextManagerLifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/lifespan.py#L110" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Lifespan wrapper for already-wrapped context manager functions.
Use this for functions already decorated with @asynccontextmanager.
### `ComposedLifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/lifespan.py#L137" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Two lifespans composed together.
Enters the left lifespan first, then the right. Exits in reverse order.
Results are shallow-merged into a single dict.

View file

@ -0,0 +1,106 @@
---
title: low_level
sidebarTitle: low_level
---
# `fastmcp.server.low_level`
## Functions
### `client_supports_extension` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L111" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
client_supports_extension(session: ServerSession, extension_id: str) -> bool
```
Check whether the connected client supports a given MCP extension.
Inspects the ``extensions`` capability on ``ClientCapabilities`` sent by the
client during initialization. In v2 the client's initialize params are
reachable via ``session.client_params``.
SDK v2 declares ``extensions`` as a real field on ``ClientCapabilities``, so
a client sending ``ClientCapabilities(extensions={...})`` populates the field
directly. We read that field first and fall back to ``model_extra`` only for
legacy-serialized clients that carried ``extensions`` as an extra key.
## Classes
### `FastMCPServerMiddleware` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L140" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Root dispatch for the FastMCP middleware chain, in the SDK's middleware layer.
v2 no longer lets FastMCP subclass ``ServerSession`` (the runner constructs
it per request), so the old ``MiddlewareServerSession._received_request``
override is replaced by a ``ServerMiddleware`` — an ordinary entry in the
SDK's own middleware list. Sitting at the root of dispatch, this
is the single entry point through which *every* inbound message flows —
requests, notifications, cancellations, ``initialize``, and even malformed or
unroutable messages the SDK can still hand us. It binds the FastMCP
request-context ContextVar and re-applies the app-scoped ``SharedContext`` for
the whole chain, then runs the FastMCP ``Middleware`` chain so
``on_message`` / ``on_request`` / ``on_notification`` observe the message.
Dispatch shapes:
- Negotiation runs the *whole* FastMCP chain here: ``initialize`` dispatches
through ``on_initialize`` and ``server/discover`` through ``on_discover``.
Neither has an interior FastMCP handler adapter, and the SDK serializes both
results before returning through its middleware seam, so this root adapter
restores core results to typed models before FastMCP middleware observes them.
- The component methods (``tools/call``, ``tools/list``, ``resources/read``,
...) still run their FastMCP chain *interior*, in the handler adapter, where
``on_call_tool`` receives the typed component result and a tool exception
propagates through ``on_message``/``on_request`` exactly where the built-in
error/logging/timing middleware expect it. The root dispatch does not re-run the
chain for these — it only steps in when such a request fails *before* the
interior runs (malformed params, routing), so ``on_message`` still observes
the failure.
- Every other message — all notifications (including ``notifications/cancelled``
and ``notifications/initialized``), ``ping``, ``logging/setLevel``, and any
unroutable/non-component request — has no interior FastMCP dispatch, so the
root dispatch runs the ``"outer"`` pass (``on_message`` plus
``on_request``/``on_notification``) here, wrapping the real SDK dispatch.
This closes the long-standing gap where these messages were invisible to
FastMCP middleware.
### `LowLevelServer` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L455" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
**Methods:**
#### `fastmcp` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L507" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
fastmcp(self) -> FastMCP
```
Get the FastMCP instance.
#### `create_initialization_options` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L514" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_initialization_options(self, notification_options: NotificationOptions | None = None, experimental_capabilities: dict[str, dict[str, Any]] | None = None, extensions: dict[str, dict[str, Any]] | None = None) -> InitializationOptions
```
#### `get_capabilities` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/low_level.py#L529" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_capabilities(self, notification_options: NotificationOptions | None = None, experimental_capabilities: dict[str, dict[str, Any]] | None = None, extensions: dict[str, dict[str, Any]] | None = None) -> mcp_types.ServerCapabilities
```
Override to advertise registered extensions and the MCP Apps UI extension.
``ServerCapabilities.extensions`` is a real declared field in v2, so we
update it directly. The
`FastMCP(experimental_capabilities=...)` merge also lives here rather
than in `create_initialization_options`: the modern `server/discover`
handler calls this directly, without going through
`create_initialization_options` at all, so merging there only reached
the handshake-era `initialize` response and silently dropped
constructor-configured experimental capabilities from `discover`.

View file

@ -0,0 +1,9 @@
---
title: mixins
sidebarTitle: mixins
---
# `fastmcp.server.mixins`
Server mixins for FastMCP.

View file

@ -0,0 +1,34 @@
---
title: providers
sidebarTitle: providers
---
# `fastmcp.server.providers`
Providers for dynamic MCP components.
This module provides the `Provider` abstraction for providing tools,
resources, and prompts dynamically at runtime.
Example:
```python
from fastmcp import FastMCP
from fastmcp.server.providers import Provider
from fastmcp.tools import Tool
class DatabaseProvider(Provider):
def __init__(self, db_url: str):
self.db = Database(db_url)
async def _list_tools(self) -> list[Tool]:
rows = await self.db.fetch("SELECT * FROM tools")
return [self._make_tool(row) for row in rows]
async def _get_tool(self, name: str) -> Tool | None:
row = await self.db.fetchone("SELECT * FROM tools WHERE name = ?", name)
return self._make_tool(row) if row else None
mcp = FastMCP("Server", providers=[DatabaseProvider(db_url)])
```

View file

@ -0,0 +1,891 @@
---
title: server
sidebarTitle: server
---
# `fastmcp.server.server`
FastMCP - A more ergonomic interface for MCP servers.
## Functions
### `default_lifespan` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L237" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]
```
Default lifespan context manager that does nothing.
**Args:**
- `server`: The server instance this lifespan is managing
**Returns:**
- An empty dictionary as the lifespan result.
### `create_proxy` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2501" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
create_proxy(target: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | SDKServer | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
```
Create a FastMCP proxy server for the given target.
This is the recommended way to create a proxy server. For lower-level control,
use `FastMCPProxy` or `ProxyProvider` directly from `fastmcp.server.providers.proxy`.
**Args:**
- `target`: The backend to proxy to. Can be\:
- A Client instance (connected or disconnected)
- A ClientTransport
- A FastMCP server instance
- A URL string or AnyUrl
- A Path to a server script
- An MCPConfig or dict
- `mode`: Protocol-era negotiation for auto-created proxy clients (a
non-Client target). By default (``None``) the backend MIRRORS the
front connection's negotiated era per request, so the whole chain
speaks one era end-to-end\: a modern front reaches a modern backend
(a guard tool's `InputRequiredResult` (SEP-2322) round-trips) and a
handshake front reaches a handshake backend (server-initiated
sampling / elicitation / roots push-forwarding works). Pass an
explicit mode (e.g. ``"auto"`` or a version string) to pin the
backend era regardless of the front; this overrides mirroring and is
appropriate when the backend only speaks one era. Ignored when
`target` is already a `Client` (which carries its own mode).
- `**settings`: Additional settings passed to FastMCPProxy (name, etc.)
**Returns:**
- A FastMCPProxy server that proxies to the target.
## Classes
### `StateValue` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L272" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
Wrapper for stored context state values.
### `FastMCP` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L278" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
**Methods:**
#### `name` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L507" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
name(self) -> str
```
#### `instructions` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L511" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
instructions(self) -> str | None
```
#### `instructions` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L515" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
instructions(self, value: str | None) -> None
```
#### `version` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L519" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
version(self) -> str | None
```
#### `website_url` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L523" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
website_url(self) -> str | None
```
#### `icons` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L527" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
icons(self) -> list[mcp_types.Icon]
```
#### `local_provider` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L534" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
local_provider(self) -> LocalProvider
```
The server's local provider, which stores directly-registered components.
Use this to remove components:
mcp.local_provider.remove_tool("my_tool")
mcp.local_provider.remove_resource("data://info")
mcp.local_provider.remove_prompt("my_prompt")
#### `add_middleware` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L598" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_middleware(self, middleware: Middleware) -> None
```
#### `add_extension` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L601" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_extension(self, extension: ServerExtension) -> None
```
Register a server extension (SEP-2133).
An extension contributes a negotiated capability, additive request
methods, a `tools/call` interceptor, and an optional lifespan — each
with access to FastMCP-level constructs (the component registry,
`Context`, auth scope). Its capability is advertised only while it is
registered.
The extension is bound to this server (so its handlers and interceptor
can reach it), its method bindings are wired onto the low-level server,
and it is recorded for capability advertisement, interception, and
lifespan entry. Registering two extensions with the same identifier is
an error, as is registering after the server's lifespan has started —
the extension's lifespan could no longer run, leaving it silently
half-active.
Extensions are served by the server they are registered on. A mounted
child's extensions do not propagate to the root: the root serves the
wire, so only root-registered extensions advertise capabilities and
answer methods (matching the lifespan, which also defers to the root).
Register extensions on the server you run.
#### `add_provider` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L673" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_provider(self, provider: Provider) -> None
```
Add a provider for dynamic tools, resources, and prompts.
Providers are queried in registration order. The first provider to return
a non-None result wins. Static components (registered via decorators)
always take precedence over providers.
**Args:**
- `provider`: A Provider instance that will provide components dynamically.
- `namespace`: Optional namespace prefix. When set\:
- Tools become "namespace_toolname"
- Resources become "protocol\://namespace/path"
- Prompts become "namespace_promptname"
#### `get_tasks` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L785" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tasks(self) -> Sequence[FastMCPComponent]
```
Get task-eligible components with all transforms applied.
Overrides AggregateProvider.get_tasks() to apply server-level transforms
after aggregation. AggregateProvider handles provider-level namespacing.
#### `add_transform` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L814" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_transform(self, transform: Transform) -> None
```
Add a server-level transform.
Server-level transforms are applied after all providers are aggregated.
They transform tools, resources, and prompts from ALL providers.
**Args:**
- `transform`: The transform to add.
#### `list_tools` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L834" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_tools(self) -> Sequence[Tool]
```
List all enabled tools from providers.
Overrides Provider.list_tools() to add enabled filtering, auth filtering,
and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
#### `get_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L917" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_tool(self, name: str, version: VersionSpec | None = None) -> Tool | None
```
Get a tool by name, filtering disabled tools.
Overrides Provider.get_tool() to filter disabled tools after all
transforms (including session-level) have been applied. This ensures
session transforms can override provider-level disables.
When the highest version is disabled and no explicit version was
requested, falls back to the next-highest enabled version.
**Args:**
- `name`: The tool name.
- `version`: Version filter (None returns highest version).
**Returns:**
- The tool if found and enabled, None otherwise.
#### `list_resources` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L971" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resources(self) -> Sequence[Resource]
```
List all enabled resources from providers.
Overrides Provider.list_resources() to add visibility filtering, auth filtering,
and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
#### `get_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1056" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_resource(self, uri: str, version: VersionSpec | None = None) -> Resource | None
```
Get a resource by URI, filtering disabled resources.
Overrides Provider.get_resource() to add visibility filtering after all
transforms (including session-level) have been applied.
When the highest version is disabled and no explicit version was
requested, falls back to the next-highest enabled version.
**Args:**
- `uri`: The resource URI.
- `version`: Version filter (None returns highest version).
**Returns:**
- The resource if found and enabled, None otherwise.
#### `list_resource_templates` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1106" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_resource_templates(self) -> Sequence[ResourceTemplate]
```
List all enabled resource templates from providers.
Overrides Provider.list_resource_templates() to add visibility filtering,
auth filtering, and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
#### `get_resource_template` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1188" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_resource_template(self, uri: str, version: VersionSpec | None = None) -> ResourceTemplate | None
```
Get a resource template by URI, filtering disabled templates.
Overrides Provider.get_resource_template() to add visibility filtering after
all transforms (including session-level) have been applied.
When the highest version is disabled and no explicit version was
requested, falls back to the next-highest enabled version.
**Args:**
- `uri`: The template URI.
- `version`: Version filter (None returns highest version).
**Returns:**
- The template if found and enabled, None otherwise.
#### `list_prompts` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1242" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
list_prompts(self) -> Sequence[Prompt]
```
List all enabled prompts from providers.
Overrides Provider.list_prompts() to add visibility filtering, auth filtering,
and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
#### `get_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1314" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
get_prompt(self, name: str, version: VersionSpec | None = None) -> Prompt | None
```
Get a prompt by name, filtering disabled prompts.
Overrides Provider.get_prompt() to add visibility filtering after all
transforms (including session-level) have been applied.
When the highest version is disabled and no explicit version was
requested, falls back to the next-highest enabled version.
**Args:**
- `name`: The prompt name.
- `version`: Version filter (None returns highest version).
**Returns:**
- The prompt if found and enabled, None otherwise.
#### `call_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1364" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult
```
Call a tool by name.
This is the public API for executing tools. By default, middleware is applied.
**Args:**
- `name`: The tool name
- `arguments`: Tool arguments (optional)
- `version`: Specific version to call. If None, calls highest version.
- `run_middleware`: If True (default), apply the middleware chain.
Set to False when called from middleware to avoid re-applying.
**Returns:**
- ToolResult.
A guard tool that requests client input (SEP-2322 multi-round-trip)
returns an ``InputRequiredToolResult`` (a ``ToolResult`` subclass); it
flows back through the middleware chain as an ordinary result and the
wire handler unwraps it into an ``InputRequiredResult`` on the response.
**Raises:**
- `NotFoundError`: If tool not found or disabled
- `ToolError`: If tool execution fails
- `ValidationError`: If arguments fail validation
#### `read_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1557" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
read_resource(self, uri: str) -> ResourceResult
```
Read a resource by URI.
This is the public API for reading resources. By default, middleware is applied.
Checks concrete resources first, then templates.
**Args:**
- `uri`: The resource URI
- `version`: Specific version to read. If None, reads highest version.
- `run_middleware`: If True (default), apply the middleware chain.
Set to False when called from middleware to avoid re-applying.
**Returns:**
- ResourceResult.
**Raises:**
- `NotFoundError`: If resource not found or disabled
- `ResourceError`: If resource read fails
#### `render_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1715" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult
```
Render a prompt by name.
This is the public API for rendering prompts. By default, middleware is applied.
Use get_prompt() to retrieve the prompt definition without rendering.
**Args:**
- `name`: The prompt name
- `arguments`: Prompt arguments (optional)
- `version`: Specific version to render. If None, renders highest version.
- `run_middleware`: If True (default), apply the middleware chain.
Set to False when called from middleware to avoid re-applying.
**Returns:**
- PromptResult.
**Raises:**
- `NotFoundError`: If prompt not found or disabled
- `PromptError`: If prompt rendering fails
#### `add_tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1795" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_tool(self, tool: Tool | Callable[..., Any]) -> Tool
```
Add a tool to the server.
The tool function can optionally request a Context object by adding a parameter
with the Context type annotation. See the @tool decorator for examples.
**Args:**
- `tool`: The Tool instance or @tool-decorated function to register
**Returns:**
- The tool instance that was added to the server.
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1810" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: F) -> F
```
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1831" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: str | None = None) -> Callable[[F], F]
```
#### `tool` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1851" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
tool(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]
```
Decorator to register a tool.
Tools can optionally request a Context object by adding a parameter with the
Context type annotation. The context provides access to MCP capabilities like
logging, progress reporting, and resource access.
This decorator supports multiple calling patterns:
- @server.tool (without parentheses)
- @server.tool (with empty parentheses)
- @server.tool("custom_name") (with name as first argument)
- @server.tool(name="custom_name") (with name as keyword argument)
- server.tool(function, name="custom_name") (direct function call)
**Args:**
- `name_or_fn`: Either a function (when used as @tool), a string name, or None
- `name`: Optional name for the tool (keyword-only, alternative to name_or_fn)
- `description`: Optional description of what the tool does
- `tags`: Optional set of tags for categorizing the tool
- `output_schema`: Optional JSON schema for the tool's output
- `annotations`: Optional annotations about the tool's behavior
- `meta`: Optional meta information about the tool
**Examples:**
Register a tool with a custom name:
```python
@server.tool
def my_tool(x: int) -> str:
return str(x)
# Register a tool with a custom name
@server.tool
def my_tool(x: int) -> str:
return str(x)
@server.tool("custom_name")
def my_tool(x: int) -> str:
return str(x)
@server.tool(name="custom_name")
def my_tool(x: int) -> str:
return str(x)
# Direct function call
server.tool(my_function, name="custom_name")
```
#### `add_resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1948" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_resource(self, resource: Resource | Callable[..., Any]) -> Resource | ResourceTemplate
```
Add a resource to the server.
**Args:**
- `resource`: A Resource instance or @resource-decorated function to add
**Returns:**
- The resource instance that was added to the server.
#### `add_template` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1961" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_template(self, template: ResourceTemplate) -> ResourceTemplate
```
Add a resource template to the server.
**Args:**
- `template`: A ResourceTemplate instance to add
**Returns:**
- The template instance that was added to the server.
#### `resource` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L1972" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
resource(self, uri: str) -> Callable[[F], F]
```
Decorator to register a function as a resource.
The function will be called when the resource is read to generate its content.
The function can return:
- str for text content
- bytes for binary content
- other types will be converted to JSON
Resources can optionally request a Context object by adding a parameter with the
Context type annotation. The context provides access to MCP capabilities like
logging, progress reporting, and session information.
If the URI contains parameters (e.g. "resource://{param}") or the function
has parameters, it will be registered as a template resource.
**Args:**
- `uri`: URI for the resource (e.g. "resource\://my-resource" or "resource\://{param}")
- `name`: Optional name for the resource
- `description`: Optional description of the resource
- `mime_type`: Optional MIME type for the resource
- `tags`: Optional set of tags for categorizing the resource
- `annotations`: Optional annotations about the resource's behavior
- `meta`: Optional meta information about the resource
**Examples:**
Register a resource with a custom name:
```python
@server.resource("resource://my-resource")
def get_data() -> str:
return "Hello, world!"
@server.resource("resource://my-resource")
async get_data() -> str:
data = await fetch_data()
return f"Hello, world! {data}"
@server.resource("resource://{city}/weather")
def get_weather(city: str) -> str:
return f"Weather for {city}"
@server.resource("resource://{city}/weather")
async def get_weather_with_context(city: str, ctx: Context) -> str:
await ctx.info(f"Fetching weather for {city}")
return f"Weather for {city}"
@server.resource("resource://{city}/weather")
async def get_weather(city: str) -> str:
data = await fetch_weather(city)
return f"Weather for {city}: {data}"
```
#### `add_prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2091" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_prompt(self, prompt: Prompt | Callable[..., Any]) -> Prompt
```
Add a prompt to the server.
**Args:**
- `prompt`: A Prompt instance or @prompt-decorated function to add
**Returns:**
- The prompt instance that was added to the server.
#### `prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2103" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prompt(self, name_or_fn: F) -> F
```
#### `prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2118" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prompt(self, name_or_fn: str | None = None) -> Callable[[F], F]
```
#### `prompt` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2132" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
prompt(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt | partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt]
```
Decorator to register a prompt.
Prompts can optionally request a Context object by adding a parameter with the
Context type annotation. The context provides access to MCP capabilities like
logging, progress reporting, and session information.
This decorator supports multiple calling patterns:
- @server.prompt (without parentheses)
- @server.prompt() (with empty parentheses)
- @server.prompt("custom_name") (with name as first argument)
- @server.prompt(name="custom_name") (with name as keyword argument)
- server.prompt(function, name="custom_name") (direct function call)
Args:
name_or_fn: Either a function (when used as @prompt), a string name, or None
name: Optional name for the prompt (keyword-only, alternative to name_or_fn)
description: Optional description of what the prompt does
tags: Optional set of tags for categorizing the prompt
meta: Optional meta information about the prompt
Examples:
```python
@server.prompt
def analyze_table(table_name: str) -> list[Message]:
schema = read_table_schema(table_name)
return [
{
"role": "user",
"content": f"Analyze this schema:
{schema}"
}
]
@server.prompt()
async def analyze_with_context(table_name: str, ctx: Context) -> list[Message]:
await ctx.info(f"Analyzing table {table_name}")
schema = read_table_schema(table_name)
return [
{
"role": "user",
"content": f"Analyze this schema:
{schema}"
}
]
@server.prompt("custom_name")
async def analyze_file(path: str) -> list[Message]:
content = await read_file(path)
return [
{
"role": "user",
"content": {
"type": "resource",
"resource": {
"uri": f"file://{path}",
"text": content
}
}
}
]
@server.prompt(name="custom_name")
def another_prompt(data: str) -> list[Message]:
return [{"role": "user", "content": data}]
# Direct function call
server.prompt(my_function, name="custom_name")
```
#### `add_completion_handler` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2230" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
add_completion_handler(self, handler: CompletionHandler) -> None
```
Register the server's argument-completion handler.
A server has a single completion handler that answers every
`completion/complete` request, switching on the reference (a prompt or
resource template) and the argument being completed. Registering it also
registers the low-level `completion/complete` handler, which is what
makes the SDK declare the completions capability — so the capability is
advertised exactly when the server can answer. Calling this again
replaces the handler.
**Args:**
- `handler`: A callable taking the reference, the
`CompletionArgument`, and the optional `CompletionContext`, and
returning candidate values (a `Completion`, a list of strings,
or None). May be sync or async.
#### `completion` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2251" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
completion(self, handler: CompletionHandler) -> CompletionHandler
```
#### `completion` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2254" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
completion(self) -> Callable[[CompletionHandler], CompletionHandler]
```
#### `completion` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2258" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
completion(self, handler: CompletionHandler | None = None) -> CompletionHandler | Callable[[CompletionHandler], CompletionHandler]
```
Decorator to register the server's argument-completion handler.
The handler answers `completion/complete` requests for prompt arguments
and resource-template parameters. It receives the reference being
completed, the argument (its name and the partial value typed so far),
and the context of arguments already supplied, and returns candidate
values. Return a list of strings, a `Completion` (to include pagination
hints), or None when the reference/argument is not one it handles — an
unhandled reference yields an empty completion, not an error.
Registering a handler declares the completions capability; a server with
none does not advertise it. This works identically on the handshake and
modern protocol eras.
Supports both `@mcp.completion` and `@mcp.completion()`.
Example:
```python
from fastmcp import FastMCP
from mcp_types import Completion, PromptReference
mcp = FastMCP("Completion Server")
@mcp.prompt
def poem(theme: str) -> str:
return f"Write a poem about {theme}"
@mcp.completion
def complete(ref, argument, context):
if isinstance(ref, PromptReference) and ref.name == "poem":
if argument.name == "theme":
options = ["nature", "love", "adventure"]
return [o for o in options if o.startswith(argument.value)]
return None
```
#### `mount` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2308" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
mount(self, server: FastMCP[LifespanResultT], namespace: str | None = None, tool_names: dict[str, str] | None = None) -> None
```
Mount another FastMCP server on this server with an optional namespace.
Mounting establishes a dynamic connection between servers. When a client
interacts with a mounted server's objects through the parent server, requests
are forwarded to the mounted server in real-time. This means changes to the
mounted server are immediately reflected when accessed through the parent.
When a server is mounted with a namespace:
- Tools from the mounted server are accessible with namespaced names.
Example: If server has a tool named "get_weather", it will be available as "namespace_get_weather".
- Resources are accessible with namespaced URIs.
Example: If server has a resource with URI "weather://forecast", it will be available as
"weather://namespace/forecast".
- Templates are accessible with namespaced URI templates.
Example: If server has a template with URI "weather://location/{id}", it will be available
as "weather://namespace/location/{id}".
- Prompts are accessible with namespaced names.
Example: If server has a prompt named "weather_prompt", it will be available as
"namespace_weather_prompt".
When a server is mounted without a namespace (namespace=None), its tools, resources, templates,
and prompts are accessible with their original names. Multiple servers can be mounted
without namespaces, and they will be tried in order until a match is found.
The mounted server's lifespan is executed when the parent server starts, and its
middleware chain is invoked for all operations (tool calls, resource reads, prompts).
**Args:**
- `server`: The FastMCP server to mount.
- `namespace`: Optional namespace to use for the mounted server's objects. If None,
the server's objects are accessible with their original names.
- `tool_names`: Optional mapping of original tool names to custom names. Use this
to override namespaced names. Keys are the original tool names from the
mounted server.
#### `from_openapi` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2379" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_openapi(cls, openapi_spec: dict[str, Any], client: httpx2.AsyncClient | None = None, name: str = 'OpenAPI Server', route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, validate_output: bool = True, **settings: Any) -> Self
```
Create a FastMCP server from an OpenAPI specification.
**Args:**
- `openapi_spec`: OpenAPI schema as a dictionary
- `client`: Optional httpx2 AsyncClient for making HTTP requests.
If not provided, a default client is created using the first
server URL from the OpenAPI spec with a 30-second timeout.
Legacy httpx clients are temporarily accepted with a deprecation
warning.
- `name`: Name for the MCP server
- `route_maps`: Optional list of RouteMap objects defining route mappings
- `route_map_fn`: Optional callable for advanced route type mapping
- `mcp_component_fn`: Optional callable for component customization
- `mcp_names`: Optional dictionary mapping operationId to component names
- `tags`: Optional set of tags to add to all components
- `validate_output`: If True (default), tools use the output schema
extracted from the OpenAPI spec for response validation. If
False, a permissive schema is used instead, allowing any
response structure while still returning structured JSON.
- `**settings`: Additional settings passed to FastMCP
**Returns:**
- A FastMCP server with an OpenAPIProvider attached.
#### `from_fastapi` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2432" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, httpx_client_kwargs: dict[str, Any] | None = None, tags: set[str] | None = None, **settings: Any) -> Self
```
Create a FastMCP server from a FastAPI application.
**Args:**
- `app`: FastAPI application instance
- `name`: Name for the MCP server (defaults to app.title)
- `route_maps`: Optional list of RouteMap objects defining route mappings
- `route_map_fn`: Optional callable for advanced route type mapping
- `mcp_component_fn`: Optional callable for component customization
- `mcp_names`: Optional dictionary mapping operationId to component names
- `httpx_client_kwargs`: Optional kwargs passed to httpx2.AsyncClient.
Use this to configure timeout and other client settings.
- `tags`: Optional set of tags to add to all components
- `**settings`: Additional settings passed to FastMCP
**Returns:**
- A FastMCP server with an OpenAPIProvider attached.
#### `generate_name` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/server.py#L2487" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
generate_name(cls, name: str | None = None) -> str
```

View file

@ -0,0 +1,31 @@
---
title: session_scoped_event_store
sidebarTitle: session_scoped_event_store
---
# `fastmcp.server.session_scoped_event_store`
Lightweight session scoping for Streamable HTTP event stores.
## Classes
### `SessionScopedEventStore` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/session_scoped_event_store.py#L19" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
EventStore adapter that isolates stream IDs to one transport session.
**Methods:**
#### `store_event` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/session_scoped_event_store.py#L34" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId
```
#### `replay_events_after` <sup><a href="https://github.com/PrefectHQ/fastmcp/blob/main/fastmcp_slim/fastmcp/server/session_scoped_event_store.py#L41" target="_blank"><Icon icon="github" style="width: 14px; height: 14px;" /></a></sup>
```python
replay_events_after(self, last_event_id: EventId, send_callback: EventCallback) -> StreamId | None
```

Some files were not shown because too many files have changed in this diff Show more