Compare commits

..

1,510 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
c33a3c3b29
Only fail triage when a granted tool is denied, and grant get_pull_request (#4562)
The guard failed on any denial, so an agent falling back to an unlisted tool
during a GitHub outage tripped it — and the error blamed the allowlist, which
was intact. It now fails only when a command the workflow actually grants is
refused, which is the signal that a pattern was mangled.

mcp__github__get_pull_request was never granted, so on a PR the agent could
only read via get_issue and reached for denied fallbacks when that failed.
2026-07-20 10:51:42 -04:00
Jeremiah Lowin
b0e782a2ee
Make the unit suite fast: in-process HTTP tests, no real sleeps, parallel Windows CI (#4554) 2026-07-20 10:51:14 -04:00
Marcelo Trylesinski
7934124fb5
Make transformed tool required order deterministic (#4564)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-20 07:12:45 -05: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
Jeremiah Lowin
33ed688a8f
Bump pinned Claude models to current versions (#4561) 2026-07-19 21:09:43 -04:00
Jeremiah Lowin
2676864163
Fix AI workflow allowlists being destroyed by tokenization (#4560) 2026-07-19 20:46:29 -04:00
Jeremiah Lowin
c6e31a3be6
Rename martian workflows to marvin (#4558) 2026-07-19 20:46:02 -04:00
Jeremiah Lowin
a3163bc275
Add 'prs welcome' label to waive the PR assignment gate (#4557)
* Add 'prs welcome' label to waive the PR assignment gate

Also documents contributor accountability, maintainer edit access, and
branch targeting in CONTRIBUTING.

* Protect 'prs welcome' from prompt-injected triage labeling
2026-07-19 20:37:40 -04:00
Jeremiah Lowin
3213776b25
Run FastMCP middleware for every inbound message (#4553)
* Make the SDK seam the root of FastMCP middleware dispatch (D3)

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

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

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

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

* Replace 'seam' language with plain dispatch terminology

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

* Cover fires-once across an MRTR continuation round

* Align cherry-picked coverage test with renamed recorder

* Rewrite only the message, never the dispatch destination
2026-07-19 20:29:08 -04:00
Jeremiah Lowin
cef327d0f2
Fix label triage applying no labels, and make blocked tool calls fail (#4555) 2026-07-19 19:24:20 -04:00
Jeremiah Lowin
1e529ee27d
Stop proxies from validating backend results or mutating shared transports (#4552) 2026-07-19 19:24:11 -04:00
Jeremiah Lowin
b9b1deacb6
Speed up the unit test suite, and fix the task-notification race it surfaced (#4550) 2026-07-19 18:52:04 -04:00
Jeremiah Lowin
eee5e91334
Fix stale MRTR/elicitation framing in client and upgrade docs (#4551) 2026-07-19 18:10:15 -04:00
Jeremiah Lowin
717f3535f6
Add guard-mode multi-round-trip tools (SEP-2322) (#4544)
* Add guard-mode MRTR server support (SEP-2322)

* Add server-side MRTR guard tests

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

* Apply formatting to MRTR guard changes

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

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

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

* Recognize InputRequiredResult inside Annotated union arms

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

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

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

* Transport MRTR asks as InputRequiredToolResult, not a raised signal

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

* Document MRTR middleware interaction and the isinstance pattern

* Update MRTR change-register verify note to InputRequiredToolResult

* Align test module docstring with result-cycle framing

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

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

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

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

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

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

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

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

* Wrap raw InputRequiredResult returned by a transform_fn

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

* Reject input-required results from background tasks

* Unwrap type aliases before stripping guard arms

* Apply ruff format

* Recursively strip guard arms through nested and composed aliases

* Reflect MRTR continuation fields on the middleware message

* Suppress output schema for InputRequiredResult subclasses

* Forward progress on modern proxy tool calls

* Suppress output schema for bare aliased guard returns

* Suppress output schema for any surviving guard return wrapping
2026-07-19 16:42:06 -04:00
marvin-context-protocol[bot]
0781e723c2
chore: Update SDK documentation (#4442)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-07-19 15:06:18 -04:00
dependabot[bot]
87fa361239
chore(deps): bump actions/setup-node from 6 to 7 (#4546)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6 to 7.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-19 15:06:07 -04:00
Bill Easton
3041aa241e
Align client, Apps, and integration docs (#4261)
* docs: align client and integration docs

Generated with Codex.

* docs: align Descope local URL

Generated with Codex.

* docs: load .env explicitly in Descope setup

* docs: load .env in Scalekit setup, guard non-mapping logging extra

* docs: handle null response_type in elicitation template, qualify STDIO env allowlist by platform

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-19 14:30:51 -04:00
Bill Easton
149a7aa2ce
Align CLI, deployment, and config docs (#4259)
* docs: align CLI and deployment docs

Generated with Codex.

* docs: restore install config support, fix CIMD placeholder, add missing CLI flags

* docs: restore contrib guidance, correct --copy availability

* docs: remove dead redirect-shadowed pages

* Fix stale --path default in run command help

* docs: correct Goose flag support, fix README link to moved testing page

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-19 14:24:07 -04:00
Bill Easton
d3b7922615
Align server component docs (#4260)
* docs: align server component docs

Generated with Codex.

* docs: clarify resource return shapes

Generated with Codex.

* docs: clarify initialize middleware response

Generated with Codex.

* docs: lead visibility filtering with names, scope keys to version targeting

* docs: correct initialize result semantics, template mime type, docket scope, visibility tip

* Warn when a visibility key omits the @ version delimiter

* Honor a resource template's declared mime_type and meta on read

* Strip internal visibility meta from resource content; document filter intersection

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-19 14:23:36 -04:00
Jeremiah Lowin
67e8448389
[codex] Add OAuthProxy RFC 9207 issuer responses (#4438)
* Add OAuthProxy issuer response parameter

* Cover OAuthProxy issuer error redirects

* Relax host origin guard defaults (#4439)

* Use exact issuer in authorize errors

* Restore HTTP host guard compatibility (#4472)

* Hugging Face Auth Integration (#4385)

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>

* Docs: add v3.4.4 changelog entries (#4473)

* Explain unnormalized issuer; cover consent-denial path base_url

* Revert "Merge remote-tracking branch 'origin/release/3.x' into codex/oauth-proxy-rfc9207-issuer"

This reverts commit 9e34b1686c, reversing
changes made to 640dc60fe0.

* Preserve callback query bytes when appending iss/code/state params

add_query_params previously decoded the existing query with parse_qsl
and re-encoded it, mutating opaque or signed query strings (a valueless
?flag became ?flag=, non-UTF-8 percent-encoded bytes got replaced).
Append the newly-encoded params to the existing query string instead of
round-tripping it through parse/encode.

Also fixes a stray bare `httpx` reference in a test that should use
httpx2 following the SDK v2 migration.

* Attach RFC 9207 iss to authorize() success redirects too

AuthorizationHandler only added iss to error redirects from the SDK's
base handler, not to code redirects returned directly by authorize()
overrides that bypass consent/upstream (as GitHub's mocked test does).
Since metadata now unconditionally advertises
authorization_response_iss_parameter_supported, any client-facing
redirect missing iss hard-fails RFC 9207-aware clients.

Also fixes HeadlessOAuth, which parsed code/state from the redirect
but silently dropped iss, so the same regression would have masked
itself across every other provider integration test too.

* Carry RFC 9207 iss through the production OAuth callback path

OAuthProxy advertises authorization_response_iss_parameter_supported and
sends iss on every authorization redirect, but the client's production
callback chain (CallbackResponse -> OAuthCallbackResult -> OAuth.callback_handler)
had no iss field, so it was silently dropped and the SDK's
validate_authorization_response_iss rejected the callback. HeadlessOAuth
already carried iss through, which is why CI stayed green while real
clients failed.

Add iss to CallbackResponse and OAuthCallbackResult, thread it through
store_result_once for both success and error branches, and pass it into
AuthorizationCodeResult in OAuth.callback_handler.

* Don't duplicate iss when a provider redirect already carries one

* Consolidate RFC 9207 iss handling into a single redirect helper

Every client-facing authorization redirect must carry exactly one iss.
That invariant was being enforced by hand at five separate call sites,
each building its own params dict -- which is how the success-redirect
path shipped without iss in the first place, and how a registered
redirect_uri that already carries its own iss could end up duplicated.
Route all five sites through build_client_redirect(), which owns the
idempotent replace-or-append behavior so no caller can get it wrong.

---------

Co-authored-by: shaun smith <1936278+evalstate@users.noreply.github.com>
2026-07-19 09:52:43 -04:00
Jeremiah Lowin
252a29e5e6
Preserve telemetry attributes when a sampler does not forward them (#4539)
* Reapply span attributes after creation to survive non-forwarding samplers

Tracer.start_span builds the span from sampling_result.attributes, not
the attributes kwarg — a custom Sampler that returns
SamplingResult(RECORD_AND_SAMPLE) without forwarding attributes
silently drops everything FastMCP passed at creation time. Reapply the
same attributes immediately after span creation (guarded by
is_recording()) so on_start hooks and samplers still see them, while
the finished span is guaranteed to carry FastMCP's telemetry
regardless of sampler behavior.

* Restore only missing span attributes, not a blanket reapply

Reapplying all attributes after span creation overwrote values a
sampler deliberately set (e.g. a redacted mcp.method.name) and
inflated dropped-attribute counts when the SDK's attribute limit was
hit. Compare against the span's existing attributes and restore only
the keys a non-forwarding sampler actually dropped, via a shared
restore_missing_attributes() helper in fastmcp.telemetry.

* Gate attribute restore on all-or-nothing, not per-key

Restoring only missing keys reinserted attributes the SDK's bounded
attribute map had already evicted under a low
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, evicting a different retained key and
inflating dropped_attributes beyond what the sampler actually dropped.
Gate on none of our attributes being present (plus dropped_attributes
== 0) instead — the regression this exists to fix is a sampler
dropping everything, and eviction under a limit always leaves some.
Renamed restore_missing_attributes to restore_dropped_attributes to
match.

* Gate attribute restore on empty span, not per-key presence

A sampler that intentionally supplies only its own attributes (e.g. to
strip component names or resource URIs for privacy/cardinality
control) left none of FastMCP's keys on the span, so the previous
all-or-nothing gate treated it identically to a bare non-forwarding
sampler and restored everything, defeating the filter. Key off the
span having no attributes at all instead — a bare sampler leaves it
empty, a filtering sampler doesn't.
2026-07-19 09:00:04 -04:00
Alexander Savchuk
2899ffb6f3
Fix #4292: SSRF guard breaks OAuth/JWKS fetches behind a corporate HTTP proxy (#4412)
* Add FASTMCP_SSRF_TRUST_PROXY to allow SSRF fetches through a corporate proxy

🤖 Generated with Claude Code

* Make SSRF fetch client trust_env explicit for proxy routing

🤖 Generated with Claude Code

* Warn when SSRF proxy trust is enabled without a configured proxy

🤖 Generated with Claude Code

* Warn when NO_PROXY would send an SSRF-trust-proxy fetch direct

🤖 Generated with Claude Code

* Refuse SSRF-trust-proxy fetches when no proxy would route the target

🤖 Generated with Claude Code

* Fix TestProxyMode mocks to patch httpx2.AsyncClient

main's httpx -> httpx2 migration (#4503) landed after these tests were
written; ssrf.py's fetch path already uses httpx2.AsyncClient, but
TestProxyMode still patched the old httpx module, so the mock silently
stopped intercepting and requests escaped to the real network.

* Fix port-qualified NO_PROXY bypass in SSRF proxy-trust guard

proxy_bypass(hostname) discarded the port, so a NO_PROXY entry like
127.0.0.1:8443 went undetected while httpx2 honored it and sent the
request direct with the blocklist already disabled. Pass host:port
instead, except for IPv6 literals, where httpx2 ignores port when
matching NO_PROXY and neither bracketed nor unbracketed host:port
reliably matches through proxy_bypass()'s own parser.

* Replace NO_PROXY prediction with explicit proxy control in SSRF trust-proxy mode

Predicting httpx2's proxy routing (via proxy_bypass(), then via httpx2's own
get_environment_proxies()/URLPattern internals) kept diverging from its real
NO_PROXY handling — three rounds, three different divergences, always in the
unsafe direction. Read HTTPS_PROXY/ALL_PROXY directly and pass it to httpx2
explicitly with trust_env=False, so the request provably goes through that
proxy instead of being predicted to. NO_PROXY is no longer evaluated in this
mode: a NO_PROXY'd host is now routed through the proxy rather than refused,
since that's strictly safer than the alternative (direct with the blocklist
already off).

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-18 21:42:52 -04:00
Jeremiah Lowin
cee327d8f7
Restore Mintlify's fixed banner positioning (#4542) 2026-07-18 21:25:39 -04:00
Viktor Szépe
81fada4922
Fix typos (#4498)
* Fix typos

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

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

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

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

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

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-18 21:18:39 -04:00
Pierre Audonnet
a57f1c8b20
Add subject field to AccessToken initialization (#4267)
* Add subject field to AccessToken initialization

Fixes #4266

Add `subject` property to the AccessToken.

```python
        return AccessToken(
            token=access_token_as_dict["token"],
            client_id=access_token_as_dict["client_id"],
            scopes=access_token_as_dict["scopes"],
            subject=access_token_as_dict["subject"],
            # Optional fields
            expires_at=access_token_as_dict.get("expires_at"),
            resource=access_token_as_dict.get("resource"),
            claims=access_token_as_dict.get("claims") or {},
        )
```

* Populate AccessToken.subject across all token verifiers

Closes #4266. get_access_token().subject was always None: the SDK's
AccessToken.subject wasn't carried into FastMCP's AccessToken by the
dependency-layer conversion, and none of the built-in TokenVerifiers
(JWT, introspection, and the OAuth-provider verifiers for Discord,
Clerk, Google, WorkOS, HuggingFace, GitHub, and Cognito) populated it
from the sub claim/field they already extract.

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-18 21:15:13 -04:00
vijaydeepsinha
654335607c
Fix-issue-4284 : Add Auth0MCPProvider for Auth0 Auth for MCP (#4411)
* Add Auth0MCPProvider for Auth0 Auth for MCP

* Document Auth0 MCP provider integration

* Add Auth0MCPProvider scope and auth rejection tests

Cover permissions-based required_scopes enforcement and unauthenticated MCP 401 responses.

* fixed documentation

* Narrow Auth0 docs to integration guide only

* Fix Auth0 provider: use httpx2 instead of httpx

httpx is a dev-only transitive dependency in this repo; runtime installs
declare httpx2 exclusively. The module-level 'import httpx' in auth0.py
broke import on a clean install of fastmcp or fastmcp-slim[server].

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-18 21:15:02 -04:00
Jeremiah Lowin
62d32c6a90
Add 4.0.0 version badge to Path Security section (#4540) 2026-07-18 20:58:32 -04:00
Jeremiah Lowin
2ebf19e5e1
Make examples/ actually trigger the ty gate (#4541)
#4466 added examples/ to [tool.ty.src] include, but two filters still
excluded it: the ty prek hook's files: scope and the static-analysis
workflow's push path triggers. An examples-only commit or direct push
to main could skip the gate entirely.
2026-07-18 20:58:24 -04:00
Jeremiah Lowin
83702a41f8
Document icon theme support (#4537)
* Document icon theme support and add round-trip tests

MCP SDK v2 added a `theme` field to `Icon` (light/dark), letting a
server ship complementary icon variants for clients that render in
different UI themes. Document the field on the icons page and cover
it with round-trip tests through the server/client protocol.

* docs: add 4.0.0 version badge to Theme Variants section
2026-07-18 20:57:36 -04:00
Jeremiah Lowin
243f054f65
Include scopes in auth challenges (#4527) 2026-07-18 20:53:52 -04:00
Jeremiah Lowin
998b37f32b
Add server-side identity assertion (SEP-990 ID-JAG) (#4483)
* Add server-side SEP-990 identity assertion (ID-JAG)

* Test SEP-990 identity assertion token endpoint

* Format identity assertion test

* Document SEP-990 identity assertion

* Thread identity_assertion through OIDCProxy

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- IdentityAssertion re-exported lazily from server.auth (the eager import
  bypassed the package's documented lazy-import boundary)
- Accept the ID-JAG aud both with and without base_url's trailing slash;
  metadata advertises the slashed form, so IdPs echoing it verbatim work
- algorithms={issuer: alg} per-issuer override, mirroring jwks_uris
- OIDC discovery serializes per-issuer and backs off 30s after a failure
  (discovery runs pre-signature, so garbage could amplify into HTTP floods)
2026-07-18 19:52:15 -04:00
nate nowack
f018f68bbf
Expose telemetry attributes on span start (#4487)
* Expose telemetry attributes on span start

🤖 Generated with Codex

* Expose sampling attributes on span start

🤖 Generated with Codex

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-18 19:46:19 -04:00
Jeremiah Lowin
7d76c9d055
Add examples/ to the ty static-analysis gate (#4466)
* Add examples/ to ty static-analysis gate

* Fix example type errors and stale SDK idioms for ty

* Use typing_extensions.TypedDict for the quiz tool-param type

Question is a take_quiz parameter, so FastMCP builds a Pydantic schema
for it; typing.TypedDict raises PydanticUserError on Python 3.10/3.11
(only 3.12+ accepts it). ty and 3.12 runs miss this, so it slipped in.

* Guard get_access_token() None case in huggingface_oauth example

Caught by the ty gate this PR adds: the example, merged separately,
had never been type-checked against examples/. Matches the existing
aws_oauth/keycloak_oauth pattern.

* Print actual YAML text in custom serializer example
2026-07-18 19:44:13 -04:00
Kevin J Gao
3fdeedb567
Improve DescopeProvider scope discovery and well-known URL support (#4489)
* Improve DescopeProvider scope discovery and well-known URL support

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify DescopeProvider scope and URL handling

Co-authored-by: Cursor <cursoragent@cursor.com>

* Make DescopeProvider scope discovery async and lazy

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🤖 Generated with Claude Code

* Use generic scope in Descope tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🤖 Generated with Claude Code

* Address Descope discovery edge cases

* Deduplicate Descope metadata fallback

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-07-18 16:18:45 -04:00
dependabot[bot]
c241cd4698
chore(deps): bump mcp from 1.26.0 to 1.27.2 in /examples/testing_demo in the uv group across 1 directory (#4514)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-18 15:49:30 -04:00
Jeremiah Lowin
7e077186fc
Clean up task sessions on connection exit (#4535) 2026-07-18 15:45:11 -04:00
Jeremiah Lowin
16383a64d6
Preserve component metadata in response cache (#4521) 2026-07-18 15:42:20 -04:00
Jeremiah Lowin
00cab8ba8f
Fix docs banner contrast (#4522)
* Fix docs banner contrast

* Banner: readable animated brand-rainbow in both themes
2026-07-18 15:38:03 -04:00
Jeremiah Lowin
d7eda92a2b
Fix OAuth request annotation (#4534) 2026-07-18 15:29:32 -04:00
Jeremiah Lowin
981a69d839
Handle expired OAuth client registrations (#4520) 2026-07-18 15:16:27 -04:00
Jeremiah Lowin
18b5ab5852
Migrate to MCP SDK v2.0.0b2 (httpx2) (#4503) 2026-07-18 15:12:47 -04:00
Jeremiah Lowin
66c0270bc1
Stabilize upgraded ty checks (#4526) 2026-07-17 17:46:53 -04:00
Jeremiah Lowin
bdb76ef4b2
Clean up disconnected task sessions (#4519) 2026-07-17 17:46:13 -04:00
Jeremiah Lowin
a3ecd1edb1
Clarify PR-reopen flow and fix label-race that broke auto-reopen (#4518)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-07-17 17:43:05 -04:00
Jeremiah Lowin
d779414f8a
Screen templated resource parameters for path traversal by default (#4482)
* Add ResourceSecurity screening for templated resources (defaults on)

* Add tests for resource path-security screening

* Document resource path-security; fix ty in tests

* Carry child template security policy through provider mount

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

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

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

* Match exempt_params under both hyphen and underscore spellings

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

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

The screening only rejects .. segments that escape the starting depth
(foo/../bar passes) — saying any standalone .. is rejected overstated
the guarantee. Also define DOCS_ROOT so the example runs.
2026-07-17 17:42:48 -04:00
Jeremiah Lowin
918b85f9b2
Reject positional-only tool parameters (#4524) 2026-07-17 17:37:28 -04:00
Jeremiah Lowin
ff2fc234b2
Trace client task management requests (#4525) 2026-07-17 17:37:12 -04:00
Jeremiah Lowin
b623c23183
Serialize deep object query parameters (#4523) 2026-07-17 17:33:57 -04:00
Grégoire
977f02347b
Forward upstream instructions through create_proxy (#4512)
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-07-17 17:29:15 -04:00
WinkleMad
6202008cf3
Don't mutate the caller's schema in compress_schema (#4492) 2026-07-17 17:29:01 -04:00
Burt Matthews
1fca15abe6
Skip unsupported JWKS keys instead of failing the whole key set (#4515) (#4517) 2026-07-17 17:28:43 -04:00
石岳峰
291fab8789
fix(server): omit ScalarElicitationType wrapper title from elicitation schemas (#4502) 2026-07-17 17:28:30 -04:00
Jeremiah Lowin
a04f6fd911
Add telemetry off-switch and mcp.protocol.version span attribute (#4481)
* Turn OpenTelemetry instrumentation on by default with explicit off-switch

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

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

The stock NoOpTracer.start_as_current_span attaches a NonRecordingSpan, hijacking the current OTel context from any enclosing application span. When telemetry is disabled, get_tracer() now returns a non-attaching pass-through tracer so trace.get_current_span() inside handlers still resolves to the caller's span.
2026-07-17 17:02:48 -04:00
Jeremiah Lowin
266c129b62
Test lifespan fires once per process over HTTP (#4480)
* Add regression test: HTTP lifespan fires once per process across sessions

* Drop redundant enter-count assertion at teardown (CodeQL)

* Assert session-manager lifespan entry directly, not user-lifespan count
2026-07-17 17:02:26 -04:00
Jeremiah Lowin
836ceac30e
Add KeyValueResponseCacheStore for distributed client response caching (#4479)
* Add KeyValueResponseCacheStore adapter for client response cache

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

* Document distributed response cache store
2026-07-17 16:41:35 -04:00
nate nowack
1d932cc778
Fix Azure scope fallback (#4469) 2026-07-09 15:29:50 -04:00
Jeremiah Lowin
9f251bad00
Forward-port Hugging Face auth provider (#4475) 2026-07-08 20:56:07 -04:00
Jeremiah Lowin
4ad78a60ef
Forward-port HTTP host guard compatibility (#4474) 2026-07-08 20:55:56 -04:00
Jeremiah Lowin
00a7745994
Docs: forward-port v3.4.4 changelog entries (#4476) 2026-07-08 20:55:43 -04:00
Jeremiah Lowin
fd5d98bd13
Test: HTTP lifespan fires once per process across sessions (#4470) 2026-07-08 20:55:26 -04:00
Jeremiah Lowin
8ba5b89918
Add server-level cache hints (SEP-2549) (#4464) 2026-07-08 09:10:41 -04:00
Jeremiah Lowin
3c43038860
Fix stale mcp.types imports in examples (#4452) 2026-07-07 08:07:07 -04:00
Jeremiah Lowin
515a2244a2
Teach fastmcp.Client the modern protocol: mode negotiation, MRTR driver, response cache (#4450) 2026-07-07 08:00:45 -04:00
Jeremiah Lowin
f30f847e1f
Merge pull request #4449 from PrefectHQ/modernize/path-security
Route skill file access through SDK path-security primitives
2026-07-07 08:00:21 -04:00
Jeremiah Lowin
f0e350942f
Merge pull request #4448 from PrefectHQ/modernize/sampling-deprecation
Deprecate ctx.sample and add clear errors for push features on 2026 connections
2026-07-07 08:00:08 -04:00
Jeremiah Lowin
7832f884c6
Merge pull request #4453 from PrefectHQ/remove/deprecated-params
Remove 3.x deprecated parameters and object-mode decorators
2026-07-07 07:59:53 -04:00
Jeremiah Lowin
3cb34034d2
Merge pull request #4451 from PrefectHQ/remove/server-method-deprecations
Remove 3.0-deprecated FastMCP server methods
2026-07-07 07:59:40 -04:00
Jeremiah Lowin
bf3a079f87
Merge pull request #4447 from PrefectHQ/remove/module-shims
Remove 3.x deprecated module shims and dead parameters
2026-07-07 07:59:16 -04:00
Jeremiah Lowin
4a8852af10
Docs: register removed deprecations in v4 change register 2026-07-07 07:53:12 -04:00
Jeremiah Lowin
7c2133a52f
Remove deprecated component-import __getattr__ shims 2026-07-07 07:53:12 -04:00
Jeremiah Lowin
805ce96689
Remove deprecated decorator_mode setting and object-mode machinery 2026-07-07 07:53:12 -04:00
Jeremiah Lowin
45475bd072
Remove deprecated exclude_args tool parameter 2026-07-07 07:53:12 -04:00
Jeremiah Lowin
da5754671b
Document as_proxy backend keyword migration 2026-07-07 07:53:11 -04:00
Jeremiah Lowin
7f032bb82e
Remove deprecated tool-level serializer parameter 2026-07-07 07:53:11 -04:00
Jeremiah Lowin
396fb876d3
Apply ruff-format: drop now-unused imports and reflow 2026-07-07 07:53:11 -04:00
Jeremiah Lowin
508a08475f
Docs: mark removed-in-4.0 deprecations in upgrade guide and change register
Annotates the v2->v3 upgrade guide's deprecated server methods as removed in
v4, and adds a Removed in 4.0 section to the change register.
2026-07-07 07:53:11 -04:00
Jeremiah Lowin
6a6fdcb2bb
Repoint tests and examples off removed deprecations
Replaces FastMCP.as_proxy() helper calls with create_proxy(), rewrites the
mount() as_proxy=/prefix= kwarg tests to plain mount() (the params are gone),
and deletes deprecation-only tests for as_proxy() and remove_tool().
2026-07-07 07:53:11 -04:00
Jeremiah Lowin
14373cc60b
Repoint examples off removed shim paths 2026-07-07 07:53:10 -04:00
Jeremiah Lowin
e5d9d1f17d
Remove deprecated FastMCP server methods for 4.0
Removes deprecations that shipped replacements in 3.0:
- FastMCP.as_proxy() classmethod -> create_proxy()
- import_server() -> mount()
- mount(prefix=) -> namespace=
- mount(as_proxy=) -> now meaningless; mounts always invoke lifespan/middleware
- add_tool_transformation() -> add_transform(ToolTransform({...}))
- remove_tool_transformation() -> pure no-op that warned callers it had no
  effect; removed entirely
- FastMCP.remove_tool() -> mcp.local_provider.remove_tool()

Also drops the now-unused URI_PATTERN/re import and FastMCPDeprecationWarning
import. The _REMOVED_KWARGS TypeError shim is retained.
2026-07-07 07:53:10 -04:00
Jeremiah Lowin
be8aee3a96
Skip live-import validation for frozen v2 doc snapshots 2026-07-07 07:53:10 -04:00
Jeremiah Lowin
ad02710e9b
Docs: register 4.0 deprecation removals in change register 2026-07-07 07:53:10 -04:00
Jeremiah Lowin
36cbef6813
Remove no-op sse_read_timeout param from StreamableHttpTransport
Tier 2 aggressive-window removal: the parameter was a deprecated no-op
on the streamable-HTTP transport (the SDK v2 client no longer supports
it). SSETransport still accepts sse_read_timeout.
2026-07-07 07:53:10 -04:00
Jeremiah Lowin
d74f7b5996
Remove deprecated PromptToolMiddleware / ResourceToolMiddleware (3.1)
Use the PromptsAsTools / ResourcesAsTools transforms instead. The
non-deprecated ToolInjectionMiddleware base class is retained.
2026-07-07 07:53:10 -04:00
Jeremiah Lowin
d1c51ea4db
Remove deprecated fastmcp.server.apps / server.app shims (3.2)
Tier 2 aggressive-window removal: these shims were deprecated in 3.2,
a shorter deprecation window than the usual policy. Canonical imports
are fastmcp.apps / fastmcp.FastMCPApp.
2026-07-07 07:53:10 -04:00
Jeremiah Lowin
9d95e34e19
Remove deprecated experimental openapi shims (2.14) 2026-07-07 07:53:10 -04:00
Jeremiah Lowin
497ed2ea00
Remove deprecated fastmcp.server.openapi shim + FastMCPOpenAPI (3.0) 2026-07-07 07:53:10 -04:00
Jeremiah Lowin
e89763d709
Remove deprecated fastmcp.server.proxy shim (3.0) 2026-07-07 07:53:09 -04:00
Jeremiah Lowin
6621024ce4
Drive the FastMCP lifespan through the SDK session manager (#4446) 2026-07-07 07:49:16 -04:00
Jeremiah Lowin
ac78e6f693
Emit one SERVER span per request and adopt spec-correct error codes (#4445) 2026-07-07 07:49:06 -04:00
Jeremiah Lowin
023a578279
Capture SharedContext for task-enabled Docket servers (#4443) 2026-07-07 07:48:49 -04:00
Jeremiah Lowin
13b9ab80ab
Use a single fastmcp import style in protocol-era tests 2026-07-06 22:06:22 -04:00
Jeremiah Lowin
77131edc00
Don't era-gate server-side sampling handlers on modern connections
The era-gate blocked every ctx.sample/sample_step on a 2026-07-28
connection, but a server-configured sampling handler answers server-side
without the client back-channel. Gate only when the request would hit the
removed client path; force the handler path (client_available=False) on
modern so "fallback" goes straight to the handler instead of a bare
client-attempt failure.
2026-07-06 21:49:18 -04:00
Jeremiah Lowin
05be554e85
Apply ruff formatting to attack corpus tests 2026-07-06 21:32:47 -04:00
Jeremiah Lowin
b023aae51a
Docs: mark sampling deprecated, update era matrix and change register 2026-07-06 21:32:17 -04:00
Jeremiah Lowin
389e3b78e7
Add path-safety attack corpus tests and document skill file confinement 2026-07-06 21:31:56 -04:00
Jeremiah Lowin
17954f569d
Deprecate ctx.sample/sample_step and era-gate sampling+elicit on modern connections 2026-07-06 21:30:03 -04:00
Jeremiah Lowin
1966e619f7
Route skill file reads through SDK safe_join for path safety 2026-07-06 21:29:22 -04:00
Jeremiah Lowin
3522a98766
Migrate to MCP Python SDK v2 (#4437) 2026-07-06 17:36:45 -04:00
Jeremiah Lowin
1eedd1f6f1
Docs: add v3.4.2 and v3.4.3 changelog entries (#4430) 2026-07-05 19:27:01 -04:00
dependabot[bot]
3b1afe6cf1
chore(deps): bump joserfc from 1.6.7 to 1.6.8 in the uv group across 1 directory (#4429)
Signed-off-by: dependabot[bot] <support@github.com>
2026-07-05 19:16:30 -04:00
marvin-context-protocol[bot]
874425a113
chore: Update SDK documentation (#4427) 2026-07-05 19:16:03 -04:00
Jeremiah Lowin
691766b5d0
[codex] Fix OpenAPI resource template requests (#4407) 2026-07-05 19:11:03 -04:00
Jeremiah Lowin
47907e0767
Fix ty 0.0.55 diagnostics and prefab-ui protocol version drift (#4428) 2026-07-05 19:10:40 -04:00
Jeremiah Lowin
c1b0396c0a
Block IPv6 transition SSRF bypasses (#4426) 2026-07-05 17:52:57 -04:00
陈家名
522ed5bb30
fix: correct replace_type docstring parameter descriptions (#4375) 2026-07-05 17:44:28 -04:00
Jeremiah Lowin
67527c1f69
Block unsafe OAuth redirect schemes (#4419) 2026-07-05 17:16:10 -04:00
Jeremiah Lowin
57a279928d
Protect streamable HTTP from DNS rebinding (#4405) 2026-07-05 17:11:08 -04:00
Jeremiah Lowin
cccb529f50
Fix DCR redirect URI validation (#4408) 2026-07-05 17:05:13 -04:00
fengguodong
0d8844d5a7
Reject empty env var keys (#4410) 2026-06-29 09:42:06 -04:00
Jeremiah Lowin
de521e651d
Fix versioned auth middleware checks (#4401) 2026-06-28 10:43:34 -04:00
Jeremiah Lowin
feaae683a4
[codex] Fix event store replay isolation (#4402) 2026-06-27 12:49:24 -04:00
Jeremiah Lowin
5de15e0c21
Block NAT64 SSRF bypass (#4400) 2026-06-27 12:21:37 -04:00
Jeremiah Lowin
a1dd8b12e1
Fix dev apps launch escaping (#4399) 2026-06-27 12:18:36 -04:00
Jeremiah Lowin
6b2a2d507a
Clarify resource path parameter safety (#4398) 2026-06-27 12:10:37 -04:00
Jeremiah Lowin
a8bb1b08c2
Raise fastmcp.ValidationError for invalid tool arguments (#4392)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-27 11:14:20 -04:00
Jacky Su
0668eb7b80
Fix/cimd flow issue (#4206) 2026-06-27 10:57:58 -04:00
marvin-context-protocol[bot]
4d5627ba5d
chore: Update SDK documentation (#4360) 2026-06-27 10:36:03 -04:00
dependabot[bot]
7643480bd6
chore(deps): bump joserfc from 1.6.5 to 1.6.7 in the uv group across 1 directory (#4394)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-27 10:35:50 -04:00
Jeremiah Lowin
dc4b0e202d
fix(tools): honor serialize_by_alias in tool result serialization (#4391) 2026-06-27 10:24:39 -04:00
Jeremiah Lowin
cccff4849e
fix(deps): clear Dependabot security alerts via lockfile bumps (#4393) 2026-06-27 10:22:36 -04:00
Jeremiah Lowin
0cffe41115
fix: validate task tool arguments against declared types (#4373) 2026-06-27 10:03:59 -04:00
Jeremiah Lowin
a612846224
fix: bound default OIDC discovery timeout and expose it on provider wrappers (#4374) 2026-06-24 17:16:18 -04:00
Jeremiah Lowin
ade2b3ff46
fix: bracket IPv6 hosts in server startup log URL (#4372) 2026-06-24 17:15:40 -04:00
Jeremiah Lowin
094908042c
fix(resources): round-trip path values with reserved characters in URI templates (#4368) 2026-06-24 13:56:04 -04:00
Jeremiah Lowin
a13e48ea6f
feat(remote): add --verify flag for TLS certificate verification (#4369) 2026-06-24 12:09:22 -04:00
Bobby Davidson
7f2d034f4d
Forward IdP auth errors to MCP client instead of showing HTML error page (#4293)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-06-24 12:09:09 -04:00
Alex LaGuardia
ea63d06241
docs: add audit/event-record recipe for tool-call middleware (#4345)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 12:03:11 -04:00
Michael@WCD
0d5667bed2
docs: fix 5 broken internal links (auth & providers pages) (#4344)
Co-authored-by: Michael Tierney <MichaelTierney315@outlook.com>
2026-06-24 12:02:20 -04:00
Jianke LIN
ba77e4626b
fix: guard recursive refs in json_schema_to_type (#4312) 2026-06-24 11:53:36 -04:00
Perseus Computing
222742a737
fix: StatefulProxyClient.clear() no longer causes KeyError on session teardown (#4328) 2026-06-24 11:53:22 -04:00
Jeremiah Lowin
6429909f05
Switch prettier hook to rbubley/mirrors-prettier (#4366) 2026-06-24 11:52:40 -04:00
Alex LaGuardia
27a0720928
Add real Monty sandbox e2e coverage for CodeMode call_tool (#4274)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-06-24 11:52:27 -04:00
Jeremiah Lowin
47101eeccd
fix: RST docstrings in fastmcp.types render raw on gofastmcp.com (#4367)
Co-authored-by: Justin van Zyl <justin.vanzyl@xero.com>
2026-06-24 11:52:05 -04:00
Jeremiah Lowin
cf1d821129
fix(fs): isolate same-named package imports across providers (#4361) 2026-06-24 10:57:01 -04:00
Jeremiah Lowin
ddcdf64813
fix(proxy): shield stateful proxy disconnect during session teardown (#4363) 2026-06-24 10:56:40 -04:00
Jeremiah Lowin
5fa4f32cca
Dedupe discriminator-required helper across schema converters (#4362) 2026-06-24 10:54:03 -04:00
dependabot[bot]
0ca2c0d115
chore(deps): bump actions/checkout from 6 to 7 (#4343)
Signed-off-by: dependabot[bot] <support@github.com>
2026-06-24 10:39:32 -04:00
Yufeng He
9b1a1a04e0
fix: preserve required discriminator tags (#4297)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-06-24 10:36:56 -04:00
Jeremiah Lowin
d7730788fa
Recognize all GitHub issue-link forms in require-issue-link workflow (#4359) 2026-06-24 10:28:36 -04:00
Tomasz Czochański
4ce8e2a5d0
Fix: async rate limiting middleware get_client_id callbacks (#4319)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-24 10:27:30 -04:00
Greig Menzies
257fe32556
fix: caching middleware TypeError on cache miss due to mismatched call_next parameter (#4301) 2026-06-24 10:27:18 -04:00
Jeremiah Lowin
3b8538e242
Allow private JWT headers (#4290) 2026-06-06 02:23:48 +01:00
marvin-context-protocol[bot]
0445c31fef
chore: Update SDK documentation (#4223) 2026-06-06 02:23:33 +01:00
Jeremiah Lowin
9261793ae0
Docs: add v3.4.1 changelog entries (#4289) 2026-06-06 02:06:45 +01:00
Jeremiah Lowin
e1b52d00fe
Add explicit starlette>=1.0.1 floor (CVE-2026-48710) (#4286) 2026-06-05 08:42:59 -04:00
Jeremiah Lowin
e58f386bac
Log refresh-token misses in OAuthProxy instead of failing silently (#4276) 2026-06-03 11:22:35 -04:00
Jeremiah Lowin
3f09c683cc
Document --notes-start-tag requirement in release instructions (#4275) 2026-06-03 10:14:28 -04:00
Jeremiah Lowin
e124bde34e
Fix MDX syntax error in changelog (#4270) 2026-06-02 22:20:44 -04:00
Jeremiah Lowin
dae11bbc40
Backfill changelog and updates through v3.4.0 (#4269) 2026-06-02 22:15:02 -04:00
René Mulder
0f4f78c464
Fix resource templates with query params on proxied servers (#4251)
Co-authored-by: René Mulder <rene@silverflow.com>
2026-05-31 14:03:49 -04:00
Jeremiah Lowin
1a06130fcf
Fix GitHub MCP resource integration test (#4253) 2026-05-30 11:48:46 -04:00
Jeremiah Lowin
58e0f5320b
Decouple FastMCP access token lifetime from upstream expires_in (#4254) 2026-05-30 11:48:23 -04:00
Jeremiah Lowin
8e66b0a47a
Upgrade ty to 0.0.39 (#4225) 2026-05-30 11:25:05 -04:00
Jeremiah Lowin
53b20168c8
Close upstream OAuth clients (#4248) 2026-05-27 19:45:45 -04:00
Diogo Santos
989f6f8bde
docs: add tool fingerprinting recipe for stable schema hashing (#4233)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-27 10:52:14 -04:00
Jeremiah Lowin
140d96aadd
Forward proxy initialize as bridge behavior (#4228) 2026-05-27 10:41:49 -04:00
Bálint Hantos
e9848d66a3
fix: Trendshift link and badge in README.md (#4236) 2026-05-26 14:18:15 -04:00
Jeremiah Lowin
a25ca12a41
Restore Horizon docs banner (#4240) 2026-05-26 14:09:10 -04:00
Bill Easton
802ceaaa6b
Add targeted coverage tests (#4230)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-25 18:57:28 -04:00
Jeremiah Lowin
2bff3725bf
Surface proxy upstream failures (#4227) 2026-05-23 14:56:10 -04:00
Jeremiah Lowin
986353f26b
Skip published docs update for prereleases (#4224) 2026-05-23 10:45:36 -04:00
Jeremiah Lowin
59223f6e89
Allow pre-bound HTTP sockets (#4222) 2026-05-23 10:08:49 -04:00
Jeremiah Lowin
70013c5a91
Add fastmcp-remote bridge package (#4208) 2026-05-23 10:04:21 -04:00
Jeremiah Lowin
b8c9d58d61
Migrate auth JWTs to joserfc (#4221) 2026-05-23 09:29:04 -04:00
Jeremiah Lowin
9b5d402bf2
Exempt maintainers from MRE auto-close (#4220) 2026-05-23 09:02:44 -04:00
marvin-context-protocol[bot]
6acacf2191
chore: Update SDK documentation (#4155) 2026-05-23 08:54:28 -04:00
Jeremiah Lowin
292e9af087
Update published docs after PyPI release (#4211) 2026-05-23 08:53:38 -04:00
Jeremiah Lowin
44180d1e3d
Add uv dependency cooldown (#4213) 2026-05-23 08:53:20 -04:00
Jeremiah Lowin
986af54afa
Move pip upgrade recovery into a Troubleshooting section (#4219) 2026-05-22 21:13:19 -04:00
Jeremiah Lowin
7a82b57efb
Document pip upgrade recovery for the fastmcp-slim package split (#4215) 2026-05-22 20:21:28 -04:00
Jeremiah Lowin
e242abee7d
Let ToolResult return an error result via is_error (#4217) 2026-05-22 20:20:52 -04:00
Jeremiah Lowin
8382569027
Add contract gate to review-issue skill (#4214) 2026-05-22 20:08:48 -04:00
Jeremiah Lowin
0e5ec37597
Add review-issue skill for triaging gated external contributions (#4212) 2026-05-22 18:47:15 -04:00
Jeremiah Lowin
fd4d24ff62
Guard require-issue-link check job to pull_request_target events (#4209) 2026-05-22 18:43:46 -04:00
Jeremiah Lowin
b194349983
Add outbound comment guardrails (#4196) 2026-05-21 06:42:48 -04:00
Jeremiah Lowin
2ac23a4de5
Fix docs app demo iframe assets (#4194) 2026-05-20 13:18:45 -04:00
Jordan Ritter
1222e8e47a
Security: Fix 3 findings in GitHub Actions workflows (#4183)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-20 12:37:31 -04:00
Bill Easton
b4c35027c2
fix(ci): scope minimize-reviews concurrency by event name (#4174)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-20 12:23:18 -04:00
yuyua9
dbf5ee24be
fix(cli): forward stateless flag in uv run path (#4177)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-20 12:22:45 -04:00
rgillinlz
81a16ed27f
fix(auth): unprefix Azure scopes echoed back to MCP clients (#4130)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-20 12:22:24 -04:00
mohanram
0022d8518f
Add token_expiry_threshold_seconds for proactive token refresh (#4142)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-20 10:47:26 -04:00
Bill Easton
9d384ffa7f
feat(code-mode): default sandbox limits and per-execution tool-call cap (#4170)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-20 10:47:12 -04:00
Bill Easton
bdbef49383
fix(code-mode): cancel Monty sandbox future on task cancellation (#4169) 2026-05-20 10:36:31 -04:00
Shigechika AIKAWA
2a262438fa
fix(auth): add /.well-known/openid-configuration alias for OAuth server metadata (#4167)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 10:36:01 -04:00
Bill Easton
1c4069efa3
fix(proxy): restore _current_server in _restore_request_context (#4168) 2026-05-20 10:34:00 -04:00
Tiago Surjus Kaneta
b0fb2c3ae6
Add valid_scopes and extra_authorize_params to WorkOSProvider (#4135) 2026-05-20 10:31:48 -04:00
dependabot[bot]
7cfcaf22b8
chore(deps-dev): bump pydantic-monty from 0.0.16 to 0.0.17 (#4023)
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-20 10:31:27 -04:00
yuyua9
834f96d462
fix(schema): preserve root metadata on fallback (#4178) 2026-05-20 09:46:56 -04:00
Pragnyan Ramtha
5c2627cb15
fix(config): read MCP config files as UTF-8 (#4164)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-20 09:43:27 -04:00
Bill Easton
2d61d8a46b
fix: add OTEL spans to sampling step and tool execution (#4059)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 09:38:58 -04:00
Bill Easton
01b971d80d
fix: preserve annotations, meta, title, icons when creating resources from templates (#4061)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 09:38:19 -04:00
dependabot[bot]
d6984f4719
chore(deps): bump the uv group across 2 directories with 1 update (#4113)
Signed-off-by: dependabot[bot] <support@github.com>
2026-05-20 09:37:51 -04:00
itaru2622
7fddab52a9
feat: new options --host and --no-log-panel | --log-panel to cli dev apps (#4123) 2026-05-20 09:37:32 -04:00
Bill Easton
344a4f81b5
fix(auth): disambiguate auth-denied vs missing component messages (#4165) 2026-05-20 09:36:27 -04:00
Bill Easton
11951585c0
fix(tests): hoist azure-identity import out of the OBO test timeout window (#4176) 2026-05-20 09:35:56 -04:00
Bill Easton
24b594b1e3
fix: VersionSpec eq matching normalizes versions and selects deterministically (#4058)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 09:35:25 -04:00
Bill Easton
d3ff774c78
ci: require external PRs to link a tracked issue (#4173)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-20 08:43:52 -04:00
Jeremiah Lowin
d8dcc273ca
Decouple component imports from server (#4150) 2026-05-15 11:49:08 -04:00
Jeremiah Lowin
255e3e4910
fix(docs): use valid FA icon on client-only package page (#4139) 2026-05-14 22:14:38 -04:00
marvin-context-protocol[bot]
73df4dcaee
chore: Update SDK documentation (#4096) 2026-05-14 22:06:38 -04:00
Jeremiah Lowin
ee48a0fd6e
Refine fastmcp-slim packaging (#4125)
* Refine fastmcp-slim packaging

* Format install hints
2026-05-12 07:11:57 -07:00
Jeremiah Lowin
bb4894d215
Add fastmcp-slim for client-only installs (#4122)
* Add fastmcp-client workspace package

* Fix client package static checks

* Document client-only package

* Harden fastmcp-client package split

* Preserve forwarded headers in full package

* Switch to fastmcp-slim package

* Fix fastmcp-slim release edges

* Match pydantic-style slim layout

* Polish fastmcp-slim packaging
2026-05-11 17:13:21 -04:00
Sarthak Bhardwaj
8209093871
fix(http): terminate active streamable-HTTP transports before lifespan shutdown (#4118) 2026-05-10 10:58:13 -04:00
Kiran Thakkar
cf59a4511f
Fix OCI Provider issue in 3.x version. Add OCI auth provider example … (#4116)
* Fix OCI Provider issue in 3.x version. Add OCI auth provider example and test

* Fix OCI Provider issue in 3.x version. Add OCI auth provider example and test. Fixed a couple of minor issues in README.

* Rerun CI
2026-05-10 10:08:00 -04:00
Craig Ringer
89b99ecfb9
fix(proxy): fall back to live identifier for backend_* span attributes (#4109)
🤖 Generated with Claude Code (Opus 4.7) on behalf of @ringerc

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 10:07:57 -04:00
itaru2622
310314cf14
fix: cli option --no-banner is NOT passed to cli but server-spec in-correctly when cli --reload option is specified. (#4083) 2026-05-09 14:57:42 -04:00
sergeykad
28722f846a
fix: drop exc_info for expected tool failures, remove unreachable ValidationError (#4029) 2026-05-09 14:56:17 -04:00
Owen Taylor
567b832bca
fix: Don't completely hide plain mcp.tool app-only tools (#4112) 2026-05-09 14:55:20 -04:00
Shaik Mohammed Kaif
6b6db33c4a
#4084 [Issues] Windows startup crash due to UnicodeDecodeError when l… (#4092)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-07 15:30:56 -04:00
Jeremiah Lowin
3d476ca01e
fix: don't cache import map in dev apps bundle (#4106) 2026-05-07 11:46:13 -04:00
Sarthak Bhardwaj
aaff9243a3
fix(auth): silence authlib.jose DeprecationWarning at JWT import (#4100)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-07 11:27:08 -04:00
Sarthak Bhardwaj
b8597f941d
fix(tool_transform): hoist $defs to schema root when ArgTransform introduces them (#4101) 2026-05-07 11:10:52 -04:00
Jeremiah Lowin
4719f3055a
Make Docket reentrant; mounted servers enter their own lifespan (#4095) 2026-05-04 17:36:50 -04:00
Taylor Wilsdon
61e56c683e
enh: Add public API for updating OAuthProxy scopes after initialization (#4091) 2026-05-04 15:49:12 -04:00
marvin-context-protocol[bot]
074189265d
chore: Update SDK documentation (#3988)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-05-04 15:34:27 -04:00
Chris Guidry
a037bd8cc7
Bump pydocket to 0.20.0 (#4031)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 15:34:06 -04:00
cuyua9
0b59af9489
Fix sampling from background tasks (#4068)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-04 12:52:35 -04:00
Ashwin Madavan
44d3f8d858
fix(ping): exit ping loop cleanly when session stream is closed (#4087) 2026-05-04 12:50:39 -04:00
Mukunda Rao Katta
20359de953
Fix #4056: keep blank query values, add token bucket regression test (#4069)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-04 12:44:38 -04:00
Mukunda Rao Katta
1cfd30840d
fix(openapi): keep blank values in parse_qs (refs #4056) (#4076)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-04 12:44:06 -04:00
Daniel Tsiang
73b7f2e44d
Add log_level parameter to FastMCP errors (#4036)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-05-04 12:38:16 -04:00
Jeremiah Lowin
51e339d8ed
docs: fix broken links in Pydantic AI guide (#4094) 2026-05-04 12:35:35 -04:00
chaoliang yan
2ffe68cfa1
fix: preserve tool decorator metadata (#4072)
Generated with Codex.

Co-authored-by: lawrence3699 <lawrence3699@users.noreply.github.com>
2026-05-04 12:22:31 -04:00
Mukunda Rao Katta
f5dea2a17c
docs(integrations): add Pydantic AI FastMCP toolset guide (#4070)
* docs(integrations): add Pydantic AI FastMCP toolset guide

* docs(integrations): add Pydantic AI to AI SDKs nav
2026-05-04 12:22:17 -04:00
Jeremiah Lowin
d0315974fa
Add UTM tracking to Horizon links (#4064) 2026-04-26 08:38:05 -04:00
Jeremiah Lowin
b96ce24e35
Refresh landing page copy (#4047) 2026-04-25 22:06:30 -04:00
Jeremiah Lowin
6f3ea0b929
Refresh landing page copy (#4043) 2026-04-25 14:46:48 -04:00
Jeremiah Lowin
a010927ea5
Add experimental_capabilities kwarg to FastMCP constructor (#4042)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-25 12:24:54 -04:00
Jeremiah Lowin
0fe01372f0
Partition ResponseCachingMiddleware cache by access token (#4041)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-25 12:09:11 -04:00
Mukunda Rao Katta
c740b6d70a
docs: add best practices for custom telemetry spans (#4001) 2026-04-25 11:43:31 -04:00
Bill Easton
4be46c6fa6
Add a sandboxed-agents deployment guide (#4027)
* Add sandboxed agents deployment guide

* Tighten sandboxed agents guide

* Refine sandboxed agents docs flow

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-25 11:40:56 -04:00
Zach Leventer
07c34acb12
Add missing return type annotation to __getattr__ (#4026) 2026-04-25 11:35:27 -04:00
Adam Azzam
e95efce988
Add UTM params to Horizon docs links (#4018)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:47:55 -04:00
Jeremiah Lowin
74efa32edf
Add run_in_thread opt-out for sync tools with thread affinity (#4010)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-22 10:31:44 -04:00
Jeremiah Lowin
485747353e
fix: cancel orphaned session_task when Client._disconnect times out (#4011)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-22 10:17:00 -04:00
Jeremiah Lowin
008f91d84b
fix: narrow _token_validator with isinstance for ty in AzureProvider.from_b2c (#4007)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-22 09:30:37 -04:00
Carlos Rian
eebdc8c031
feat: add AzureB2CProvider for Azure AD B2C user flows (#3995) 2026-04-22 09:24:09 -04:00
Jeremiah Lowin
2d6143c6d8
fix: honor upstream refresh token expiry in OAuthProxy (#3990) 2026-04-20 14:03:57 -04:00
dependabot[bot]
db6bc3e94c
chore(deps-dev): bump pydantic-monty from 0.0.14 to 0.0.16 (#3984)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-20 13:26:23 -04:00
marvin-context-protocol[bot]
34f6e68826
chore: Update SDK documentation (#3969)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-04-20 13:00:37 -04:00
chaoliang yan
5009d64465
fix: handle circular JSON Pointer $ref in dereference_refs (#3896)
Co-authored-by: lawrence3699 <lawrence3699@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-20 13:00:18 -04:00
Jeremiah Lowin
a075b14432
Be constructively skeptical of bot reviews on own PRs (#3971) 2026-04-18 17:22:54 -04:00
Jeremiah Lowin
801385df44
fix: bound _refresh_locks with LRU eviction to prevent memory leak (#3968) 2026-04-18 11:07:38 -04:00
Jeremiah Lowin
64fbc52e1d
fix: skip stdio subprocess test on Windows CI (#3966)
Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2026-04-18 10:30:03 -04:00
dependabot[bot]
c759890d9e
chore(deps-dev): bump pydantic-monty from 0.0.11 to 0.0.12 (#3940)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-17 21:06:00 -04:00
marvin-context-protocol[bot]
b4c4cfd3d5
chore: Update SDK documentation (#3917)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-04-17 18:29:09 -04:00
Jeremiah Lowin
37a46231fd
Bump deps with open dependabot alerts (#3965) 2026-04-17 18:04:22 -04:00
Jeremiah Lowin
97bff96ef1
Revert "Forward backend capabilities in ProxyProvider (#3956)" (#3964)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-17 17:51:27 -04:00
Jeremiah Lowin
1e67c53a17
Reject dot-segments in redirect URI allowlist matching (#3963) 2026-04-17 17:49:25 -04:00
Gnani Rahul
ff8aa484ff
docs: standardize ToolAnnotations examples (#3952) 2026-04-17 16:42:57 -04:00
Jeremiah Lowin
e1ea695d68
Restructure docs navigation (#3951)
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
2026-04-17 16:42:39 -04:00
Jeremiah Lowin
789a298650
Graceful fallback for unsupported regex patterns in json_schema_to_type (#3959)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-17 16:42:10 -04:00
Jeremiah Lowin
7fb037f20c
Harden OAuth Proxy silent consent against AS-in-the-middle (#3960) 2026-04-17 16:39:50 -04:00
Jeremiah Lowin
eb1426251c
Allow upstream client_id to be used directly without DCR (#3957)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-17 15:22:44 -04:00
Jeremiah Lowin
98f69bdba0
Forward backend capabilities in ProxyProvider (#3956)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-17 15:22:08 -04:00
Jeremiah Lowin
e3f845f558
Improve real-world schema crash test: failure dump, cluster analysis, TypeErrors baseline ratchet (#3958)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-17 15:08:48 -04:00
Chris Guidry
34313ea112
Restore task snapshot via a worker-level dependency (#3945)
* Restore task snapshot via a worker-level dependency

`get_access_token()` returned `None` inside background tasks whenever
`FASTMCP_DOCKET_URL` pointed at a `redis+cluster://` URL. The write side
was fine — it went through `docket.redis()`, which is cluster-aware —
but fastmcp kept a parallel sync Redis client just to read the snapshot
back, and `Redis.from_url()` rejects the cluster scheme.

Docket 0.19.1 ships worker-level dependencies that resolve per task in
the same asyncio.Task as user code, so ContextVars propagate cleanly.
That lets us load the snapshot once via `restore_task_snapshot` and
drop the sync Redis path entirely. Sync helpers like
`get_access_token()` and `get_http_request()` now just read a
ContextVar; Docket is the sole Redis consumer.

Closes #3897

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Revert TaskKey stub to a plain return

NotImplementedError would fire at module import if anything evaluated
the default; a no-op stub keeps the module usable without the
fastmcp[tasks] extra, which is what we want.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 08:24:42 -04:00
Jeremiah Lowin
ac1416bd2e
fix tests that relied on task=True returning error results (#3954) 2026-04-17 07:48:46 -04:00
Bill Easton
39b421a464
OTEL: Instrument all MCP list operations and enrich delegate spans (#3890)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:45:50 -04:00
Bill Easton
5593cf3e11
fix: FileSystemProvider reload race condition (#3938)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 18:45:11 -04:00
Gnani Rahul
012f674ee4
Fix raise_on_error handling for tool tasks (#3946) 2026-04-16 18:37:52 -04:00
Bill Easton
7dd573980a
fix: OpenAPI request director sends multipart and form-urlencoded as JSON (#3932)
* fix: OpenAPI request director content-type dispatch and cookie params

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Stringify multipart form values and cookie params for httpx

httpx rejects non-string scalars in files= and cookies=.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add tests for non-string multipart values and cookie stringification

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Consolidate to single httpx.Request construction point

Eliminate early returns by using variables for files/data kwargs.
All httpx body kwargs accept None, so we set exactly one and
pass all to a single Request() call.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Use _query_scalar_to_str for multipart booleans, add tuple passthrough test

Reuse existing boolean serialization (true/false not True/False) for
multipart form fields. Add test for file-like tuple passthrough.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Normalize media type for dispatch, use OpenAPI serialization for cookies

- Strip content-type parameters (e.g. "; charset=utf-8") and lowercase
  before matching, so variants like "Multipart/Form-Data" match correctly
- Use _query_scalar_to_str for cookie values (true/false not True/False)
- Add boolean cookie test

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Preserve media-type parameters in Content-Type header

Use raw_content_type (with charset etc.) for the outgoing header,
normalized form only for dispatch matching.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Pass bytes/file-like values directly in multipart, add charset preservation test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:28:55 -04:00
Bill Easton
970b92bb16
fix: allow hyphens in resource template parameter names (#3929)
* fix: allow hyphens in resource template parameter names

Normalize hyphens to underscores at the regex group level in build_regex()
and at the param extraction level in from_function(). No API changes —
build_regex still returns Pattern | None, match_uri_template still returns
the same dict shape.

Closes #3921

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Guard against query params clobbering path params

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add tests for wildcard hyphens, expand, and query clobber guard

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ruff format fix

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add collision detection for hyphen/underscore param name normalization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:28:51 -04:00
Bill Easton
7184a4ca21
OTEL: Fix attribute compliance with MCP semantic conventions (#3889)
* OTEL: Fix attribute compliance and improve telemetry helpers

Attribute compliance:
- Remove rpc.system/service/method (MCP is not traditional RPC)
- Add gen_ai.tool.name on tools/call spans
- Add gen_ai.prompt.name on prompts/get spans
- Fix session_id check (truthy -> is not None)

Telemetry helper improvements:
- Add is_recording() guards to skip work on non-recording spans
- Add error.type attribute with __qualname__ on error spans
- Use isinstance check for ToolError to set "tool_error" error type
- Include exception message in span status description
- Add tool_name/prompt_name params to server_span and client_span

Client call_tool enrichment:
- Reflect tool-level errors (result.isError) on client span status
  so callers see ERROR even though the MCP protocol call succeeded

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove resource URI from span names to avoid high-cardinality

Per MCP semantic conventions, resource URIs SHOULD NOT be included in
span names by default since they can be unbounded (especially with
templates like users://{id}/profile). The URI remains available via
the mcp.resource.uri attribute.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add missing gen_ai/mcp attributes to proxy and delegate spans

- Proxy tool spans: add gen_ai.tool.name
- Proxy prompt spans: add gen_ai.prompt.name
- All delegate spans: add mcp.method.name
- Docs: remove rpc.* references, update span names and attributes table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Hoist ToolError imports to module level, add rpc.* migration note

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 16:28:47 -04:00
Jeremiah Lowin
e4bb6666ab
Add default prefill to FormInput.collect_input (#3937) 2026-04-14 13:23:36 -04:00
Jeremiah Lowin
8c020adfc6
fix: narrow OpenAI audio format dict to Literal for ty (#3936) 2026-04-14 13:09:54 -04:00
Bill Easton
8d1b28958f
fix: prevent schema mutation in _prune_param and _convert_nullable_field (#3927)
🤖 Generated with Claude Code

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 12:10:27 -04:00
Bill Easton
4ea102b433
fix: ProxyTool crashes on non-TextContent error responses (#3926)
* fix: handle non-TextContent error responses in ProxyTool

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Avoid serializing binary content into ToolError messages

Use type name instead of str(content) to prevent dumping
large base64 payloads into error messages.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* ruff format fix

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 12:10:23 -04:00
Bill Easton
757678bc7e
fix: reject self-mount to prevent infinite recursion (#3925)
* fix: server safety guards for self-mount, duplicate middleware, mount arg order

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Remove duplicate middleware and mount arg order checks

These are runtime type checking, not bugs — a type checker catches them.
Keep only the self-mount guard which is a semantic check.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 12:10:19 -04:00
Bill Easton
55f3cade10
Improve Claude workflow prompts based on output audit (#3934)
* Improve Claude workflow prompts based on 60-day output audit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Encourage tl;dr-first structure and collapsible details across workflows

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add quality gates, evidence standards, and calibration examples to workflows

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 11:06:12 -05:00
Jeremiah Lowin
7d7607473d
Stop pydantic 2.13 from leaking _WrappedResult docstring into tool output schemas (#3918) 2026-04-13 21:38:13 -04:00
Jeremiah Lowin
b732a4a516
Overhaul apps docs (#3915) 2026-04-13 21:24:00 -04:00
marvin-context-protocol[bot]
5c2ff1bd7b
chore: Update SDK documentation (#3914)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-04-13 21:23:43 -04:00
Jeremiah Lowin
f4f2ec07fb
Deprecate ctx.elicit() without response_type (#3916) 2026-04-13 21:23:31 -04:00
dependabot[bot]
338b80c3ae
chore(deps): bump the uv group across 2 directories with 1 update (#3913)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-13 20:12:30 -04:00
Jeremiah Lowin
110cd3adcb
Add response_title and response_description to ctx.elicit() (#3912) 2026-04-13 20:12:09 -04:00
marvin-context-protocol[bot]
3117846176
chore: Update SDK documentation (#3909)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-04-13 20:08:29 -04:00
Bill Easton
031c7e03b4
Fix RetryMiddleware not retrying tool errors (#3858)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-13 20:08:04 -04:00
Bill Easton
200d79e7d2
Enable PERF and T20 ruff rules (#3845)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:07:32 -04:00
Jeremiah Lowin
82f310fe61
AuthKit: auto-bind token audience to resource URL (RFC 8707) (#3905) 2026-04-13 17:11:17 -04:00
marvin-context-protocol[bot]
c2dafc1c88
chore: Update SDK documentation (#3901)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-04-13 14:00:07 -04:00
Rishav Mitra
e66cce2538
fix: task.wait() hangs indefinitely when task enters input_required (#3798)
* fix: resolve OpenAPI 3.x server variables in _create_default_client

When an OpenAPI spec defines server variables (e.g. `https://{region}.api.example.com/v1`),
the default values are now substituted before constructing the httpx client base URL.
Previously, the URL was used as-is, causing all requests to fail for specs that use
server variable templating.

Fixes #1681

* fix: use str.replace instead of format_map for server variable substitution

format_map applies Python string formatting rules, so variable names
like {api.version} would be treated as attribute access and raise errors.
Literal token replacement handles all valid OpenAPI variable names safely.

* fix: task.wait() now returns on input_required instead of hanging

Previously, wait() used a terminal-state allowlist (completed, failed,
cancelled), so tasks entering input_required would hang until timeout.

Replaced with inverse logic: return whenever the task exits the 'working'
state. This handles input_required and any future blocking states without
needing to update the allowlist.

Fixes #3779

* fix: include submitted in in_progress_states to avoid premature return

* fix: revert submitted, update state docstring to match MCP spec

* fix: add _wait_terminal() so result() waits for completed/failed/cancelled

wait() correctly returns on input_required for human-in-the-loop use cases,
but result() needs to wait until the task fully resolves. Add a private
_wait_terminal() helper that loops through non-terminal states and use it
in all result() implementations.
2026-04-13 13:59:09 -04:00
Bill Easton
db6d7a8a61
fix: elicitation scalar return, resource auto-serialization, Client.new() state, prompt errors (#3859)
* fix: elicitation scalar return, resource auto-serialization, Client.new() state, prompt errors

- Auto-wrap scalar elicitation responses for ScalarElicitationType schemas
  so handlers can return T directly for ctx.elicit("msg", str/int/float)
- Auto-serialize dict/int/float/bool/None resource returns to JSON text
  instead of crashing with TypeError
- Reset _task_registry and _submitted_task_ids in Client.new() so cloned
  clients have independent task tracking state
- Include original error message in prompt render errors (matching tool
  error behavior)

Fixes #3856

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix misleading comment and add list/tuple auto-serialization for resources

The comment said "list/tuple of primitives" but the isinstance check
didn't include list or tuple. Now it does, and the comment matches.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix TaskNotificationHandler binding in Client.new() and meta forwarding for JSON resources

Two review-identified bugs:

1. Client.new() shallow-copies _session_kwargs, so the cloned client's
   TaskNotificationHandler still dispatches to the original client.
   Fix: create a fresh _session_kwargs dict with a new handler bound
   to the new client.

2. convert_result() for dict/int/float/bool/None fell through to
   ResourceResult(raw_value) which lost component meta (CSP, permissions).
   The str/bytes path correctly wrapped in ResourceContent with meta.
   Fix: explicitly serialize JSON-native types and wrap with meta,
   matching the str/bytes path. Other types still fall through for
   error handling.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Preserve custom message handlers in Client.new()

Only replace the message handler with a new TaskNotificationHandler
if the current handler IS a TaskNotificationHandler. If the user
provided a custom message_handler, preserve it in the clone.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix client.new and add regression tests

* Honor declared MIME type for auto-serialized JSON resources

* Fix static analysis: remove unused StdioTransport import, fix ty:ignore comment

* Exclude list[ResourceContent] from JSON auto-serialization path

A bare list[ResourceContent] would match the isinstance(list) check
and get JSON-serialized instead of passing through to ResourceResult
normalization. Check for ResourceContent items first.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:56:49 -04:00
Chris Guidry
9a063963f4
Scope tasks to authorization context, not session (#3800)
* Scope tasks to authorization context, not session

Tasks were keyed by the transport-layer Mcp-Session-Id, which is
server-assigned and changes on reconnect — so clients lost access to
their running tasks after any connection interruption.

The MCP spec says tasks should be bound to authorization context, not
session.  This replaces session_id with task_scope (derived from
AccessToken.client_id, URL-encoded) in all task data Redis keys and
Docket task keys.  When no auth is configured, a "_" sentinel is used
and security comes from UUID task ID entropy per the spec.

Session ID is still used for transport-level concerns (notification
queues, subscriber registration) and is now stored in the
TaskContextSnapshot payload so background workers can still deliver
notifications.

Also extracts all the task context infrastructure (TaskContextInfo,
TaskContextSnapshot, snapshot loading, session/server registries) from
server/dependencies.py into a new server/tasks/context.py to keep the
DI module from sprawling further.

Closes #3758

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Rename _redis_key to _snapshot_redis_key

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Document in-process session registry as an optimization

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Tidy imports and docstrings

Hoist imports where safe, keep subscriptions/notifications deferred in
handlers.py since they pull in docket at module level. Sharpen docstrings
on keys.py and context.py so each module owns its lane. Clean up the
re-export block in dependencies.py.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix misleading comment on re-export block

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Tighten task scope: include sub claim, partition keyspaces

Addresses review feedback on #3800:

- Compose task scope from client_id and the JWT sub claim (when present)
  so fixed-OAuth deployments isolate per user, not just per client.
- Replace the "_" anonymous sentinel with a tagged keyspace partition.
  Docket keys are now auth:{enc_scope}:... or anon:..., and Redis keys
  use fastmcp:task:auth:{enc_scope}:... or fastmcp:task:anon:...,
  routed through a single task_redis_prefix() helper.
- get_task_scope() returns the raw scope (or None); encoding happens
  once at the keys.py boundary, collapsing the previous double-quote
  invariant.
- Drop the dormant fallback in notifications.py that routed
  input_required relays into the anon keyspace when task_scope was
  missing -- log and skip instead.
- Add comprehensive parser/encoder tests in test_task_keys.py covering
  round-trips, malformed keys, and adversarial scopes ("anon", "_", and
  scopes containing : / | %).
- Add cross-scope rejection tests: distinct client_ids, distinct sub
  claims under a shared client_id, and authenticated vs anonymous.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 13:50:43 -04:00
Stephan Eberle
a6bd66aca8
docs: link fastmcp-keycloak-local companion project from Keycloak integration page (#3904) 2026-04-13 13:43:19 -04:00
Adam Azzam
f21e51794c
Allow auth providers to override protected resource base URLs (#3900)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-13 13:36:55 -04:00
Stephan Eberle
99bf81c64f
Add Keycloak OAuth Provider for Enterprise Authentication and local dev (#1937) 2026-04-13 12:23:10 -04:00
Vonbai
279b601f80
Harden forced client disconnect cleanup (#3885)
* Harden forced client disconnect cleanup

* Handle cancelled force-close waits

Generated with Codex.

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-13 12:11:11 -04:00
Jeremiah Lowin
74d09ee0f5
Fix wildcard resource template params in mounted servers (#3899) 2026-04-13 11:25:51 -04:00
Jeremiah Lowin
0a921f5372
refactor: unify object-schema conversion through _object_schema_to_type (#3884) 2026-04-12 21:33:22 -04:00
marvin-context-protocol[bot]
e8f2fefb06
chore: Update SDK documentation (#3876)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-04-12 21:23:37 -04:00
Jeremiah Lowin
fa9ad6cb81
fix: strip titles from bare-metadata nodes (Gemini 2.5 Flash) (#3881) 2026-04-12 21:06:27 -04:00
Ke Wang
f23599283c
fix: resolve list[dict] return type producing Root() instead of dicts (#3880)
When a tool returns `list[dict]`, the client deserializes each dict as a
`Root()` dataclass with no fields instead of preserving the original dict
data.

The root cause is in `_get_from_type_handler`: its `"object"` branch
always fell through to `_create_dataclass` for schemas without
`properties`, creating an empty dataclass named `Root`. The top-level
`json_schema_to_type` already handled this case correctly (returning
`dict[str, Any]`), but that logic was not shared with `_schema_to_type`
which is used when converting nested schemas (e.g., array items).

Extract `_object_schema_to_type` to unify the four object-schema cases
(dict, typed dict, BaseModel with extra, dataclass) so both top-level
and nested paths produce the correct type.

Fixes #3867

Co-authored-by: Ke Wang <ke@pika.art>
2026-04-12 17:54:00 -04:00
Jeremiah Lowin
9a447cb08d
test: bump OpenAPI init perf threshold to 200ms for Windows CI (#3879) 2026-04-12 17:17:14 -04:00
vincent
673e6bb0f7
docs: add uv-managed dependencies and uvx examples to mcp-json configuration (#3843)
Closes #1707

- Add example for configuring mcp.json with uv-managed projects (pyproject.toml)
- Add examples for running published pip packages via uvx
- Update both main and v2 docs

Co-authored-by: Emily Chen <emilychen.techwriter@gmail.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-12 17:09:15 -04:00
Jeremiah Lowin
57f1b1bced
fix: drop configurable dedupe from AggregateProvider, always warn (#3877) 2026-04-12 17:03:02 -04:00
Jeremiah Lowin
dfe9b307bd
Rename filesystem-provider example dir to avoid mcp/ collision (#3878) 2026-04-12 17:01:58 -04:00
Jeremiah Lowin
1d39e26025
Fix CIMD clients getting required_scopes instead of valid_scopes (#3836) 2026-04-12 16:34:56 -04:00
Jeremiah Lowin
7f80f78906
ci: speed up schema crash test (CSafeLoader + xdist-safe aggregation) (#3873) 2026-04-12 16:34:13 -04:00
dependabot[bot]
82253ad0f5
chore(deps-dev): bump pydantic-monty from 0.0.9 to 0.0.10 (#3809)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-12 16:13:20 -04:00
dependabot[bot]
6a82eb3ead
chore(deps): bump actions/checkout from 4 to 6 (#3864)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-12 15:39:34 -04:00
dependabot[bot]
9264728330
chore(deps): bump astral-sh/setup-uv from 6 to 7 (#3865)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-12 15:39:24 -04:00
Lars Kiesow
06f58eb617
Fix MCP tool on docs welcome page (#3874) 2026-04-12 15:39:09 -04:00
dependabot[bot]
683ce919e3
chore(deps): bump extractions/setup-just from 3 to 4 (#3863)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-12 13:53:48 -04:00
marvin-context-protocol[bot]
56456cb188
chore: Update SDK documentation (#3808)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-04-12 13:53:38 -04:00
Jeremiah Lowin
f3c00ba1b7
Extract parameter descriptions from docstrings (#3872) 2026-04-12 13:43:48 -04:00
Bill Easton
fb03e85592
Fix high-severity test quality issues (#3854)
- Delete entirely commented-out test_run_server.py (99 lines dead code)
- Fix test_pydantic_model_with_stringified_json_no_strict: replace
  try/except-both-branches-pass with clear pytest.raises assertion
- Fix test_path_traversal_blocked: remove dead assertions after
  pytest.raises (lines after raise never execute)

Error handling middleware test fixes are in a separate PR (#3858)
which also fixes the underlying RetryMiddleware bug.

🤖 Generated with Claude Code

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:14:52 -04:00
Bill Easton
cae9333f4f
fix: cap consecutive final_response validation retries (#3851)
* Cap consecutive final_response validation retries to 3

Previously, when the LLM repeatedly called final_response with data that
failed validation, the retry loop would continue up to 100 times (the
shared max_iterations limit), wasting tokens on a model that cannot
satisfy the schema.

Add _MAX_VALIDATION_RETRIES (default 3) that caps consecutive validation
failures. The counter resets when the LLM calls other tools (not
final_response), so the cap only applies to consecutive failures.

Fixes #3848

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add tests for consecutive validation retry cap

Tests cover:
- Validation failures within cap followed by success
- Consecutive validation failures exceeding cap (raises RuntimeError)
- Counter reset when LLM calls other tools between validation failures

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Slim down validation retry cap tests

Reduce boilerplate with helper functions.
Simplify counter-reset test from 5 calls to 4.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix static analysis: move imports to module level and format

Move CreateMessageResultWithTools and ToolUseContent imports to the
top of the test file so ty can resolve the names used in return-type
annotations of the helper functions. Also fix ruff import sorting
and formatting issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Align validation retry semantics with text-response retries

Change `>=` to `>` so _MAX_VALIDATION_RETRIES means "number of
retries after the initial attempt" (total = N+1), matching the
convention used by _MAX_TEXT_RESPONSE_RETRIES in the text-response
retry path.

Before: _MAX=3 meant 3 total attempts (>= comparison)
After:  _MAX=3 means 1 initial + 3 retries = 4 total (> comparison)

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:14:06 -04:00
Bill Easton
d5a3d54662
fix: GoogleGenaiSamplingHandler leaks thought parts and gives unhelpful errors on empty responses (#3849)
* Fix GoogleGenaiSamplingHandler thought part leaking and unhelpful errors

- Filter thought parts (part.thought=True) from response content instead
  of leaking them as TextContent in _response_to_result_with_tools
- Include finish_reason in error messages when no content is found, so
  safety-filtered responses (SAFETY, RECITATION, etc.) are distinguishable
- Add specific error message for thinking-only responses in
  _response_to_create_message_result

Fixes #3846

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add tests for thought part filtering and error message improvements

Tests cover:
- Thought parts filtered from tool-path responses
- Thought-only responses produce descriptive errors
- Safety-filtered responses include finish_reason in error
- Normal responses (text + function calls) unaffected

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix ruff format and ty check issues

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix ty check errors in tests

Remove unused ty: ignore comments from lines where isinstance() narrows
the type, and add correct ty: ignore[invalid-argument-type] and
ty: ignore[not-subscriptable] comments on lines in newly added test
functions where ty cannot infer the union type is a list. Also apply
ruff format fix in test_task_return_types.py.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix: use all() not any() for thinking-only detection

Addresses review feedback: any() would misclassify mixed responses
(thought + function_call) as thinking-only, hiding the real error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 13:10:55 -04:00
Jeremiah Lowin
f71680c427
docs: require agents to respect DNM markers on PRs (#3871) 2026-04-12 12:58:17 -04:00
Bill Easton
73deedf3a4
Enable 4 ruff rules (DTZ, ERA, ISC, INP) and fix 9 violations (#3842)
- DTZ: Use timezone-aware datetime.now(timezone.utc)
- ERA: Remove commented-out code, clarify ambiguous comment
- ISC: Convert explicit string concatenation to implicit
- INP: Add missing __init__.py in auth/handlers/

🤖 Generated with Claude Code

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:54:52 -04:00
Bill Easton
901453902f
Fix broken code examples in docs (#3869)
* Fix broken code examples in docs

- Tag error output blocks as ```text instead of ```python (anthropic,
  openai integration docs + v2 mirrors)
- Quote unquoted URL in Descope config example (+ v2 mirror)
- Fix GoogleGenAISamplingHandler → GoogleGenaiSamplingHandler casing
  in sampling docs
- Fix import path: handlers.GoogleGenaiSamplingHandler →
  handlers.google_genai.GoogleGenaiSamplingHandler in v3-features

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix remaining broken doc examples and add skip tags for false positives

- BearerTokenAuth → StaticTokenVerifier in deployment/http.mdx
- providers.oauth → server.auth import in authentication.mdx
- ListToolsNext → updated list_tools API in v3-features.mdx
- OAuthClientProvider → OAuth in v2/storage-backends.mdx
- Add test="skip" for upgrade guides, contrib placeholders, f-string backticks

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Ratchet doc example baselines to zero

All 1444 examples now pass syntax and import checks.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add pytest-examples dev dep, fix client_id in StaticTokenVerifier example, commit missed openapi fixes

- Add pytest-examples to dev dependencies (fixes CI ModuleNotFoundError)
- Include required client_id in StaticTokenVerifier token payload
- Commit previously unstaged HTTPRoute import fixes in openapi.mdx

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Update deprecated import paths across docs

- fastmcp.server.openapi → fastmcp.server.providers.openapi
- fastmcp.server.proxy → fastmcp.server.providers.proxy
- fastmcp.server.apps → fastmcp.apps
- Tag upgrade guide "Before" examples with test="skip"

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:53:04 -04:00
Bill Easton
d6b55c0b2f
Raise on unhandled content types in sampling handler dispatch chains (#3857)
* Raise on unhandled content types in sampling handler dispatch chains

The Anthropic and OpenAI sampling handlers have isinstance chains that
dispatch on MCP content types but silently drop unhandled variants like
EmbeddedResource and ResourceLink. This adds explicit else-raise guards
to match the Gemini handler's behavior and the single-content dispatch
paths that already raise.

Raising is the right choice over warn-and-skip: a partial conversion
produces a plausible-but-wrong LLM response (the model confidently
answers based on incomplete input), which is worse than a clear error
that tells the user exactly what isn't supported.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add tests for unsupported content type raises in sampling handlers

Tests the new ValueError raises for unsupported content types
(e.g. EmbeddedResource) in the Anthropic and OpenAI message
conversion loops. Uses model_construct to bypass Pydantic's
union validation since the raise is a defensive guard for
future SDK content types.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:52:31 -04:00
Bill Easton
f26c8fad3e
fix: retry when LLM returns text instead of calling final_response (#3850)
* Retry when LLM returns text instead of calling final_response tool

Instead of raising RuntimeError immediately when the LLM returns a text
response instead of calling the `final_response` tool for structured
output, retry up to 3 times with an explicit nudge message asking the
model to use the tool. This mirrors the existing retry behavior for
validation errors but with a separate, smaller cap.

Fixes #3847

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add tests for text response retry logic

Tests cover:
- Text response followed by successful final_response (retry works)
- Text response exceeding max retries (raises RuntimeError)
- Nudge message appended to history on retry
- No retry when result_type is None (text is valid)

Addresses review feedback from PR review tool (v1 flagged missing tests as high severity).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Slim down text response retry tests

Remove test_nudge_message_in_history (implementation detail).
Reduce boilerplate in remaining 3 tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix ruff format

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:52:16 -04:00
Jeremiah Lowin
af957e773f
Replace ___ with hash-based backend tool routing and per-tool prefab resources (#3824)
Replaces the ___ separator for FastMCPApp backend tool routing with a
deterministic hash(app_name, tool_name) prefix, and replaces the shared
singleton prefab renderer resource with per-tool resources synthesized
on demand.

Backend tools are now callable via <hash>_<local_name> instead of
<app_name>___<local_name>. The dispatcher walks the provider tree
recursively via get_tool_by_hash (same pattern as get_app_tool).

Each prefab tool gets its own renderer resource at
ui://prefab/tool/<hash>/renderer.html with per-tool CSP — fixing the
bug where PrefabAppConfig(csp=...) never actually applied.

Closes #3735, closes #3805
2026-04-12 12:52:07 -04:00
Bill Easton
671eaf0f03
fix: strip title fields from tool schemas for Gemini 2.5 Flash compatibility (#3861)
* fix: strip title fields from tool schemas for Gemini compatibility

Gemini 2.5 Flash produces MALFORMED_FUNCTION_CALL when a function
declaration's parameters_json_schema contains 'title' fields (which
Pydantic adds by default). Strip them in _convert_tool_to_google_genai
before passing to the API.

Fixes #3860

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix ty errors and lowest-deps test failure

- Add assertions for non-None before subscripting FunctionDeclaration
  fields (ty check)
- Test compress_schema directly instead of constructing FunctionDeclaration
  which may not support parameters_json_schema in google-genai==1.18.0

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:51:49 -04:00
Bill Easton
74797c8cac
fix: don't pass HTTP kwargs to run_async when transport is unspecified (#3838)
When transport is None (the default), run_async resolves it to
settings.transport which defaults to "stdio". The previous guard
`transport != "stdio"` passed HTTP kwargs through for None transport,
causing TypeError in run_stdio_async.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:51:34 -04:00
Bill Easton
9e2e602038
fix: cross-provider duplicate detection, error visibility, mask propagation (#3827)
- AggregateProvider._collect_list_results detects duplicate component
  names across providers, respecting the server's on_duplicate setting
- Provider errors logged at WARNING instead of DEBUG
- Parent server re-masks ToolErrors from mounted children at the
  FastMCPError catch boundary instead of mutating the child server

Fixes #3825

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:51:21 -04:00
Bill Easton
7825355b98
Promote 7 ty rules from ignore to warn, fix 9 violations (#3852)
Enables stricter type checking by promoting rules that default to
ignore: division-by-zero, possibly-missing-attribute,
possibly-missing-import, possibly-unresolved-reference,
unsupported-dynamic-base, unsupported-operator, unused-ignore-comment.

6 of the 7 rules had zero violations. possibly-unresolved-reference
had 9 (5 in src/, 3 in tests/, 1 walrus-operator false positive
suppressed with ty: ignore).

🤖 Generated with Claude Code

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:50:56 -04:00
Bill Easton
b99b050e80
Enable 7 zero-violation ruff rules: PLE, LOG, RSE, FA, FLY, SLOT, TID (#3841)
🤖 Generated with Claude Code

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 12:50:14 -04:00
Jeremiah Lowin
95102c7d7c
Stop forwarding inbound HTTP headers to unrelated remote servers (#3837) 2026-04-11 12:14:38 -04:00
Bill Easton
d0bcec979c
fix: TransformedTool sync fn crash and schema mutation (#3823)
* fix: TransformedTool sync fn crash, schema mutation, output_schema=False

- Handle sync transform_fn in run() using is_coroutine_function check
  instead of unconditionally awaiting (fixes TypeError crash)
- Deep copy parent property schemas to prevent mutation corruption
- Accept output_schema=False via BeforeValidator (converts to None)
- Remove inaccurate docstring claiming str/None shorthand for transform_args

Fixes #3821

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add regression tests for sync transform_fn and schema mutation

🤖 Generated with Claude Code

Co-authored-by: Jeremiah Lowin <jeremiah@lowin.dev>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jeremiah@lowin.dev>
2026-04-11 11:50:23 -04:00
Bill Easton
790f0bcb47
fix: replace or with is not None checks for config/override merging (#3833)
* fix: replace `or` with `is not None` checks for config/override merging

Falsy-but-valid values like port=0 (OS-assigned), host="" (all interfaces),
and description="" (explicitly cleared) were silently dropped by `x or default`
patterns across CLI, transport, and component registration.

Fixes #3832

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace `or` with `is not None` for description in FunctionResourceTemplate

🤖 Generated with Claude Code

Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2026-04-11 11:50:20 -04:00
Bill Easton
4b59e0d94b
Fix json_schema_to_type crashes on keywords, boolean schemas, empty enums, and name collisions (#3818)
* Fix crash bugs in json_schema_to_type

- Handle boolean schemas (True/False) at the public entry point
- Append trailing underscore to Python keyword property names (PEP 8)
- Return Any for empty enum values instead of crashing Pydantic
- Deduplicate field names after sanitization to prevent collisions
  (e.g. "foo-bar" and "foo_bar" both sanitizing to "foo_bar")

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Move local imports to module level in test_json_schema_type

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-11 11:29:26 -04:00
Bill Easton
468559978a
Add real-world schema crash test against APIs.guru directory (#3826)
Integration test that runs json_schema_to_type against 232K schemas
from 4,120 real-world OpenAPI specs (APIs.guru openapi-directory).
Snapshots crash counts as regression baselines so future changes
can't silently increase the crash rate.

Current baseline (openapi-directory@f7207cf0):
  TypeErrors:   2,342 (datetime serialization)
  SchemaErrors:   273 (invalid regexes in specs)
  Timeouts:         0
  Other:            0

Skipped unless openapi-directory is cloned locally.
Run with: pytest -m integration tests/.../test_real_world_schemas.py

🤖 Generated with Claude Code

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:23:55 -04:00
Bill Easton
eec52b1f02
fix: materialize generators before result conversion, handle bytes gracefully (#3830)
- Detect async/sync generators after tool execution and materialize
  into lists before the result conversion pipeline processes them
- Generator materialization runs inside timeout scope so slow generators
  respect the configured timeout
- Handle bytes return types: UTF-8 bytes as text, non-UTF-8 as base64
- Suppress output_schema for bytes return types (can't be structured JSON)
- Catch UnicodeDecodeError alongside PydanticSerializationError in
  convert_result for robustness

Fixes #3829

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:23:51 -04:00
Bill Easton
c946664a16
Validate actual base64 data size in FileUpload, not client-reported size (#3816)
The store_files tool checked the client-provided `size` field to enforce
max_file_size, but this field is untrusted input. A client could set
size=1 while sending a multi-megabyte payload, bypassing the limit.

Now computes actual size from the base64 data length instead.

🤖 Generated with Claude Code

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:23:49 -04:00
Chris Guidry
db9e2685fb
Bump pydocket>=0.19.0, drop fakeredis pin (#3822)
pydocket 0.19.0 fixes the fakeredis 2.35.0 FakeConnection rename
internally, so we no longer need to carry the fakeredis ceiling
ourselves. Removes the direct fakeredis[lua]<2.35.0 dependency from the
tasks extra entirely — it's just a transitive of pydocket now.

🤖 Generated with Claude Code

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 13:41:18 -04:00
Jeremiah Lowin
faf5f86e09
Version-check is_docket_available() to avoid transitive pydocket crash (#3807) 2026-04-09 21:52:21 -04:00
Jeremiah Lowin
ce9c4bcd53
Note generate-notes API in release workflow docs (#3806) 2026-04-09 18:08:57 -04:00
Jeremiah Lowin
d1adb041ea
Pin fakeredis<2.35.0 in tasks extra (#3804) 2026-04-09 16:28:48 -04:00
Jeremiah Lowin
0194c6e8ea
Document session state isolation across mount boundaries (#3801) 2026-04-09 16:15:57 -04:00
Jeremiah Lowin
6592aaa70e
fix: accept both client_id and identifier_uri as Azure audience (#3797) 2026-04-08 20:25:42 -04:00
dependabot[bot]
9f0d8d3d12
chore(deps): bump the uv group across 2 directories with 1 update (#3795)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 20:08:26 -04:00
aimable100
556fd8fa42
Harden client tool result error handling (#3778) 2026-04-08 13:57:10 -04:00
marvin-context-protocol[bot]
e064ba69de
chore: Update SDK documentation (#3791)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-04-07 21:42:16 -04:00
marvin-context-protocol[bot]
a3c5cc12d3
chore: Update SDK documentation (#3757)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-04-07 19:49:20 -04:00
Jeremiah Lowin
f5be772403
fix: bump ty to >=0.0.29 and suppress new false positives (#3790) 2026-04-07 19:49:09 -04:00
Jeremiah Lowin
f14456d63a
docs: document forward_resource parameter on OAuthProxy (#3788) 2026-04-07 19:42:10 -04:00
Jeremiah Lowin
2b9d3eee0f
fix: use identifier_uri as audience for Azure token validation (#3787)
* fix: use identifier_uri as audience for Azure token validation

* fix test expectations for identifier_uri audience
2026-04-07 18:37:52 -04:00
Jeremiah Lowin
e1ea133fb7
fix: Cognito token verification checks client_id instead of aud (#3786)
* fix: Cognito token verification checks client_id instead of aud

* fix: Cognito token verification checks client_id instead of aud

* handle list audience values in Cognito client_id check
2026-04-07 18:09:43 -04:00
Kakarlamudi Venkata Dhanush
042db1d0e3
Fix OpenAPI 3.0 nullable fields in tool input schemas (#3768)
* Fix OpenAPI 3.0 nullable fields leaking into tool input schemas

* fix: convert nullable fields in input schemas and fix recursion

* Fix unused loop variable in OpenAPI converter

* Refactor OpenAPI nullable conversion and add tests

* Clean up and add integration tests for nullable input schemas

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-07 18:09:26 -04:00
Jeremiah Lowin
e5b96343d1
Fix boolean property schemas in JSON Schema parsing (#3785) 2026-04-07 17:55:51 -04:00
Jeremiah Lowin
3ef9130269
fix: cap consent cookie size to prevent header overflow (#3784) 2026-04-07 17:16:14 -04:00
buyua9
3cb5579253
fix(docs): correct FastMCP tool name in welcome docs (#3781) 2026-04-07 14:48:50 -04:00
Jeremiah Lowin
8ee81b3037
fix: boolean false values dropped in form submissions (#3776)
* fix: boolean false values dropped in form submissions

* revert apps_dev.py boolean coercion
2026-04-07 11:23:34 -04:00
Jeremiah Lowin
bbccc52b60
fix: broken link in changelog (#3775) 2026-04-06 21:16:01 -04:00
Fatia Kusuma Dewi
5587cb7c43
changeable allowed_client_redirect_uris (#3772) 2026-04-06 20:58:09 -04:00
kaiisfree
0212a718c7
Remove deprecated asyncio.iscoroutinefunction fallback (#3767)
The `asyncio.iscoroutinefunction` call is deprecated in Python 3.14
and slated for removal in 3.16. Since `is_coroutine_function` already
unwraps `functools.partial` layers before checking, the asyncio
fallback is redundant on all supported Python versions — it can never
return True when `inspect.iscoroutinefunction` returned False on the
unwrapped function.

Fixes #3765

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 20:00:14 -04:00
Kakarlamudi Venkata Dhanush
c642d6a5cf
fix: propagate upstream_claims in load_access_token (#3750) 2026-04-06 19:55:26 -04:00
Kakarlamudi Venkata Dhanush
07e950ec09
fix: FastAPI TestClient compatibility and lifespan re-initialization (#3736) 2026-04-06 19:53:27 -04:00
Rishav Mitra
99eaeb8af4
fix: substitute server variable defaults when building base URL from OpenAPI spec (#3770)
* fix: resolve OpenAPI 3.x server variables in _create_default_client

When an OpenAPI spec defines server variables (e.g. `https://{region}.api.example.com/v1`),
the default values are now substituted before constructing the httpx client base URL.
Previously, the URL was used as-is, causing all requests to fail for specs that use
server variable templating.

Fixes #1681

* fix: use str.replace instead of format_map for server variable substitution

format_map applies Python string formatting rules, so variable names
like {api.version} would be treated as attribute access and raise errors.
Literal token replacement handles all valid OpenAPI variable names safely.
2026-04-06 19:48:30 -04:00
Jeremiah Lowin
eac646e744
chore: split SDK navigation into standalone $ref file (#3773) 2026-04-06 11:18:14 -04:00
Jeremiah Lowin
4bbc4eec3b
fix: ResponseLimitingMiddleware no longer breaks outputSchema tools (#3756) 2026-04-05 10:36:38 -04:00
mateeaaaaaaa
34e632db7f
fix: add proxy timeouts and modernize networking in apps dev (#3741)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-04-05 09:39:34 -04:00
Jeremiah Lowin
e2f72d863c
chore: split v2 docs navigation into separate file (#3762) 2026-04-04 21:17:39 -04:00
Chris Guidry
d41bcb2c9e
Unify background task context forwarding, fix concurrent dependency bugs (#3710)
* Unify background task context forwarding and fix concurrent dependency bugs

We've been getting a steady trickle of edge-case reports around background tasks
and contextual dependencies over the last few months (#3654, #3656, #3569). Each
one pointed at a different symptom, but they all traced back to the same area:
the way context is negotiated between the "frontend" server and Docket workers
was grown piecemeal, with each new piece of context (access tokens, HTTP headers,
origin request IDs) getting its own Redis key, its own restore function, and its
own ContextVar. This made it hard to reason about what state was available where,
and the shared-instance Dependency pattern made concurrent tasks stomp on each
other's cleanup state.

This takes a step back and reworks the whole thing as a single unified system:

- Dependency subclasses (_CurrentContext, Progress, _CurrentAccessToken, etc.)
  are now stateless factories — __aenter__ returns a fresh per-invocation
  object, so concurrent tasks never share mutable state. Fixes #3654, #3656.

- The three individual context-snapshot Redis keys (access_token, http_headers,
  origin_request_id) are collapsed into a single TaskContextSnapshot stored as
  one JSON key per task. The three _restore_task_* functions and two ContextVars
  they populated are gone.

- Sync functions like get_http_request() and get_access_token() now find the
  snapshot transparently in background tasks via a 3-tier sync fallback:
  ContextVar (set by _CurrentContext for functions with deps) → in-memory dict
  (same-process workers) → sync Redis GET (out-of-process workers). No function
  wrapping needed.

- The _wrap_for_task_http_headers hack is deleted. FunctionTool registers its
  raw function with Docket so Docket sees and resolves ALL dependencies,
  including Docket-native ones like Retry and Timeout.

- ProxyTool.from_mcp_tool() now propagates execution.taskSupport metadata from
  remote tools. Fixes #3569.

- Removed redundant _current_docket/_current_worker ContextVar management from
  Context.__aenter__/__aexit__ (they're only set in the lifespan now).

Closes #3654
Closes #3656
Closes #3569

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address code review feedback

- _OptionalCurrentContext: guard __aexit__ against cleaning up contexts it
  didn't create (check is_background_task before delegating)
- Narrow except clauses in snapshot loading (OSError, JSONDecodeError, etc.
  instead of bare Exception)
- Fix docstrings on register_with_docket for resources/prompts/templates
- Simplify Progress: read ExecutionProgress directly from current_execution
  instead of creating and manually entering a DocketProgress wrapper

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Use pop-on-access transfer buffer instead of bounded LRU cache for snapshots

The in-memory snapshot dict is a transfer mechanism, not a cache. Entries go
in at submission and come out at the worker's first access. Using pop instead
of get means the dict only holds entries during the brief submission-to-execution
window, bounded by task concurrency (~10) rather than a 10,000-entry LRU limit.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Drop in-memory transfer buffer, use sync Redis for all backends

Instead of maintaining an in-memory dict to bridge the async/sync gap, use
a sync Redis client directly. For memory:// backends (fakeredis), shares the
same FakeServer instance via docket._redis.get_memory_server() so data written
by the async Docket client is visible to sync reads. For real Redis, creates a
standard sync connection. No in-process state to manage at all.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Move snapshot operations to TaskContextSnapshot methods

capture(), from_json(), to_json(), save() are now classmethod/instance methods
on the dataclass instead of free functions. Deduplicates JSON parsing that was
copy-pasted between the async and sync load paths.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Trim implementation details from register_with_docket docstrings

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Clarify docket lookup comment in submit_to_docket

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Restore docket/worker ContextVar bridge in Context.__aenter__

Servers that own the Docket (the parent) re-set _current_docket/_current_worker
from their instance attributes when entering a Context. Mounted children skip
this (their _docket is None), so they inherit the parent's value. This is needed
for ASGI deployments where ContextVars set during the lifespan don't propagate
to request handlers.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Key snapshot cache by task_id to prevent cross-task context leakage

Docket workers may reuse the same asyncio context for sequential tasks.
The ContextVar cache now stores (task_id, snapshot) tuples so stale entries
from previous tasks are automatically ignored.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:48:27 -04:00
marvin-context-protocol[bot]
697426b39e
chore: Update SDK documentation (#3755)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-04-03 10:44:08 -04:00
dependabot[bot]
75583a6b1f
chore(deps): bump anthropic from 0.86.0 to 0.87.0 in the uv group across 1 directory (#3742)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 10:43:57 -04:00
Jeremiah Lowin
333aaddff5
fix: remove CSP from tool metadata, keep on resource only (#3754) 2026-04-03 10:22:33 -04:00
Justin Quick (C4ADS)
927a03bee1
use result.object otherwise CallToolResult not scriptable (#3753) 2026-04-03 10:17:53 -04:00
marvin-context-protocol[bot]
a52ab0e92d
chore: Update SDK documentation (#3725)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-03-31 19:32:38 -04:00
dependabot[bot]
1cccdc2d27
chore(deps): bump fastmcp from 3.1.1 to 3.2.0 in /examples/testing_demo in the uv group across 1 directory (#3728)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-31 19:32:25 -04:00
Adam Azzam
c3c41b4402
[codex] Add FastMCP docs telemetry (#3727) 2026-03-31 19:32:09 -04:00
marvin-context-protocol[bot]
4f7668807a
chore: Update SDK documentation (#3712)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-03-31 12:02:59 -04:00
Jeremiah Lowin
61f3feec6a
fix: use explicit None checks for JWT exp validation (#3724) 2026-03-31 12:02:44 -04:00
AIKAWA Shigechika
5be249ada3
fix(google): use sub (user ID) for client_id instead of aud (app ID) (#3722) 2026-03-31 10:33:58 -04:00
Jeremiah Lowin
665514e19a
Add forward_resource flag to OAuthProxy (#3711) 2026-03-30 15:55:23 -04:00
Jeremiah Lowin
f189d1f7fb
Bump pydantic-monty to 0.0.9 (#3707)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-30 11:09:39 -04:00
Jeremiah Lowin
6faa2d61f8
Remove hardcoded prefab-ui version from pinning warnings (#3708) 2026-03-30 11:09:26 -04:00
marvin-context-protocol[bot]
dd8816c6cc
chore: Update SDK documentation (#3701)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-03-30 09:33:47 -04:00
Jeremiah Lowin
d27495960a
docs: note that custom routes are unauthenticated (#3706) 2026-03-30 09:32:38 -04:00
Jeremiah Lowin
4a54be2d5f
Add examples gallery page (#3705) 2026-03-30 09:21:32 -04:00
Jeremiah Lowin
961dd50456
Add interactive map example with geocoding (#3702) 2026-03-30 08:43:58 -04:00
Jeremiah Lowin
f01d0c581c
Add quiz example app, fix dev server empty string args (#3700) 2026-03-29 21:17:53 -04:00
marvin-context-protocol[bot]
85b7efd746
chore: Update SDK documentation (#3694)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-03-29 21:03:58 -04:00
Jeremiah Lowin
27abe3c3f0
Add sales dashboard and live system monitor examples, bump prefab-ui to 0.17 (#3696) 2026-03-29 21:00:15 -04:00
Jeremiah Lowin
493989f231
Improve quickstart: pie chart, interactive row selection, screenshots (#3699) 2026-03-29 16:15:42 -04:00
Jeremiah Lowin
dd7d89aaae
fix: dev apps log panel UX improvements (#3698) 2026-03-29 12:51:05 -04:00
Jeremiah Lowin
9b89eb192d
Add apps quickstart tutorial (#3695) 2026-03-29 12:08:50 -04:00
Jeremiah Lowin
60d2817245
Polish apps docs for 3.2 release (#3693) 2026-03-29 11:01:38 -04:00
Mostafa Kamal
57a7f121d4
Add Clerk OAuth provider (#3677) 2026-03-29 11:01:22 -04:00
dependabot[bot]
d1f7195d7b
chore(deps): bump actions/setup-node from 4 to 6 (#3691)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-29 10:08:01 -04:00
Jeremiah Lowin
596210c2ba
Fix docs gaps in app provider pages (#3690) 2026-03-28 21:26:21 -04:00
Jeremiah Lowin
79821c4eff
Reorganize apps overview TOC (#3689) 2026-03-28 21:18:51 -04:00
marvin-context-protocol[bot]
3354c40992
chore: Update SDK documentation (#3670)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-03-28 21:15:58 -04:00
dependabot[bot]
7ed31e42d4
chore(deps): bump cryptography from 46.0.5 to 46.0.6 in /examples/testing_demo in the uv group across 1 directory (#3685)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-28 21:15:46 -04:00
Jeremiah Lowin
d4289b27c0
Add prefab-ui version pinning warning to docs (#3688) 2026-03-28 21:15:27 -04:00
Jeremiah Lowin
2c08edecec
Add FormInput provider, bump prefab-ui to 0.15.0 (#3687) 2026-03-28 21:15:13 -04:00
Jeremiah Lowin
beb35a4ed8
Add Approval and Choice providers (#3686) 2026-03-28 20:58:17 -04:00
Jeremiah Lowin
8c10bb8881
fix: remove stale ty:ignore directives for ty 0.0.26 (#3684) 2026-03-28 20:02:40 -04:00
Jeremiah Lowin
923695bd9a
fix: strip discriminator after dereferencing schemas (#3682) 2026-03-28 19:46:05 -04:00
Jeremiah Lowin
5338629474
Add FileUpload provider (#3669) 2026-03-28 19:45:43 -04:00
Jeremiah Lowin
90bd4ffaa4
fix: restore HTTP headers in worker execution path for background tasks (#3681) 2026-03-28 18:01:31 -04:00
marvin-context-protocol[bot]
1d8a8bcf36
chore: Update SDK documentation (#3668)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-03-27 21:48:45 -04:00
Miguel Miranda Dias
5879119de5
fix: HTTP request headers not accessible in background task workers (#3631)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-03-27 21:48:33 -04:00
Jeremiah Lowin
5683c0f495
Merge pull request #3667 from PrefectHQ/app-tool-prefixed-names 2026-03-27 21:46:37 -04:00
Anthony James Padavano
16eb2ffcb0
fix: serialize object query params per OpenAPI style/explode rules (#3662)
Object-typed query parameters with explode=true (the default) were
passed as raw Python dicts to httpx, which called str() on them —
producing Python repr syntax (single quotes, capitalized booleans)
instead of proper query parameter serialization.

Per the OpenAPI specification, style=form with explode=true on objects
expands each property as a separate query parameter (e.g.
?myAttribute=true). This change handles dict values in both the
explode=true and explode=false branches of _serialize_query_params,
using the existing _query_scalar_to_str helper for correct boolean
formatting.

Fixes #2857
2026-03-27 21:42:36 -04:00
Bill Easton
2491993327
Fix prompt caching middleware missing wrap/unwrap round-trip (#3666)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 21:42:09 -04:00
Bill Easton
376a4a57e2
Docs: add FileTreeStore sanitization warnings and update examples (#3661)
Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
2026-03-27 21:41:56 -04:00
Jeremiah Lowin
773f6586e0
Route app tool calls via ___-prefixed names instead of _meta
Hosts (Goose, MCP Jam) don't forward _meta on callServerTool, which
broke app tool routing entirely. Encode the app identity in the tool
name on the wire instead: the resolver writes "AppName___tool_name",
and the server parses it to route via get_app_tool.
2026-03-27 21:31:16 -04:00
Jeremiah Lowin
3e1aadb282
Docs: showcase hero, narrative improvements, panel closed by default (#3657) 2026-03-27 14:39:32 -04:00
Jeremiah Lowin
c63be5d178
Add release notes for v3.1.0, v3.1.1, and v2.14.6 (#3659) 2026-03-27 14:34:13 -04:00
Jeremiah Lowin
2a2f4c8e4d
Clean error when dev apps ports are in use (#3658) 2026-03-27 14:31:08 -04:00
Jeremiah Lowin
cd483a1432
docs: improve contributor guidelines for framework contributions (#3653) 2026-03-27 10:43:12 -04:00
Jeremiah Lowin
8ff28c13fa
Fix blocking docs issues: chart imports, Select API, Rx consistency (#3652) 2026-03-27 10:32:22 -04:00
Jeremiah Lowin
b9ea53618d
fix: resolve CurrentFastMCP/ctx.fastmcp to child server in mounted background tasks (#3651) 2026-03-27 10:24:17 -04:00
Jeremiah Lowin
426572670e
Docs: generative UI page, fix imports, add PrefabAppConfig (#3650) 2026-03-27 10:18:58 -04:00
Jeremiah Lowin
b7cb7cea81
Add PrefabAppConfig for customizable Prefab tool setup (#3648) 2026-03-27 10:08:48 -04:00
Jeremiah Lowin
77d8b3ae97
fix: scope deprecation warning filter to FastMCPDeprecationWarning (#3649) 2026-03-27 09:54:08 -04:00
Jeremiah Lowin
ce7758e9b3
Add GenerativeUI provider, bump prefab-ui 0.14.0 (#3647) 2026-03-27 09:20:40 -04:00
Miguel Miranda Dias
59a126a0b6
fix(server): preserve mounted tool task metadata (#3632)
* fix(server): preserve mounted tool task metadata

* fix(server): move task execution metadata to base tool

* cleanup: remove stale import, tighten execution metadata guard

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-03-27 09:15:33 -04:00
Bill Easton
b1505ba5d7
Run MCP conformance tests in CI (#3628)
🤖 Generated with Claude Code

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 22:19:13 -04:00
dependabot[bot]
396bf1572a
chore(deps): bump requests from 2.32.5 to 2.33.0 in the uv group across 1 directory (#3638)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-26 22:17:23 -04:00
Bill Easton
f5804f4761
fix: recover StdioTransport after subprocess exits (#3630)
* fix: recover StdioTransport after subprocess exits

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: improve Windows reliability for stdio crash recovery

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 22:16:54 -04:00
Bill Easton
65ea57963a
fix: filesystem provider import machinery (#3626)
* fix: filesystem provider import machinery

- Temporary sys.path entries (both package and non-package mode) now removed
  immediately after exec_module via try/finally, eliminating permanent process-wide pollution
- Non-package files use bare stem as sys.modules key only if unclaimed; falls back
  to private hash-based key to prevent stdlib shadowing (e.g. json.py clobbering json)
- Reload of private-key modules uses spec.loader.exec_module directly instead of
  importlib.reload, which cannot find files by their private synthetic name
- _find_package_root gains stop_at parameter; discover_and_import passes provider_root
  to prevent package root discovery from escaping above the provider boundary

Closes #3625 (issues 2, 3, 6)

🤖 Generated with Claude Code

* fix: use contextlib.suppress for SIM105 linting

🤖 Generated with Claude Code

* test: add import machinery regression tests

🤖 Generated with Claude Code

* fix: resolve provider_root before path comparison; improve tests

- Resolve provider_root in import_module_from_file so the stop_at boundary
  in _find_package_root works correctly when provider_root is a relative path
  (e.g. FileSystemProvider(Path("./mcp"))) — previously the resolved file_path
  and unresolved stop_at.parent would never compare equal
- Fix test_stdlib_not_shadowed: use unconditional finally to restore sys.modules["json"]
- Strengthen test_same_stem_files: assert mod_a is not mod_b and that sys.modules["helpers"]
  was not clobbered by the second import
- Replace direct _find_package_root unit test with an integration test through
  import_module_from_file(provider_root=...) that also verifies the module name
  and that tmp_path is not added to sys.path

🤖 Generated with Claude Code
2026-03-26 17:53:12 -04:00
Jeremiah Lowin
6c76bea3e8
Add tag to deployment pages (#3624) 2026-03-25 11:39:25 -04:00
Jeremiah Lowin
ec5de0d26f
fix(docs): correct misleading stateless_http header (#3622)
Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2026-03-25 11:00:27 -04:00
marvin-context-protocol[bot]
145dbbfb4c
chore: Update SDK documentation (#3615)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-03-25 10:58:34 -04:00
Jeremy Simon
492db9972f
fix: resolve EntraOBOToken dependency injection through MultiAuth (#3609)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-03-25 10:58:19 -04:00
Jeremiah Lowin
00df243171
Tighten too-long heuristic for design-document issues (#3620) 2026-03-25 10:57:53 -04:00
AIKAWA Shigechika
c3f0223bbb
fix(google): replace deprecated /oauth2/v1/tokeninfo with /oauth2/v3/userinfo (#3603)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-03-25 10:39:34 -04:00
Jeremiah Lowin
1eabe7f74a
Move app modules to fastmcp.apps package (#3616) 2026-03-24 21:51:20 -04:00
marvin-context-protocol[bot]
c04c9d0ce5
chore: Update SDK documentation (#3592)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-03-24 20:26:38 -04:00
Jeremiah Lowin
c397e68d39
Update ty ignore comments for 0.0.25 compatibility (#3614) 2026-03-24 20:26:26 -04:00
Jeremiah Lowin
ca76b82878
Respect OpenAPI content type in request body serialization (#3611) 2026-03-24 18:39:37 -04:00
Jeremiah Lowin
6f4df1eac4
Tune too-long triage heuristic (#3610) 2026-03-24 14:17:39 -04:00
Jeremiah Lowin
7c98326d61
Forward enable_cimd to OAuthProxy in all provider subclasses (#3608) 2026-03-24 13:48:15 -04:00
Jeremiah Lowin
c04ce8972f
Apps Phase 1: docs, examples, app-only tool filtering (#3593)
* Apps Phase 1: docs, examples, app-only tool filtering, Rx() migration

* Clarify architecture page is Prefab-specific

* Fix component reference inaccuracies and DataTable search prop

- Charts import: clarify they must come from prefab_ui.components.charts
- DataTable: searchable→search (the actual prop name), remove nonexistent
  table-level sortable prop
- Select: remove nonexistent options prop, show SelectOption children
- Tabs: default_value→value
- Fix search=True in inventory, patterns, datatable examples

* Consistent Rx usage across all examples, fix imports

* Address review: fix chart imports, Select import, docstring --stdio claims
2026-03-24 13:35:29 -04:00
Jeremiah Lowin
39af26885f
Dev apps: show more/less for long tool descriptions (#3600) 2026-03-23 17:13:31 -04:00
Jeremiah Lowin
d23f553874
Fix dev apps form: union types, textarea support, JSON parsing (#3597) 2026-03-23 15:53:04 -04:00
Jeremiah Lowin
6f30e89dd1
Fix query parameter serialization to respect OpenAPI explode/style settings (#3595)
* Fix query parameter serialization to respect OpenAPI explode setting

* Support pipeDelimited and spaceDelimited query param styles

* Lowercase booleans in comma/pipe/space-joined query values

* Omit empty arrays from query string when explode=false

* Handle object query params with explode=false
2026-03-23 15:19:55 -04:00
dependabot[bot]
b92f5662d7
chore(deps): bump j178/prek-action from 1 to 2 (#3578)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-22 19:59:44 -04:00
marvin-context-protocol[bot]
352525c8e5
chore: Update SDK documentation (#3573)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-03-22 19:59:33 -04:00
Jeremiah Lowin
e5dce51286
Fix app tool routing: visibility check and middleware propagation (#3591) 2026-03-22 19:59:23 -04:00
Rushabh Doshi
204e566227
Fix loopback redirect URI port matching per RFC 8252 §7.3 (#3589)
🤖 Generated with Claude Code

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 19:35:02 -04:00
Jeremiah Lowin
96497acd16
Route app tool calls through provider chain, remove process-level registry (#3587)
Adds Provider.get_app_tool(app_name, tool_name) — a dedicated method for
finding app-visible tools by their original name, bypassing transforms.
AggregateProvider queries children, WrappedProvider delegates to inner,
FastMCPProvider delegates to nested server. The default implementation
checks _get_tool and matches meta.fastmcp.app.

This replaces the process-level _APP_TOOLS registry. Tool routing now
works through the provider tree, which exists in every process — no
shared state needed for horizontal scaling.
2026-03-22 19:29:33 -04:00
Jeremiah Lowin
52feff6878
Transparently refresh upstream token in OAuthProxy.load_access_token() (#3584)
* Transparently refresh upstream token in OAuthProxy.load_access_token()

When upstream token validation fails during load_access_token, attempt
to refresh using the stored refresh token before returning None. This
prevents premature 401s that force clients into expensive full re-auth
flows when the upstream token expires.

Co-authored-by: Claude <noreply@anthropic.com>

* Gate transparent refresh on token expiry, add advisory lock

Only attempt upstream refresh when the token is actually expired, not
on any validation failure (scope mismatch, revocation, etc.). Add
per-token advisory lock to prevent concurrent async tasks from racing
to refresh the same upstream token.

* Re-check expiry inside lock, reload from storage after refresh failure

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-22 10:43:44 -04:00
Jeremiah Lowin
d27c26e0ff
Replace UUID global keys with (app_name, tool_name) registry (#3585)
* Replace UUID global keys with (app_name, tool_name) registry

Collapses three module-level registries (_APP_TOOL_REGISTRY,
_FN_TO_GLOBAL_KEY, _NAME_TO_GLOBAL_KEY) into one: _APP_TOOLS keyed by
(app_name, tool_name). Removes UUID generation, global key stamping in
metadata, and the complex resolver that mapped callables and strings
through multiple fallback paths.

The server now reads _meta.fastmcp.app from the MCP request (set by the
Prefab renderer) and routes directly to the named app's tool. Two apps
with the same tool name are disambiguated by app name, not by UUID.

The resolver is simplified to pass-through: CallTool("save") serializes
as "save", and the server resolves it at call time using the app context.

* Read app name from _meta.prefab.app to match Prefab renderer

* Inject _meta.fastmcp.app into @app.ui() structured content

The @app.ui() decorator stores the FastMCPApp name in the tool's metadata.
When the tool result is serialized, _prefab_to_json injects it as
_meta.fastmcp.app in the structured content. The Prefab renderer reads
this on init and echoes it back as _meta.fastmcp.app on every
callServerTool call, completing the routing loop.
2026-03-22 10:41:50 -04:00
Jeremiah Lowin
9d1a9cccd8
Add release instructions to CLAUDE.md (#3583) 2026-03-22 09:57:47 -04:00
Marcus Shu
2d7bc4e73d
Add encoding parameter to FileResource (#3580)
* feat: Add encoding parameter to FileResource

- Add optional encoding field (str | None, default None) to FileResource.
- Pass encoding through to read_text() for cross-platform text file reading.
- Preserve backward compatibility by defaulting to system encoding.

* test: Add tests for FileResource encoding parameter

- Test UTF-8 reading with explicit encoding for non-ASCII content.
- Test backward compatibility when no encoding is specified.
- Test that encoding is ignored for binary file reads.
- Test Latin-1 reading with matching encoding.

* docs: Document FileResource encoding parameter

- Add encoding="utf-8" to FileResource example in resource classes guide.
- Update FileResource description to mention encoding support.

* feat: Change FileResource encoding default from None to utf-8

- Default to utf-8 instead of system encoding to prevent cross-platform footgun.
- Update field description to reflect new default.
- Update test to verify default encoding is utf-8 with non-ASCII content.
- Remove redundant encoding="utf-8" from docs example since it is now the default.
2026-03-22 09:55:16 -04:00
Jeremiah Lowin
718ed8b140
fix: preserve tool properties named 'title' during schema compression (#3582) 2026-03-22 09:48:20 -04:00
Jeremiah Lowin
1be9b4d39c
Comprehensive MCP Apps docs, string CallTool resolution (#3575)
* Comprehensive MCP Apps docs, string CallTool resolution, bump prefab-ui >=0.13.0

Rewrites the apps documentation as a learning journey: overview → Prefab apps
→ FastMCPApp → patterns → dev tools → custom HTML. Adds a new FastMCPApp page
covering composable apps with @app.tool()/@app.ui(), CallTool, forms, actions,
and composition. Teaches Rx() and set_initial_state() as the primary state API.

Adds string-based CallTool resolution so CallTool("save_contact") resolves to
the tool's global key, matching callable ref behavior. Requires prefab-ui 0.13.0
which passes strings through the tool resolver.

* Detect ambiguous string CallTool resolution across apps

* Simplify string name registry to plain dict (last-write-wins)
2026-03-21 19:44:11 -04:00
Jeremiah Lowin
85faad59a1
Add MCP message inspector to dev apps UI (#3570) 2026-03-21 12:20:16 -04:00
Jeremiah Lowin
faef74b374
fix: store absolute token expiry to prevent stale expires_in on reload (#3572) 2026-03-21 12:12:50 -04:00
Bill Easton
196e2b48c5
fix: increase sleep duration in proxy cache tests (#3567) 2026-03-21 11:38:58 -04:00
nate nowack
f01c8fd7f4
Update startup banner deploy URL to Prefect Horizon (#3557)
🤖 Generated with Claude Code

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 10:17:48 -04:00
marvin-context-protocol[bot]
55094fd49b
chore: Update SDK documentation (#3551)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-03-18 18:48:36 -04:00
Jeremiah Lowin
27e65fc978
Bump anthropic minimum to 0.48.0 for Base64ImageSourceParam (#3553) 2026-03-18 15:37:25 -04:00
Jeremiah Lowin
ff20c56c85
Add review-pr skill for Codex bot workflow (#3552) 2026-03-18 15:30:29 -04:00
Jeremiah Lowin
269c9c9f46
Extract TokenCache utility, add caching to GitHubTokenVerifier (#3547)
* feat: add TokenCache utility and caching to GitHubTokenVerifier

Extract the caching machinery from IntrospectionTokenVerifier into a
shared TokenCache class in fastmcp.utilities.token_cache, then wire
it into both IntrospectionTokenVerifier and GitHubTokenVerifier.

* Remove dead constant, validate negative cache params

* Fix overwrite eviction bug, skip cache on scope lookup failure
2026-03-18 15:26:09 -04:00
Jeremiah Lowin
02d55dec32
Graceful degradation for multi-server proxy setup (#3546)
* Graceful degradation for multi-server proxy setup

* Add comment for broad except, parameterize test ordering
2026-03-18 15:25:56 -04:00
Jeremiah Lowin
734b93b999
Support ImageContent and AudioContent in sampling handlers (#3550)
* feat: support ImageContent and AudioContent in sampling handlers

Co-authored-by: Claude <noreply@anthropic.com>

* Validate image MIME types, fix silent drop in assistant list messages

* Reject image/audio in assistant messages with tool_calls

* Reject ImageContent in assistant messages for Anthropic

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-18 15:25:16 -04:00
Jeremiah Lowin
9aa31d57f2
fix: suppress output schema for ToolResult subclass annotations (#3548)
* fix: suppress output schema for ToolResult subclass annotations

* use issubclass_safe/is_class_member_of_type for ToolResult subclass checks

* use parsed_fn.return_type for ToolResult check in transform fallback
2026-03-18 15:22:37 -04:00
Jeremiah Lowin
a50dcd8705
fix: filter warnings by message in KEY_PREFIX test (#3549) 2026-03-18 11:33:19 -04:00
dependabot[bot]
62dcea64c2
chore(deps): bump pyasn1 in the uv group across 1 directory (#3538)
Bumps the uv group with 1 update in the / directory: [pyasn1](https://github.com/pyasn1/pyasn1).


Updates `pyasn1` from 0.6.2 to 0.6.3
- [Release notes](https://github.com/pyasn1/pyasn1/releases)
- [Changelog](https://github.com/pyasn1/pyasn1/blob/main/CHANGES.rst)
- [Commits](https://github.com/pyasn1/pyasn1/compare/v0.6.2...v0.6.3)

---
updated-dependencies:
- dependency-name: pyasn1
  dependency-version: 0.6.3
  dependency-type: indirect
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-18 10:38:45 -04:00
marvin-context-protocol[bot]
754fb01088
chore: Update SDK documentation (#3541)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-03-18 10:38:33 -04:00
Jeremiah Lowin
360c9c31e5
fix: resolve Pyright "Module is not callable" on @tool, @resource, @prompt decorators (#3540)
* pin pydantic-monty to 0.0.8

* rename tool/prompt/resource base modules to avoid decorator name shadow

* add sys.modules shims for old submodule import paths

* preserve original module paths in deprecation warnings

* clarify when sys.modules shims can be removed
2026-03-17 18:11:42 -04:00
Jeremiah Lowin
6bffb22c6c
pin pydantic-monty to 0.0.8 (#3539) 2026-03-17 17:03:37 -04:00
Jeremiah Lowin
3407671ac2
chore: gitignore .claude/worktrees/ (#3529) 2026-03-16 14:50:15 -04:00
marvin-context-protocol[bot]
25d550caf1
chore: Update SDK documentation (#3478)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-03-15 16:51:51 -04:00
Jeremiah Lowin
f9ed06176a
fix: route ResourcesAsTools/PromptsAsTools through server middleware (#3495)
* fix: enforce auth/visibility in ResourcesAsTools and PromptsAsTools for non-FastMCP providers

🤖 Co-authored-by: Claude <noreply@anthropic.com>

* fix: honor stdio auth bypass and correct transform ordering in provider wrappers

Co-authored-by: Claude <noreply@anthropic.com>

* fix: move context/dependencies imports into function to break circular import

* fix: route ResourcesAsTools/PromptsAsTools through ctx.fastmcp

Instead of manually reimplementing auth, visibility, and session
transforms in the transform layer, tool functions now call
ctx.fastmcp.read_resource() / ctx.fastmcp.render_prompt() which
routes through the server's full middleware chain. This matches
the pattern CodeMode uses with ctx.fastmcp.call_tool().

The isinstance(provider, FastMCP) branching is removed entirely.

* feat: add _scope parameter for provider-scoped listing

AggregateProvider can now filter which child providers to query when
listing components. ResourcesAsTools and PromptsAsTools use this to
scope listings to their configured provider while still routing
through ctx.fastmcp for full middleware coverage.

The scope matching walks wrapped providers, so a
WrappedProvider(Namespace, inner=MyProvider) matches if MyProvider
is in the scope list.

* test: add coverage for ResourcesAsTools scoped to a sub-server

* fix: delegate to super() when _scope is None, add AggregateProvider to scope matching

* simplify: remove _scope machinery, route everything through ctx.fastmcp

Reverts the _scope parameter from Provider/AggregateProvider/Server.
ResourcesAsTools and PromptsAsTools now simply route through
ctx.fastmcp for all operations. Apply to a FastMCP server instance
for proper auth/visibility/middleware coverage.

Tests rewritten to use FastMCP server directly instead of raw providers.

* warn when ResourcesAsTools/PromptsAsTools is applied to a non-FastMCP provider

* docs: explain that ResourcesAsTools/PromptsAsTools should wrap a FastMCP server

* raise TypeError instead of warning when applied to non-FastMCP provider

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-15 16:35:54 -04:00
Jeremiah Lowin
bb37c3bb6b
fix: reject refresh tokens used as Bearer access tokens (#3524) 2026-03-15 15:21:30 -04:00
Jeremiah Lowin
b0b3a6dd95
fix: use raw strings for regex in pytest.raises match (#3523) 2026-03-15 15:14:15 -04:00
Jeremiah Lowin
b2d4cc90e5
fix: validate server names in install commands (#3522)
* fix: validate server names in install commands

* Scope validation to shell-backed install paths only
2026-03-15 14:54:24 -04:00
Jeremiah Lowin
8fdb3cc27c
fix: CSRF double-submit cookie check in consent flow (#3519)
* Upgrade examples/testing_demo lockfile, drops diskcache (CVE-2025-69872)

* fix: add CSRF double-submit cookie check to consent flow (GHSA-rww4-4w9c-7733)

* fix: preserve CSRF state across concurrent flows, fix test isolation

* fix: reject non-__Host consent-state cookie on HTTPS
2026-03-15 14:22:01 -04:00
Jeremiah Lowin
93753e137e
Claude/review contributor guidelines (#3517)
* Tighten contributor guidelines and too-long triage

🤖 Generated with Claude Code

https://claude.ai/code/session_01VTVuVM1t2Mas8F6z8j3wJh

* Relax issue length guidance from ~15 lines to "fit in a single screen"

🤖 Generated with Claude Code

https://claude.ai/code/session_01VTVuVM1t2Mas8F6z8j3wJh

* Fix conflicting no-comment rule for too-long label

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-15 14:15:21 -04:00
Jeremiah Lowin
e0455fec95
Update security policy (#3521) 2026-03-15 14:12:17 -04:00
Jeremiah Lowin
00ed31f2cc
Upgrade examples/testing_demo lockfile, drops diskcache (CVE-2025-69872) (#3518) 2026-03-15 12:00:51 -04:00
Matt Hallowell
943af98b0d
move stateless_http transport kwarg to http_app as FastMCP constructor no longer supports stateless_http as kwarg (#3510) 2026-03-15 11:51:43 -04:00
Jeremiah Lowin
32dfe50f39
Treat refresh_expires_in=0 as missing, fall back to 30-day default (#3514)
Keycloak returns refresh_expires_in=0 for offline tokens (offline_access scope),
meaning "no fixed time-based expiry". The truthiness check on this value caused
the proxy to skip issuing a PROXY_RT, forcing browser re-auth every hour.

Closes #3509

🤖 Generated with Claude Code

Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2026-03-15 11:49:50 -04:00
Jeremiah Lowin
b5a45034ab
Add security label to triage and release notes (#3516) 2026-03-15 11:43:16 -04:00
Jeremiah Lowin
d1ed7cd24d
Bump PyJWT >= 2.12.0 (CVE-2026-32597) (#3515)
* Bump PyJWT >= 2.12.0 (CVE-2026-32597)

* Move PyJWT floor to azure optional dependency group

* Bump PyJWT in examples/testing_demo lockfile
2026-03-15 11:35:42 -04:00
Jeremiah Lowin
226bfb49fa
fix: remove forced follow_redirects from httpx_client_factory calls (#3496)
* fix: stop passing follow_redirects to httpx_client_factory

Remove the `follow_redirects=True` kwarg that was being forced onto
custom httpx_client_factory calls with a type: ignore suppression.
The McpHttpClientFactory protocol does not include follow_redirects,
so this was a protocol violation. httpx already strips Authorization
headers on cross-origin redirects via its _redirect_headers mechanism.

🤖 Co-authored-by: Claude <noreply@anthropic.com>

* fix: restore follow_redirects=True for custom httpx client factories

httpx already strips Authorization headers on cross-origin redirects,
so follow_redirects is safe to keep. Removing it broke redirect
handling for users providing custom factories.

* fix: remove vacuous test that never invoked connect_session

The test asserted on received_kwargs but never called connect_session,
so the factory was never invoked and the assertion was a no-op.

* fix: use AsyncClient with transport= instead of monkey-patching _transport
2026-03-15 11:30:41 -04:00
Jeremiah Lowin
3bea192c8c
fix: prefer IdP-granted scopes over client-requested scopes in OAuthProxy (#3492)
* fix: use IdP-granted scopes instead of client-requested scopes in OAuthProxy

* fix: use parse_scopes instead of split for IdP scope strings

Some providers (e.g. GitHub) return comma-delimited scopes like
"repo,gist" rather than the RFC 6749 space-delimited format.

* Merge origin/main into fix/oauth-proxy-use-idp-granted-scopes

* fix: remove unrelated transform and http.py changes from PR scope

* fix: remove accidentally staged worktree directories
2026-03-15 11:29:58 -04:00
Jeremiah Lowin
b720fc5e38
fix: prevent path traversal in skill download (#3493)
* fix: prevent path traversal in skill download via malicious skill names

Co-authored-by: Claude <noreply@anthropic.com>

* fix: resolve skill_dir once and use consistently to prevent overwrite bypass

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-15 11:22:13 -04:00
Jeremiah Lowin
ea529f6a49
feat: make upstream_client_secret optional in OAuthProxy (#3486)
* feat: make upstream_client_secret optional in OAuthProxy

Extract _create_upstream_oauth_client() factory method for subclass
override. Cookie signing falls back to JWT key material when no secret.

* fix: include client_id in revocation requests for public clients

* fix: use factory method for revocation auth
2026-03-15 11:14:56 -04:00
dependabot[bot]
abc89879a7
Bump actions/create-github-app-token from 2 to 3 (#3511)
Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 2 to 3.
- [Release notes](https://github.com/actions/create-github-app-token/releases)
- [Commits](https://github.com/actions/create-github-app-token/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/create-github-app-token
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-15 10:48:40 -04:00
Jeremiah Lowin
40bdfb6b1d
fix: URL-encode path params to prevent SSRF/path traversal (GHSA-vv7q-7jx5-f767) (#3507)
* fix: URL-encode path params in OpenAPI provider to prevent SSRF/path traversal

Co-authored-by: Claude <noreply@anthropic.com>

* Exempt too-long from core-category requirement in triage

* fix: also encode dots in path params to prevent bare .. traversal

* fix: only encode .. (not all dots) to preserve valid dotted values

* fix: encode all dots in path params to prevent single-dot normalization

* fix: check decoded path stays within prefix in double-encoding test

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-15 10:45:24 -04:00
Jeremiah Lowin
c861862aed
Add too-long label to Marvin triage workflow (#3508)
* Add too-long label to Marvin triage workflow

* Exclude .claude/ from loq file size checks

* Exempt too-long from core-category requirement in triage
2026-03-14 16:48:33 -04:00
Jeremiah Lowin
959ac2b111
Auto-close upgrade check issue when build passes (#3505)
* Auto-close upgrade check issue when build goes green

Closes #3484

* Restrict close-on-success to main branch
2026-03-14 16:46:38 -04:00
Jeremiah Lowin
9be42d9013
perf: reduce PBKDF2 iterations in tests, fix warnings and timeouts (#3504)
- Use 10 PBKDF2 iterations in test_mode (vs 1M in production) for
  JWT key derivation — cuts auth test setup from ~2.5s to <0.1s
- Add timeout(15) to subprocess-spawning tests (TestKeepAlive,
  test_mcp_config) that exceed 5s under parallel CI load
- Remove pytestmark filterwarnings overrides in tests/deprecated/
  that were leaking DeprecationWarning to test output
- Fix deprecated add_tool_transformation() usage in test_authorization
- Document new settings in settings.mdx
2026-03-14 16:42:06 -04:00
Jeremiah Lowin
7017106c63
fix: restrict $ref resolution to local refs only (SSRF/LFI) (#3502)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-14 16:35:53 -04:00
Jeremiah Lowin
71ba030380
fix: reject empty/OIDC-only required_scopes in AzureProvider (#3503)
🤖 Generated with Claude Code
2026-03-14 16:35:43 -04:00
Jeremiah Lowin
5ff64ce21b
fix: handle re.error from malformed URI templates in build_regex (#3501) 2026-03-14 16:34:06 -04:00
Jeremiah Lowin
d5d5c45be3
Add contributing guidelines and update issue/PR templates (#3485)
* Add contributing guidelines, update issue and PR templates

* Fix CONTRIBUTING.md link paths in issue templates

Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>

---------

Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2026-03-14 16:25:35 -04:00
Jeremiah Lowin
ca8069cb86
Enforce per-tool auth checks in SamplingTool.from_callable_tool wrapper (#3494)
Co-authored-by: Claude <noreply@anthropic.com>
2026-03-14 16:14:50 -04:00
nightcityblade
01c57a9e04
fix: validate workspace path is a directory before cursor install (#3440)
Add an is_dir() check after the existence check in
install_cursor_workspace() to provide a clear error message when
a file path is passed instead of a directory.

Fixes #3426

Co-authored-by: nightcityblade <nightcityblade@gmail.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-03-14 16:14:03 -04:00
Jeremiah Lowin
32f1118a11
Expose minimum_check_interval, reduce task pickup latency (#3500)
* perf: expose minimum_check_interval, reduce task pickup latency

The Docket Worker polls for new tasks every minimum_check_interval
(previously hardcoded to 250ms in pydocket). Expose this setting so
users can tune it, default to 50ms, and override to 10ms in tests.

This cuts average task pickup latency from ~125ms to ~5ms per task.

* perf: reduce task test overhead and eliminate cross-test contamination

- Expose minimum_check_interval setting (default 50ms, 10ms in tests)
  to reduce Docket Worker task pickup latency
- Isolate fakeredis per test via unique memory:// URLs to prevent
  stale _async_blocking tasks from contaminating subsequent tests
- Make client disconnect timeout configurable (default 5s, 1s in tests)
- Add --durations=50 to CI for passive performance regression detection
- Remove 15s timeout band-aids from task test conftest files
- Add explicit @pytest.mark.timeout(10) to cancellation tests
- Fix deprecated FastMCP.as_proxy() usage in test_task_proxy.py
2026-03-14 16:08:28 -04:00
Jeremiah Lowin
cedf734f15
fix: task test teardown hanging 5s per test (#3499)
Fix context manager ordering in FastMCPTransport so the task group
(server run + subscriptions) is cancelled before the lifespan
(Docket Worker) tears down. Also break subscription loop on terminal
task states.

Closes #3498
2026-03-14 15:33:44 -04:00
Jeremiah Lowin
e35a74f004
Add fastmcp dev apps command with browser UI preview (#3489)
* Add FastMCPApp — a Provider for composable MCP applications

* Wire Prefab callable resolver via to_json(tool_resolver=) parameter

* Remove inspect.signature compat check, use try/except until prefab 0.10.0

* Address review: fix add_tool registry gaps, normalize auth errors, bump prefab to 0.10.0

* Register global key after _add_component succeeds

* Simplify: extract decorator dispatch, use get_fastmcp_meta, expose get_global_tool

* Remove prek from Marvin workflows

These workflows run Claude to respond to /marvin mentions — linting
the repo is unnecessary and fails without renderer deps installed.

* Return ResolvedTool from callable resolver, add contacts example

The callable resolver now returns ResolvedTool (from prefab_ui) instead of a
plain string, carrying metadata like unwrap_result that the renderer needs to
correctly handle structuredContent envelopes. The unwrap_result flag is derived
from the tool's x-fastmcp-wrap-result output schema marker.

* Bump prefab-ui requirement to >=0.11.0

* Remove stale ty ignore comments now that prefab-ui 0.11 is published

* Add fastmcp dev apps command with browser UI preview

* Improve fastmcp dev apps: dropdown picker, reload flag, process cleanup

- Replace Tabs with Pages+Select for tool picker (Rx-based reactive state)
- Add --reload/--no-reload flag (default: True) to fastmcp dev apps
- Kill entire process group on shutdown so port 8000 is freed properly
- Suppress uvicorn websockets deprecation warning (websockets-sansio)
- Bump prefab-ui to >=0.11.1 (fixes get_renderer_head bug in 0.11.0)
- Add farewell tool to greet_server example for multi-tool testing

* Add docs for fastmcp dev apps command

* Fix orphaned server on startup failure, guard Unix-only signal handling

* Show tool title in picker, remove editable prefab source

* Bump prefab-ui to >=0.11.2

* Fail fast when prefab-ui is not installed

* Add apps/development docs, link from prefab and sidebar

* Fix optional field defaults, fail with non-zero on startup timeout
2026-03-14 12:29:37 -04:00
Hakancan
11960b1e19
fix: monty 0.0.8 compatibility, drop external_functions from constructor (#3468)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-03-14 12:23:54 -04:00
Sascha Buehrle
dc7ae0bad1
fix: remove hardcoded version from CLI help text (#3456)
Changed 'FastMCP 2.0' to 'FastMCP' per maintainer feedback.
Dynamic version via fastmcp.__version__ still available via version flag.
2026-03-14 12:07:24 -04:00
Jeremiah Lowin
0b8479ad73
Add verify parameter for SSL certificate configuration (#3487)
* feat: add `verify` parameter for SSL certificate configuration

* Propagate verify to OAuth preflight clients

* Propagate verify to pre-constructed OAuth instances

* Fix verify override not propagating to existing OAuth factory

* Warn when both httpx_client_factory and verify are provided

* Preserve user-provided OAuth factory when transport has verify

* Skip OAuth re-sync when transport has custom httpx_client_factory
2026-03-14 11:48:32 -04:00
Matthieu B
7235029486
feat: type require_authorization_consent as bool | Literal["external"] (#3473)
When set to "external", the built-in consent screen is skipped
(same as False) but no security warning is logged, since consent
is handled externally by the upstream IdP.

Forwarded through all OAuthProxy subclasses: GoogleProvider,
GitHubProvider, AzureProvider, DiscordProvider, WorkOSProvider,
OIDCProxy, Auth0Provider, AWSCognitoProvider, and OCIProvider.
2026-03-14 11:40:51 -04:00
Jeremiah Lowin
139d2d8f96
Propagate x-fastmcp-wrap-result in tool result _meta (#3490)
* Propagate x-fastmcp-wrap-result flag in tool result _meta

🤖 Generated with Claude Code

Co-authored-by: Claude <noreply@anthropic.com>

* Skip listTools round-trip when _meta has x-fastmcp-wrap-result

🤖 Generated with Claude Code

Co-authored-by: Claude <noreply@anthropic.com>

* Use namespaced meta key: {"fastmcp": {"wrap_result": true}}

🤖 Generated with Claude Code

Co-authored-by: Claude <noreply@anthropic.com>

* Clean up _parse_call_tool_result: hoist cast import, document local CallToolResult import, extract fastmcp_meta

🤖 Generated with Claude Code

Co-authored-by: Claude <noreply@anthropic.com>

* Merge _meta in tasks result handler instead of overwriting

🤖 Generated with Claude Code

* Preserve type validation in meta-based unwrap path

🤖 Generated with Claude Code

* Fix type validation for wrapped task results, guard non-dict meta

🤖 Generated with Claude Code

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-14 11:09:46 -04:00
Jeremiah Lowin
e2bdc9288b
Support logging/setLevel and add client_log_level setting (#3491) 2026-03-14 10:36:01 -04:00
d 🔹
68e76fea2e
fix: forward custom_route endpoints from mounted servers (#3462)
* fix: forward custom_route endpoints from mounted servers

When a child server with custom HTTP routes (registered via
@server.custom_route()) is mounted onto a parent, the routes were
silently dropped because _get_additional_http_routes() only returned
self._additional_http_routes without recursing into mounted providers.

This caused 404s for endpoints like /readyz health checks that worked
in v2 but broke in v3 (regression).

The fix updates _get_additional_http_routes() to traverse providers,
unwrap _WrappedProvider layers (from namespace transforms), find
FastMCPProvider instances, and recursively collect their server's
custom routes.

Fixes #3457

* fix: narrow type annotation from BaseRoute to Route

All items in _additional_http_routes are Route objects (created via
Route(...) in custom_route()). Using list[Route] instead of
list[BaseRoute] fixes the ty type checker failure where .path is
accessed on BaseRoute which doesn't have that attribute.

Removes unused BaseRoute imports from both server.py and transport.py.

* fix: revert route type to list[BaseRoute] to fix ty errors

The previous commit narrowed _additional_http_routes from list[BaseRoute]
to list[Route], which broke:
- component_manager appending Mount objects (Mount is BaseRoute, not Route)
- tests assigning list[BaseRoute] variables (generics are invariant)

Revert to list[BaseRoute] and use isinstance(r, Route) guards in tests
for type-safe .path access.

* fix: remove unused import and fix import grouping

- Remove unused `Route` import from server.py
- Fix import grouping in test_advanced.py (ruff check)

* Address review: move imports to module root, type Provider, add collision note

* fix: sort imports in transport.py

---------

Co-authored-by: voidborne-d <voidborne-d@users.noreply.github.com>
Co-authored-by: d 🔹 <258577966+voidborne-d@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-03-13 20:37:22 -04:00
dependabot[bot]
fcae127664
Bump authlib (#3390)
Bumps the uv group with 1 update in the /examples/testing_demo directory: [authlib](https://github.com/authlib/authlib).


Updates `authlib` from 1.6.6 to 1.6.7
- [Release notes](https://github.com/authlib/authlib/releases)
- [Changelog](https://github.com/authlib/authlib/blob/main/docs/changelog.rst)
- [Commits](https://github.com/authlib/authlib/compare/v1.6.6...v1.6.7)

---
updated-dependencies:
- dependency-name: authlib
  dependency-version: 1.6.7
  dependency-type: indirect
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-13 19:47:58 -04:00
Jeremiah Lowin
24d7aefe28
fix: shield lifespan teardown from cancellation (#3480)
* fix: shield lifespan teardown from cancellation

Co-authored-by: Claude <noreply@anthropic.com>

* fix: stabilize flaky task and timeout tests under parallel execution

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-13 19:47:42 -04:00
Jeremiah Lowin
e41e1fec10
fix: resolve ty 0.0.23 type-checking errors and bump pin (#3481) 2026-03-13 19:45:14 -04:00
Jeremiah Lowin
a52036ed0f
Cache component lists in ProxyProvider (#3479)
* fix: cache component lists in ProxyProvider to avoid redundant backend connections

Every call_tool through a proxy was triggering _list_tools() to resolve
the tool by name, opening a full MCP session just for the lookup, then
opening a second session for the actual execution. This caches component
lists on the ProxyProvider with a configurable TTL (default 300s),
cutting backend handshakes in half for repeated calls.

* docs: document component caching and session reuse for proxy providers

* fix: add sleep in cache TTL test for Windows clock resolution

* docs: clarify cache scope and dynamic backend guidance
2026-03-13 19:44:45 -04:00
Jeremiah Lowin
cfff8e8d09
remove "good first issue" from triage workflow (#3482) 2026-03-13 19:03:09 -04:00
Jeremiah Lowin
59a6fb3839
fix: normalize Google scope shorthands and surface valid_scopes (#3477)
* fix: normalize Google scope shorthands and surface valid_scopes

Google accepts shorthand scopes like "email" in authorization requests but
returns full URIs like "https://www.googleapis.com/auth/userinfo.email" in
token responses. The verifier now normalizes shorthands at initialization so
the subset check works regardless of which form was used. GoogleProvider also
now exposes valid_scopes for controlling which scopes clients can request
beyond the required minimum.

Co-authored-by: Claude <noreply@anthropic.com>

* remove unused GOOGLE_SCOPE_ALIASES_REVERSE

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-13 18:42:16 -04:00
Jeremiah Lowin
e1338e06a2 Set readOnlyHint=True on ResourcesAsTools generated tools 2026-03-13 17:32:21 -05:00
Jeremiah Lowin
f7d46e7747 Add dependency version compatibility guidance to code-review skill 2026-03-13 17:24:53 -05:00
marvin-context-protocol[bot]
2c451bf6ba
chore: Update SDK documentation (#3378)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-03-13 18:07:14 -04:00
d 🔹
33c3acfc87
fix: use intent-based flag for OIDC scope patch in load_access_token (#3465)
When OIDCProxy has verify_id_token=True and the IdP issues the same JWT
for both access_token and id_token, the value-equality check
`verification_token != upstream_token_set.access_token` evaluated to
False, skipping the scope patch entirely. This left AccessToken.scopes
empty, causing RequireAuthMiddleware to return 403 insufficient_scope.

Replace the value-equality check with an intent-based virtual method
`_uses_alternate_verification()` that OIDCProxy overrides to return
`self._verify_id_token`. The base OAuthProxy returns False (preserving
existing behavior for non-OIDC providers).

Fixes #3461

Co-authored-by: voidborne-d <voidborne-d@users.noreply.github.com>
2026-03-13 17:59:03 -04:00
Martim Santos
f08205789e
fix: add version to components (#3458) 2026-03-13 17:33:50 -04:00
Jeremiah Lowin
30308332bb
Add FastMCPApp — a Provider for composable MCP applications (#3385)
* Add FastMCPApp — a Provider for composable MCP applications

* Wire Prefab callable resolver via to_json(tool_resolver=) parameter

* Remove inspect.signature compat check, use try/except until prefab 0.10.0

* Address review: fix add_tool registry gaps, normalize auth errors, bump prefab to 0.10.0

* Register global key after _add_component succeeds

* Simplify: extract decorator dispatch, use get_fastmcp_meta, expose get_global_tool

* Remove prek from Marvin workflows

These workflows run Claude to respond to /marvin mentions — linting
the repo is unnecessary and fails without renderer deps installed.

* Return ResolvedTool from callable resolver, add contacts example

The callable resolver now returns ResolvedTool (from prefab_ui) instead of a
plain string, carrying metadata like unwrap_result that the renderer needs to
correctly handle structuredContent envelopes. The unwrap_result flag is derived
from the tool's x-fastmcp-wrap-result output schema marker.

* Bump prefab-ui requirement to >=0.11.0

* Remove stale ty ignore comments now that prefab-ui 0.11 is published
2026-03-09 13:46:14 -04:00
Jeremiah Lowin
08f590fa9e
Sanitize untrusted output in fastmcp list and fastmcp call (#3409)
* Sanitize untrusted CLI client output

🤖 Generated with GPT-5.2-Codex

* Fix ruff format violations
2026-03-07 12:31:46 -05:00
Jeremiah Lowin
5ed14650ab
Block insecure HS* JWT verification with JWKS/public keys (#3430)
* Block HS* JWT verification with public keys/JWKS

🤖 Generated with GPT-5.2-Codex

* Fix ruff format violations

🤖 Generated with Claude Code

* Handle bytes public_key in HS* algorithm PEM check
2026-03-07 12:20:48 -05:00
Jeremiah Lowin
ad517d09fa
Fix async partial callables rejected by iscoroutinefunction (#3438)
* Fix async partial callables rejected by iscoroutinefunction (#3423)

Add `is_coroutine_function()` utility that unwraps `functools.partial`
before checking, and guard `isroutine` checks so partials aren't
misrouted through `__call__`.

* Also check asyncio.iscoroutinefunction in is_coroutine_function
2026-03-07 12:10:09 -05:00
Jeremiah Lowin
bafd5419fa
Redact sensitive headers in OpenAPI provider debug logging (#3436)
* Redact sensitive headers in OpenAPI provider debug logging (#3427)

* Use safe-header allowlist instead of sensitive-header denylist for redaction
2026-03-07 12:10:05 -05:00
Jeremiah Lowin
58e25ccf47
Preserve kw-only defaults when rebuilding functions for resolved annotations (#3429)
* Preserve kw-only defaults in cloned adapters (🤖 GPT-5.2-Codex)

* Fix ruff format violation in test_types.py
2026-03-07 12:10:01 -05:00
Jeremiah Lowin
ceb8ff18c6
Fix $ref output schema object detection regression (#3420)
* Fix  output schema object detection

🤖 Generated with GPT-5.2-Codex

* Fix ty invalid-type-alias-type error

Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
🤖 Generated with Claude Code

* Fix $ref resolution to handle JSON Pointer escaping and nested paths

---------

Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2026-03-07 12:09:56 -05:00
Jeremiah Lowin
6637bcebe1
Fix server lifespan overlap teardown (#3415)
* Fix server lifespan overlap teardown

🤖 Generated with GPT-5.2-Codex

* Clear lifespan state when non-owner session is last to exit
2026-03-07 12:09:50 -05:00
Jeremiah Lowin
9f8347dbaf
Fix get_* returning None when latest version is disabled (#3439)
* Fix get_* returning None when latest version is disabled (#3421)

When a visibility transform disabled the highest version of a component,
get_tool/get_resource/get_resource_template/get_prompt returned None
instead of falling back to the next-highest enabled version. The list_*
path already worked correctly because deduplication runs after visibility
filtering. The get_* path now falls back to listing all versions and
picking the highest enabled one when the top version is disabled.

* Apply auth checks in version fallback paths

The fallback code in get_tool, get_resource, get_resource_template, and
get_prompt bypassed auth filtering when falling back to older versions
after the highest version was disabled. This could expose auth-protected
older versions to unauthorized users.
2026-03-07 11:55:10 -05:00
Jeremiah Lowin
297880bbfa
Block recursive self-invocation in BulkToolCaller (#3433)
* Block BulkToolCaller self-invocation

🤖 Generated with GPT-5.2-Codex

* Fix ruff format violation in test_bulk_tool_caller.py

🤖 Generated with Claude Code
2026-03-07 11:41:15 -05:00
Jeremiah Lowin
44b8238e40
Guard default progress handler against zero totals (#3432)
🤖 Generated with GPT-5.2-Codex
2026-03-07 11:41:11 -05:00
Jeremiah Lowin
d316f193a8
Fix transform arg collisions with passthrough params (#3431)
🤖 Generated with GPT-5.2-Codex
2026-03-07 11:41:07 -05:00
Jeremiah Lowin
901d8cdd60
Block HS* algorithms when JWTVerifier is configured with JWKS (#3419)
* Block HS* algorithms with JWKS in JWT verifier

🤖 Generated with GPT-5.2-Codex

* Fix ruff format: remove extra blank line in test_supabase.py

🤖 Generated with Claude Code

Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>

---------

Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2026-03-07 11:41:04 -05:00
Jeremiah Lowin
544f072659
Guard OAuth callback result from post-completion overwrites (#3417)
* Guard OAuth callback result overwrite 🤖 Generated with GPT-5.2-Codex

* Fix ruff formatting in test_oauth_callback_race.py

Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 11:41:00 -05:00
Jeremiah Lowin
dcaeef0f3d
Handle missing npx.cmd fallback on Windows 🤖 Generated with GPT-5.2-Codex (#3416) 2026-03-07 11:40:57 -05:00
Jeremiah Lowin
0b97aca02a
Re-hash response cache keys 🤖 Generated with GPT-5.2-Codex (#3414) 2026-03-07 11:40:53 -05:00
Jeremiah Lowin
706b56d555
Harden fastmcp metadata parsing in proxy paths (#3412)
🤖 Generated with GPT-5.2-Codex
2026-03-07 11:40:49 -05:00
Jeremiah Lowin
ea19a2a5f5
Block out-of-skill symlink hashing in skills scan (#3410)
🤖 Generated with GPT-5.2-Codex
2026-03-07 11:40:46 -05:00
Jeremiah Lowin
163c16876f
Avoid stale context leakage when proxying with an already‑connected ProxyClient (#3408)
* Avoid reusing connected ProxyClient sessions

🤖 Generated with Codex

* Fix static analysis: ruff format + ty type narrowing
2026-03-07 11:40:43 -05:00
Jeremiah Lowin
1708e53d9e
Bind AWS Cognito token verification to configured app client (#3406)
* Bind Cognito verifier audience to client ID

🤖 Generated with GPT-5.2-Codex

* Fix ty error: narrow return type of AWSCognitoProvider.get_token_verifier

🤖 Generated with Claude Code
2026-03-07 11:40:39 -05:00
Jeremiah Lowin
9ec4e7ae1b
Validate version metadata to reject non-scalar types (#3437)
* Validate version metadata to reject non-scalar types (#3422)

🤖 Generated with Claude Code

* Reject bool values in version coercion
2026-03-07 11:40:36 -05:00
Jeremiah Lowin
fadb630142
Validate workspace path is a directory in cursor install (#3426) (#3435) 2026-03-07 11:40:33 -05:00
Jeremiah Lowin
9ccaef2b6a
Raise ValueError for invalid boolean query params in resource templates (#3424) (#3434) 2026-03-07 11:40:29 -05:00
Jeremiah Lowin
85c71fa834
Bound client auto-pagination loops to prevent unbounded list fetches (#3411)
* Cap client auto-pagination pages

🤖 Generated with GPT-5.2-Codex

* Raise on pagination limit instead of returning partial data

Add max_pages kwarg (default 250) to list_tools/list_resources/
list_resource_templates/list_prompts so users can control the bound.
2026-03-06 17:43:47 -05:00
Jeremiah Lowin
799c4f1673
Escape client_id in OAuth consent details (#3418)
🤖 Generated with GPT-5.2-Codex
2026-03-06 17:33:12 -05:00
Jeremiah Lowin
56b165537f
Fix initialize McpError fallthrough 🤖 Generated with Codex (#3413) 2026-03-06 17:31:19 -05:00
Jeremiah Lowin
e3b6b0c470
Deprecate PromptToolMiddleware and ResourceToolMiddleware (#3389)
* Deprecate PromptToolMiddleware and ResourceToolMiddleware

* Remove deprecated middleware from docs entirely

* Remove entire Tool Injection section from middleware docs
2026-03-06 17:17:01 -05:00
Jeremiah Lowin
72d04d7c7f Widen CachableMessage.content to match Message content types 2026-03-06 17:12:29 -05:00
Eric Robinson
9319a2c645 Support ImageContent and AudioContent in Message class
Message.content now accepts ImageContent and AudioContent in addition to
TextContent and EmbeddedResource, matching MCP's ContentBlock type. This
fixes ProxyPrompt.render() silently JSON-serializing image/audio content
instead of preserving it.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:12:29 -05:00
Jeremiah Lowin
216f43d0ba
Bind Discord token verifier to client ID (#3405)
🤖 Generated with GPT-5.2-Codex
2026-03-06 17:01:44 -05:00
Jeremiah Lowin
b8c37bfa89
Fix WorkOS token scope verification bypass 🤖 Generated with Codex (#3407) 2026-03-06 17:01:35 -05:00
Jeremiah Lowin
519f9c569c
Fix auto-close MRE script posting comment without closing (#3386)
* Fix auto-close MRE script posting comment without closing issue

* Surface partial failures when comment post fails after close
2026-03-04 16:56:29 -05:00
Jeremiah Lowin
1c64d1c6b1
Forward scopes_supported through RemoteAuthProvider subclasses (#3388) 2026-03-04 16:55:30 -05:00
Sumanshu Nankana
34d848b16a
feat: add --config-path flag to claude-desktop install command (#3380)
* feat: add --config-path flag to claude-desktop install command

* feat: add --config-path flag to claude-desktop install command

* docs: add --config-path option to install-mcp documentation

* fix: show specific error message when provided --config-path does not exist
2026-03-04 15:30:59 -05:00
Jeremiah Lowin
d4a8354ba2
Add settings and environment variables reference (#3384)
* Add settings reference page

* Note .env caveat for nested Docket settings
2026-03-04 15:29:31 -05:00
Jeremiah Lowin
ea214282c1 Add tag to docs 2026-03-03 19:06:11 -05:00
Jeremiah Lowin
fce4182860 Fix uv sync flag in upgrade check notification 2026-03-03 17:08:46 -05:00
Jeremiah Lowin
b061126955 Fix ty 0.0.20 compatibility and improve upgrade check notifications 2026-03-03 17:08:46 -05:00
marvin-context-protocol[bot]
d8e6540f24 chore: Update SDK documentation 2026-03-03 17:03:23 -05:00
Jeremiah Lowin
0142fefe1b Add test: VersionFilter applied before CatalogTransform is respected 2026-03-03 17:03:11 -05:00
Jeremiah Lowin
03673d9fad Deduplicate versioned tools in CatalogTransform.get_tool_catalog() 2026-03-03 17:03:11 -05:00
Jeremiah Lowin
f923b671ce Remove stale catalog cache from CodeMode execute 2026-03-03 16:49:50 -05:00
Jeremiah Lowin
cea6e935e9 Increase uv transport test timeout to 60s for cold-start CI 2026-03-03 16:49:40 -05:00
Jeremiah Lowin
fec8e8d5ab Fix flaky test_multi_client_lifespan and test_multi_client_force_close 2026-03-03 16:43:27 -05:00
Jeremiah Lowin
cb34191128 Fix resource templates with query params on mounted servers
Closes #3366
2026-03-03 16:43:27 -05:00
Jeremiah Lowin
6aff9c94be Remove form-action from default consent CSP, forward consent_csp_policy in all providers
Drop form-action from the default Content Security Policy on the OAuth
consent page. Chrome enforces form-action across the entire redirect
chain, which breaks flows where an HTTPS callback internally redirects
to a custom scheme (e.g. claude://, cursor://). Since the form posts
to itself and all redirects are server-controlled, form-action adds
no security value here.

Also forward the consent_csp_policy parameter through all concrete
OAuth providers (Auth0, Azure, Google, GitHub, Discord, WorkOS, AWS
Cognito, OCI) so users can override the CSP without accessing private
attributes.
2026-03-03 14:55:00 -05:00
Jeremiah Lowin
0cfc9399d7 Update docs banner and fix mobile layout
Replace PyAI Conf banner with Prefect Horizon banner. Add mobile
media query to reduce banner font size and padding so text stays
on one line at narrow viewports.
2026-03-03 14:34:49 -05:00
Jeremiah Lowin
ecc1ede928
Add early-development warning to Prefab docs (#3362) 2026-03-02 21:53:03 -05:00
Jeremiah Lowin
8bc31360e8
Restructure docs navigation: CLI section, Composition, More (#3361)
* WIP: Move mounting docs to servers/composition, remove deprecated import_server content

* WIP: Add CLI section under More, move testing to Features, restructure nav

* WIP: Rename querying to client, remove factory functions from CLI overview

* WIP: Promote CLI to top-level section, move Upgrading to More

* WIP: Rename CLI installing page to install-mcp

* WIP

* Add Google Gemini sampling handler docs, fix version badges
2026-03-02 21:09:48 -05:00
yangbaechu
8a356ad870
Add include_unversioned option to VersionFilter (#3349)
* Add include_unversioned option to VersionFilter

* test: add include_unversioned=False coverage for prompts/templates; align naming/imports
2026-03-02 21:06:50 -05:00
Joris Vandermeersch
fccbd43c1e
Add FASTMCP_TRANSPORT setting for default transport selection (#1796)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Co-authored-by: Joris Vandermeersch <joris.vandermeersch@vrt.be>
2026-03-02 18:08:59 -05:00
Jeremiah Lowin
404b820144
Add ListTools, search limit, and catalog size annotation to CodeMode (#3359)
* Add tests for two-stage pattern, empty full-detail results, empty inputs

* Add ListTools, search limit, catalog size annotation; split tests

Co-authored-by: Claude <noreply@anthropic.com>

* Remove BM25 internal cap so Search.limit is the sole truncation point

* Pass default_limit to BM25 instead of arbitrary high cap

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-02 18:00:21 -05:00
Bill Easton
49534ce91d
Add Google GenAI Sampling Handler (#2977)
* Add Google GenAI sampling handler

Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>

* chore: Update SDK documentation

* Filter non-Gemini model hints in _get_model

Match the Anthropic/OpenAI handler pattern of only selecting
provider-compatible models from hints.

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-03-02 18:00:03 -05:00
Ankesh Kumar Thakur
af557388c7
feat(contrib): auto-sync MCPMixin decorators with from_function signatures (#3323)
* feat: auto-sync MCPMixin decorators with from_function signatures

Replace hard-coded parameter lists in mcp_tool/mcp_resource/mcp_prompt
with **kwargs validated at decoration time against inspect.signature of
the underlying from_function. Registration methods now splat **kwargs
through directly, so any new from_function parameter is supported
automatically without touching the contrib module.

Also fixes enabled=False being silently ignored during registration.

🤖 Generated with Claude Code

* fix: make enabled keyword-only in mcp_tool and mcp_prompt

Prevents silent positional arg remapping — callers who previously
passed description/title as the second positional arg would have
silently bound to enabled instead.

🤖 Generated with Claude Code
2026-03-02 17:30:30 -05:00
marvin-context-protocol[bot]
902b63f676
chore: Update SDK documentation (#3313)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-03-02 17:27:38 -05:00
Jeremy Simon
f2dd2e5124
Fix session persistence across tool calls in multi-server MCPConfigTransport (#3330)
* connect ProxyClient via AsyncExitStack

* test session persistence for multiple tool calls

* switch to StatefulProxyClient

* changed to StatefulProxyClient

* stash weakref instead of context

* clear transport list before building new session
2026-03-02 17:19:43 -05:00
Jeremiah Lowin
2ab8beb195
Remove system role references from docs (#3356)
The MCP spec only supports "user" and "assistant" roles for prompt
messages. Fixes #3160.

Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2026-03-02 16:36:24 -05:00
Jeremiah Lowin
59da3e4ed4
Decompose CodeMode into composable discovery tools (#3354)
* Decompose CodeMode into composable discovery tools

CodeMode now owns only the execute sandbox. Discovery tools (search,
get_schema, etc.) are composable via the discovery_tools parameter.
Each is a Callable[[GetToolCatalog], Tool] factory.

Ships SearchTool (lightweight name+description results) and SchemaTool
(brief markdown or full JSON schemas by tool name) as built-in defaults.

* Rename to Search/GetSchemas/Tags, add tag filtering, fix bugs

- Rename SearchTool→Search, SchemaTool→GetSchemas, Categories→Tags
- Add tags parameter to Search for LLM-side tag filtering
- Add Tags discovery tool for browsing tools by tag
- Fix shared singleton default factories (now per-instance)
- Fix get_schema full mode returning invalid JSON on partial matches
- Fix "untagged" filter inconsistency between Tags and Search
- Split serialization tests to comply with loq line limit
- Rewrite docs for conceptual clarity

* Add three-tier detail levels, remove default_arguments, rename Tags→GetTags, rewrite docs

* Clean up __all__ exports, return valid JSON for empty full-detail results
2026-03-02 16:35:55 -05:00
Chris Guidry
547daf7a36
Replace vendored DI with uncalled-for (#3301)
* Replace vendored DI with uncalled-for

FastMCP vendored a minimal DI engine extracted from Docket (~164 lines)
with try/except fallback patterns everywhere. The `uncalled-for` package
is a clean, typed extraction of this same system, and since Docket will
also depend on it (chrisguidry/docket#353), `uncalled_for.Dependency`
becomes the single canonical base class.

This deletes the `_vendor/docket_di/` directory, replaces all the
try/except import patterns with direct `uncalled_for` imports, and
updates the `Dependency.execution` → `current_execution` ContextVar
references to match the Docket branch. The `Progress` class now
delegates to an internal impl and returns `self` from `__aenter__`
(matching Docket's pattern) so that ty's generic resolution works
without `type: ignore` suppressions.

Temporarily points pydocket at the `use-uncalled-for` branch so both
sides can be validated together in CI.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Re-export Dependency from fastmcp.dependencies

Internal code like azure.py should import from the fastmcp namespace
rather than reaching into uncalled_for directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Import Dependency from fastmcp namespace in tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Add generic type parameters to Dependency subclasses

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Mention uncalled-for in DI docs

The DI engine now comes from uncalled-for, so the docs should credit
it alongside Docket. Also updates the Docket docs link to docket.lol.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Point docket dependency at main

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Bump uncalled-for pin to >=0.2.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix uncalled-for imports for 0.2.0 API changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Support Shared() dependencies without docket

Enters a SharedContext at server lifetime so that Shared() dependencies
from uncalled-for resolve once and are cached across tool/resource/prompt
calls. When running with docket, the Worker already handles this; this
covers the non-docket path and direct call_tool() usage.

Also re-exports Shared from fastmcp.dependencies.

Closes #3251

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Bump docket lockfile to latest main

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove duplicate test classes from rebase conflict resolution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Point docket dependency at pydocket>=0.18.0 release

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Pair SharedContext __aenter__ with __aexit__ in Context lifecycle

The old `_ensure_shared_context` on the server called `__aenter__()` on a
lazy `SharedContext` but never `__aexit__()`, leaking the exit stack and
its resources. Moved the SharedContext management into Context's own
enter/exit so it's properly paired: when docket is available the lifespan
handles it, otherwise Context creates and cleans up a per-request one.

Updated Shared() tests to use Client (which runs the lifespan) rather
than calling server methods directly, since cross-request sharing
requires a lifespan.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Hoist SharedContext import to module level

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 16:35:15 -05:00
Andrew Israel
5fb72c7200 Adds PropelAuth as an AuthProvider
Adds the PropelAuthProvider which delegates to the IntrospectionTokenVerifier
and optionally does an additional resource check.
Adds an example server and client which makes an authenticated request
and gets information from the token.
Updates the documentation (but only for v3 as this isn't in v2).
2026-03-02 16:18:22 -05:00
dgenio
8ad4eb8321 Add proxy_set_header Connection '' to nginx configs
Generated with GitHub Copilot
2026-03-02 16:09:44 -05:00
dgenio
f3b1055b2a Add SSE Polling cross-reference to nginx timeout guidance
Generated with GitHub Copilot
2026-03-02 16:09:44 -05:00
dgenio
62c5520180 Add reverse proxy (nginx) section to HTTP deployment docs 2026-03-02 16:09:44 -05:00
Jeremiah Lowin
7eba044dd8 Bump tool sleep to actually validate timeout precedence 2026-03-02 13:50:29 -05:00
Jeremiah Lowin
f9a4eb1647 Fix flaky SSE timeout test
The client timeout of 0.1s was too tight — the SSE handshake alone
consumed it under CI load before the tool call ever started. Raised
to 0.5s which still validates the precedence logic.
2026-03-02 13:50:29 -05:00
Georgios Roros
ee6f469aa5 Fix flaky timing test on Windows CI
Increase performance test threshold from 100ms to 1.0s. Windows CI runners
are slower than Linux, and the 100ms threshold was too tight. The test's
intent is to catch obvious regressions (e.g., accidentally re-introducing
code generation), not to precisely benchmark.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-02 13:28:28 -05:00
Jeremiah Lowin
33a69d7d0a
Add MultiAuth for composing multiple token verification sources (#3335)
* Add MultiAuth for composing multiple token verification sources

🤖 Generated with Claude Code

https://claude.ai/code/session_01WwKYDCqjM2FqYwY5ZNVvjb

* Fix ruff lint/format in MultiAuth tests

🤖 Generated with Claude Code

https://claude.ai/code/session_01WwKYDCqjM2FqYwY5ZNVvjb

* Fix MultiAuth well-known route delegation and empty scopes handling

🤖 Generated with Claude Code

https://claude.ai/code/session_01WwKYDCqjM2FqYwY5ZNVvjb

* Harden MultiAuth: exception resilience, mcp_path propagation, test coverage

- verify_token now catches exceptions from individual sources and
  continues to the next, so one broken verifier can't take down the
  whole chain
- set_mcp_path propagates to verifiers, not just the server
- Fix jwks_url→jwks_uri typo in class docstring
- Add tests for raising verifiers, valid-token HTTP acceptance,
  and set_mcp_path propagation

* Clean up MultiAuth: precompute sources, deduplicate test helpers

* Fix version badges to 3.1.0

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-03-02 12:09:22 -05:00
Magnus
1e72f2457b
Add search_result_serializer hook and serialize_tools_for_output_markdown (#3337) 2026-03-01 14:20:24 -05:00
dgenio
9d635bc026 Address maintainer review: reload support, type annotation, test imports 2026-03-01 13:29:54 -05:00
dgenio
52144ce20b Apply ruff formatting to test file 2026-03-01 13:29:54 -05:00
dgenio
2b28cd2a88 Add integration tests for module mode in run and inspector 2026-03-01 13:29:54 -05:00
dgenio
d35ce8dd2e Fix module mode docstring example order 2026-03-01 13:29:54 -05:00
dgenio
173b93a4c3 Use bare python for module mode when uv wraps the command 2026-03-01 13:29:54 -05:00
dgenio
f14f7783df Skip load_server() in inspector when --module is active 2026-03-01 13:29:54 -05:00
dgenio
7a582ff5a0 Add -m/--module flag to fastmcp run and dev inspector 2026-03-01 13:29:54 -05:00
Yang Geonhee
d7674b3c0c Handle AuthorizationError as exclusion in AuthMiddleware list hooks 2026-03-01 13:23:02 -05:00
Jeremiah Lowin
610551c7b6
Split large test files to comply with loq line limit (#3328) 2026-02-28 11:21:11 -05:00
Claude
7690e995b8 Add tests for default sampling capability (issue #3329)
🤖 Generated with Claude Code

https://claude.ai/code/session_01BsxYNsUhJPx14QiJ4FHqx6
2026-02-28 11:06:21 -05:00
Claude
7a92b82097 Don't advertise sampling.tools capability by default
🤖 Generated with Claude Code

https://claude.ai/code/session_01BsxYNsUhJPx14QiJ4FHqx6
2026-02-28 11:06:21 -05:00
Jeremiah Lowin
72129c3d03 Fix symlink note in CLAUDE.md 2026-02-27 21:38:07 -05:00
Jeremiah Lowin
0afd990ee6 Improve code-mode docs formatting; add docs formatting guideline 2026-02-27 21:38:07 -05:00
Jeremiah Lowin
75d6e2fc10 Document resource limits for MontySandboxProvider 2026-02-27 21:38:07 -05:00
Jeremiah Lowin
18d5902367 Document transforms kwarg in server constructor reference 2026-02-27 21:28:34 -05:00
Jeremiah Lowin
14e64b3b22 Use transforms= kwarg in docs examples 2026-02-27 21:28:34 -05:00
Jeremiah Lowin
09a99e1ecc Accept transforms as FastMCP init kwarg 2026-02-27 21:28:34 -05:00
Jeremiah Lowin
b01e66dc58 Add resource limits to MontySandboxProvider 2026-02-27 21:24:56 -05:00
Jeremiah Lowin
3ff1472ea9
Add Prefab Apps integration for MCP tool UIs (#3316)
* Add prefab auto-wiring for MCP Apps (#3119)

Tools that return prefab types (UIResponse, Component) automatically get
wired to the shared prefab renderer resource. Works via app=True,
return type inference, or both.

* Prefab compatibility updates

* Use published prefab-ui >=0.6.0, remove local source override

* Migrate UIResponse to PrefabApp for Prefab UI integration

PrefabApp is a pure data object with to_json(), html(), and csp()
methods. Tools can return PrefabApp, bare Components, or
ToolResult with structured_content for custom LLM fallback text.

* Add Prefab UI apps documentation

* Add mini apps and full apps documentation pages

Mini apps covers the common single-screen patterns: charts (bar, line,
area, pie), data tables with sorting/search/pagination, forms (manual
and Pydantic-generated), status displays, conditional content, and
layout composition with tabs and accordions.

Full apps covers multi-page applications using Pages/Page components,
shared state across pages, and using ToolCall with result_key for
server-driven state updates.

* Reframe apps docs around motivation, add generative UIs page

The docs now lead with the problem — MCP tools stuff data into the LLM
context window, and building HTML/JS/CSS frontends is a non-starter for
Python developers — before introducing Prefab as the solution. Mini apps
are framed as the primary use case: focused, single-purpose UIs that
present data visually and collect structured input.

New generative UIs page covers the concept of LLMs producing component
JSON directly, enabling adaptive dashboards, tailored forms, and
exploratory workflows.

* Tag Prefab docs pages as SOON instead of NEW

* Rename Low-Level API to Custom HTML Apps

The page is about using the MCP Apps extension directly, not a FastMCP
or Prefab internal API. Reframed to make clear this is the open MCP
protocol with FastMCP providing convenience wrappers.

* Tighten apps docs and widen content area

Strip editorial motivation from all app doc pages — let code examples
do the talking. Add content-area max-width override (44rem) to style.css.

* Restructure apps docs, fix code issues

Rename Prefab UI → Prefab Apps, mini-apps → patterns, remove
generative-uis and full-apps pages. Rewrite prefab page to lead with
what users do (declare a UI, return it) before explaining internals.
Patterns page now has fully self-contained copy-pasteable examples with
explicit imports and links to prefab docs. Forms show the two-tool
pattern (form + handler). Add patterns_server.py example.

Code fixes: move get_args to module-level import, remove dead
AuthCheckCallable type alias, fix ToolCall→CallTool in all docs.

* Remove unused ToolResult import from chart_server

* Handle composite Prefab types in type inference and schema suppression

_has_prefab_return_type and the output schema suppression logic only
checked bare classes, missing unions (Column | None) and Annotated
wrappers (Annotated[PrefabApp | None, ...]). Recurse through Union,
types.UnionType, and Annotated to detect Prefab types in composite
annotations.
2026-02-27 14:37:57 -05:00
Jeremiah Lowin
2d1fe3ec0f
Narrate search example clients (#3321)
* Narrate search example clients for screenshot-readability

* Show actual call_tool invocation in result panels
2026-02-27 12:17:31 -05:00
Adam Azzam
b9153404f4
Add experimental CodeMode transform (#3297)
* code mode

* update uv.lock for monty optional dep

🤖 Generated with Claude Code

* retry CI

* Address PR review comments on CodeMode transform

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fix ty unresolved-attribute error on search_helper

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* more idiomacy

* harden

* fix docs

* harden

* fix red CI

* Refactor CodeMode to use CatalogTransform base class

Removes the duplicate ContextVar bypass pattern in favor of the shared
CatalogTransform machinery. Also fixes a pre-existing bug where
`from __future__ import annotations` caused NameError for Annotated
in nested function scopes at runtime.

* Remove redundant _get_visible_tools wrapper in CodeMode

* Rewrite CodeMode docs with proper motivation and structure

* Fix type narrowing in collision test

* Stop unwrapping tool results in CodeMode's call_tool

call_tool() inside execute blocks now returns structured content as-is,
preserving the {"result": value} wrapping. This means the output schema
shown in search results accurately describes what call_tool() returns,
so LLMs can trust the schema when writing code.

Also adds examples/code_mode/ with a server and narrated client demo.

* Simplify call_tool return type: dict | str

* Fix example client to unwrap structured results

* Let server resolve tool versions instead of pinning first match

* Rewrite CodeMode docs to match current behavior

* Rename optional extra from monty to code-mode

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-02-27 12:14:03 -05:00
Jeremiah Lowin
3438b77e88 Fix stale docs: update tag filtering API and add output_schema ParamField 2026-02-27 12:10:22 -05:00
Wang Yiyang
9344224452 Update docs/servers/server.mdx(Fix "FastMCP Constructor Parameters")
Fix description of "FastMCP Constructor Parameters": Remove parameters `on_duplicate_tools`, `on_duplicate_resources` and `on_duplicate_prompts`, which are no longer accepted by FastMCP(). Add the new parameter `on_duplicate` and its description.
2026-02-27 11:52:00 -05:00
Jeremiah Lowin
c96c0400f3
feat: Search transforms for tool discovery (#3154)
* feat: Add search transforms for tool discovery

RegexSearchTransform and BM25SearchTransform collapse large tool
catalogs into a search interface so LLMs discover tools on demand
instead of receiving the full listing.

* chore: Update SDK documentation

* fix: call_tool recursion guard, atomic BM25 rebuild, hash includes descriptions

* Extract CatalogTransform base class for catalog-aware transforms

Transforms that replace list_tools() with synthetic components (like
search) need to read the real catalog at call time without triggering
their own replacement logic. CatalogTransform handles the re-entrant
bypass via per-instance ContextVar, exposing transform_tools() as the
subclass hook and get_tool_catalog() for catalog access.

* Add search transform examples for regex and BM25

* Add README for search transform examples

* Polish search example clients with rich output

* Remove hardcoded tool counts from search example subtitles

* Clarify that review bot feedback should be evaluated on its merits

* Expand search transform docs with proper hierarchy

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-26 22:42:38 -05:00
Jeremiah Lowin
c349bd9e25
Remove stale add_resource() key parameter from docs (#3309)
The `key` parameter was removed from `add_resource()` in the 2.x era and no longer exists in the implementation. Removes all references and the "Custom Resource Keys" section from both the current and v2 docs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Nc1qEJ1rKaRRxB5h6Qu5V3

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-26 16:10:02 -05:00
manojPal23234
507e6b80ab
OpenAPI: rewrite $ref under propertyNames and patternProperties in _replace_ref_with_defs; add regression test for dict[StrEnum, Model] (#3306)
* Normalize OpenAPI $ref everywhere (incl. propertyNames); migrate components→$defs; add regression test

* Fix: normalize $ref in propertyNames and additionalProperties; add regression test

* Deterministic migration: components.schemas override $defs on collision; preserve direct $defs refs via alias; add collision test

* Fix: rewrite $ref in propertyNames and patternProperties in _replace_ref_with_defs

* Fix syntax error, formatting, and stray files

* Skip boolean subschemas in patternProperties

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-02-26 16:08:54 -05:00
Jeremiah Lowin
2d3d0d5eab
Fix ty 0.0.19 type errors (#3310)
* Fix ty 0.0.19 type errors

🤖 Generated with Claude Code

* Fix ruff formatting in sampling/run.py

🤖 Generated with Claude Code

https://claude.ai/code/session_01GWzbyF1vHvVeS4yJ5bhScf

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-26 16:08:40 -05:00
marvin-context-protocol[bot]
80efbd3d57
chore: Update SDK documentation (#3273)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-26 11:19:53 -05:00
Jeremiah Lowin
43ac5395b9
Add SessionStart hook to install gh CLI in cloud sessions (#3308)
Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2026-02-26 11:12:23 -05:00
Jeremiah Lowin
8a4c9b33e1
Add in-memory caching for token introspection results (#3298) 2026-02-26 10:43:40 -05:00
Claude
892e1731f7 Allow http_client with static public_key in JWTVerifier
🤖 Generated with Claude Code

https://claude.ai/code/session_012QKWmKd21vypDmxWbwuE4e
2026-02-25 16:41:24 -05:00
Claude
730175910c Raise error when http_client and ssrf_safe=True are both provided
🤖 Generated with Claude Code

https://claude.ai/code/session_012QKWmKd21vypDmxWbwuE4e
2026-02-25 16:41:24 -05:00
Claude
1704ffe88f Add http_client parameter to token verifiers for connection pooling
🤖 Generated with Claude Code

https://claude.ai/code/session_012QKWmKd21vypDmxWbwuE4e
2026-02-25 16:41:24 -05:00
Jeremiah Lowin
d773079aba Lazily expose server.dependencies instead of dropping it 2026-02-25 16:27:51 -05:00
Jeremiah Lowin
d9541c9c21 Lazy-load heavy imports to reduce import time
Defer auth providers (JWTVerifier, OAuthProxy, OIDCProxy) and Client
to avoid eagerly importing authlib, cryptography, key_value.aio, and
beartype on every `from fastmcp import FastMCP`.
2026-02-25 16:27:51 -05:00
Jeremiah Lowin
270783e614
reverse CLAUDE.md/AGENTS.md symlink direction (#3294) 2026-02-25 11:18:59 -05:00
Jeremiah Lowin
59bb0a3ce7
Fix link to loq repository in AGENTS.md (#3289) 2026-02-24 19:45:56 -05:00
Aymen El Amri
549f48bc7a
fix: skip max_completion_tokens when maxTokens is None (#3284) 2026-02-24 14:04:03 -05:00
Jeremiah Lowin
e87ede075c
fix: stop suppressing server stderr in fastmcp call (#3283)
Server subprocess stderr was being sent to /dev/null, which silently
discarded print(..., file=sys.stderr) and logging output from tools.
2026-02-23 15:07:31 -05:00
Cedric
5815e1eff8
fix: Replace hardcoded TTL with DEFAULT_TTL_MS - issue #3279 (#3280) 2026-02-23 12:05:22 -05:00
Jeremiah Lowin
37126d6d3f
fix: remove position override from docs banner CSS (#3282) 2026-02-23 11:26:48 -05:00
Jeremiah Lowin
e19f2396b3
Add v3.0.2 release notes (#3276) 2026-02-22 11:41:37 -05:00
Manrique Vargas
c71840631e
docs: add context-aware tool factory example (#3264)
Fixes PrefectHQ/fastmcp#1841

Signed-off-by: machov <mv1742@nyu.edu>
2026-02-22 11:24:22 -05:00
Guillaume FORTAINE
40d3190317
fix: propagate origin_request_id to background task workers (#3175)
* Fix background Context request correlation

* Make OptionalCurrentContext type-safe

Refactor OptionalCurrentContext to wrap CurrentContext instead of overriding __aenter__ with a wider return type. Adds a background-task origin_request_id round-trip test and applies ruff formatting.
2026-02-22 11:16:30 -05:00
Jeremiah Lowin
d85cfb84e1
Revert to long-lived PR approach for auto-generated docs/schema (#3272)
🤖 Generated with Claude Code

https://claude.ai/code/session_01LUn4EnV6nZS5UgFz9wtyB3

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-22 11:13:06 -05:00
Bill Easton
83d6254757
Allow Marvin to open PRs on comment (#3267)
* Update marvin-comment-on-issue.yml

* remove conflicting instructions
2026-02-21 15:07:27 -06:00
不做了睡大觉
0d4580fef3
fix: prevent MCP transport auth header from leaking to downstream OpenAPI APIs (#3260) (#3262)
* fix: prevent MCP transport auth header from leaking to downstream OpenAPI APIs (#3260)

Two issues in OpenAPITool.run():

1. get_http_headers() does not exclude 'authorization', so the MCP
   client's auth token is included in forwarded headers.

2. mcp_headers.update() overwrites existing request headers, including
   the Authorization header that was already set from the httpx client's
   configured API key.

Fix:
- Add 'authorization' to exclude_headers in get_http_headers() to
  prevent MCP transport credentials from being forwarded by default.
- Change mcp_headers forwarding to use the same non-overwriting pattern
  as client headers (only set if key not already present), making the
  behavior consistent and preventing accidental overwrites.

Fixes #3260

* Add include parameter to get_http_headers(); update proxy transports

The authorization exclusion is correct for the default case (OpenAPI
tools should not forward MCP transport credentials), but proxy
transports need auth headers forwarded to upstream MCP servers.

The new `include` parameter lets callers opt specific headers back in
despite the default exclusion set. Proxy transports now explicitly
request authorization forwarding.

* Include authorization header in CurrentHeaders dependency

CurrentHeaders is user-facing — tools use it to inspect the caller's
auth token for custom logic. Reading a header in your own code is safe;
the exclusion is meant to prevent blindly forwarding it to third-party
APIs.

---------

Co-authored-by: User <user@example.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-02-21 09:23:27 -05:00
Jeremiah Lowin
3ec7cfa0d5
Add v3.0.1 release notes (#3259) 2026-02-20 20:58:59 -05:00
Jeremiah Lowin
bb3e159bc6
Fix decorator overload return types for function mode (#3258)
* Fix TypeVar placement to satisfy ruff E402

* chore: Update SDK documentation

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-20 20:30:39 -05:00
Jeremiah Lowin
43e9cdd275
Fix ty compatibility with upgraded deps (#3257)
* Fix ty compatibility with upgraded deps

* chore: Update SDK documentation

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-20 19:51:44 -05:00
Jeremiah Lowin
2218a6f52a
Use max_completion_tokens instead of deprecated max_tokens in OpenAI handler (#3254)
🤖 Generated with Claude Code

Co-authored-by: Marvin Context Protocol <41898282+Marvin Context Protocol@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2026-02-20 19:31:38 -05:00
Jeremiah Lowin
1d7e92ecab
Fix ty ignore syntax in OpenAPI provider (#3253)
* Fix ty ignore syntax in OpenAPI provider

* Fix flaky rate limiting and ping timing tests

* Assert rate limit error message in flaky test fix

* Catch only ToolError in rate limiting test
2026-02-20 18:06:43 -05:00
Jeremiah Lowin
f84b2dae04
Add verify_id_token option to OIDCProxy (#3248)
* Add verify_id_token option to OIDCProxy

Closes #3240

* chore: Update SDK documentation

* Preserve raw_token_data fields across token refresh

* chore: Update SDK documentation

* Use client_id as verifier audience in verify_id_token mode

* chore: Update SDK documentation

* Return upstream access_token in AccessToken when verifying id_token

* chore: Update SDK documentation

* Skip verifier scope checks in verify_id_token mode

* chore: Update SDK documentation

* Recompute derived scope state after restoring required_scopes in verify_id_token mode

* chore: Update SDK documentation

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-20 14:20:47 -05:00
Jeremiah Lowin
7e0eb2ff9d
Update Marvin workflow models to claude-sonnet-4-6 (#3250) 2026-02-20 11:09:16 -05:00
Jeremiah Lowin
fc6290479f
Fix Marvin app token to use PrefectHQ org installation (#3249) 2026-02-20 11:08:38 -05:00
Jeremiah Lowin
ea1328660e
Fix NameError with future annotations and Context/Depends parameters (#3243)
* Fix NameError with future annotations and Context/Depends parameters

Closes #3238, closes #905

* chore: Update SDK documentation

* Drop unnecessary pre-resolution of annotations

Pydantic (even 2.11.7) uses __module__ not __globals__ to resolve
annotations, so setting __module__ alone is sufficient.

* chore: Update SDK documentation

* Restore annotation pre-resolution for Pydantic compat

The wrapper's __globals__ is read-only and points to dependencies.py,
so some Pydantic versions use it instead of __module__ when resolving
string annotations. Pre-resolving via get_type_hints on the original
function ensures annotations are type objects before Pydantic sees them.

* chore: Update SDK documentation

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-20 09:22:10 -05:00
Jeremiah Lowin
1caf40eee8
Fix circular reference crash in OpenAPI schemas (#3245)
* Fix circular reference crash in OpenAPI schemas (#3242, #1206, #1016)

* chore: Update SDK documentation

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-20 09:21:54 -05:00
Jeremiah Lowin
5e2d698356
Fix dedupe bot labeling pipeline (#3244)
* Fix dedupe bot adding wrong label by making labeling deterministic

* Scope label step to comments from current workflow run

* Fetch newest comments first to avoid pagination miss
2026-02-20 09:12:44 -05:00
Jeremiah Lowin
aa7946d353
Preserve skill metadata through provider wrapping (#3237)
* Preserve computed _meta through provider wrapping; add skill identity to _meta

* chore: Update SDK documentation

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-19 12:24:39 -05:00
Jeremiah Lowin
390a11d7d2
Document mounted server state store isolation in upgrade guide (#3236)
* Document mounted server state store isolation in upgrade guide

* Add missing FastMCP import to upgrade guide example
2026-02-19 12:05:50 -05:00
Jeremiah Lowin
35bbf4800f
Animated (#3231) 2026-02-19 11:56:51 -05:00
Jeremiah Lowin
b894a0b747
Accept callables in Tool.from_tool() (#3235)
* Accept callables in Tool.from_tool()

* chore: Update SDK documentation

* Add end-to-end client test for decorated function transform

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-19 11:31:13 -05:00
Jeremiah Lowin
3f2090f10a
Fix non-serializable state lost between middleware and tools (#3234)
* Fix non-serializable state lost between middleware and tools

Inherit _request_state dict from parent Context in __aenter__ so
middleware and tool contexts share the same in-memory state.

Closes #3228

* chore: Update SDK documentation

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-19 10:57:59 -05:00
Jeremiah Lowin
08387157bb
Fix MDX parse error: escape curly braces in upgrade guide prompts (#3227) 2026-02-18 21:55:35 -05:00
Jeremiah Lowin
06155d3d67
Fix v2 upgrade guide: remove incorrect v1 import advice (#3226)
* Fix v2 upgrade guide: remove incorrect v1 import migration advice

* Drop unnecessary reassurance about unchanged import

* Improve v2 upgrade guide opening: mention deprecated kwargs and async shifts
2026-02-18 21:29:45 -05:00
Jeremiah Lowin
9975e1cc6e
Document dict-to-Message prompt migration in v2 upgrade guide (#3225) 2026-02-18 19:07:30 -05:00
Jeremiah Lowin
64e9153426
Sync README with welcome.mdx, fix install count (#3224)
* Sync README with welcome.mdx, fix install count in release notes

* Tighten README, sync with welcome.mdx, fix install count
2026-02-18 18:38:20 -05:00
Jeremiah Lowin
f085428b32
Fix v3.0.0 changelog compare link: v2.14.5, not v2.14.1 (#3223) 2026-02-18 18:26:51 -05:00
Jeremiah Lowin
92f4c503cd
v3.0.0 release notes, changelog, and README updates (#3222)
* Add v3.0.0 changelog and updates entries

* Update README for v3.0 GA: fix image paths, add upgrade guides
2026-02-18 16:21:45 -05:00
Jeremiah Lowin
e717e1623c
Update thumbnail background (#3219)
* Update thumbnail background

* New thumbnail

* JPEG encoded

* Update banner
2026-02-18 15:53:46 -05:00
Chris Guidry
8a13032419
Use standard traceparent/tracestate keys per OTel MCP semconv (#3221)
* Use standard traceparent/tracestate keys per OTel MCP semconv

The OTel semantic conventions for MCP (https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/)
put `traceparent` and `tracestate` directly in `params._meta` without a prefix.
FastMCP was using `fastmcp.traceparent` / `fastmcp.tracestate`, which meant
non-FastMCP clients sending the standard keys couldn't propagate traces.

Switches injection to the bare keys and adds fallback extraction for the old
prefixed keys so older FastMCP clients still work.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: Update SDK documentation

* Drop legacy fastmcp.-prefixed trace key fallback

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore: Update SDK documentation

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-18 15:51:24 -05:00
Jeremiah Lowin
a2efd686a9
Normalize repo references to PrefectHQ/fastmcp casing (#3218) 2026-02-18 14:51:18 -05:00
Jeremiah Lowin
78010ef06e
Fix invalid uv add fastmcp@latest command (#3217) 2026-02-18 14:46:19 -05:00
Jeremiah Lowin
1eb57ba262
Add upgrade guides for users coming from the MCP SDK (#3215)
* Add upgrade guides for users coming from the MCP SDK

* Fix incorrect Image import path in LLM migration prompt

* Align LLM prompts with prose across all three upgrade guides

* Move upgrade guides under getting-started/upgrading, add install section and --upgrade flag
2026-02-18 14:30:07 -05:00
Jeremiah Lowin
958e657521
Update docs for 3.0 GA release (#3216)
* Update docs for 3.0 GA release

* Add upgrade command to v2 migration section

* Revert "Add upgrade command to v2 migration section"

This reverts commit ee03d0d847.
2026-02-18 14:29:28 -05:00
Jeremiah Lowin
657298112b
Fix MDX parsing error and update card images (#3213)
* Fix MDX parsing error and add broken link CI check

Escape curly braces in docstring example that broke MDX parsing,
update card images, and add docs broken link check to CI.

* Revert CI broken link check — Mintlify runs this already

* Use code fence instead of inline backticks for MDX escaping

* chore: Update SDK documentation

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-18 13:04:45 -05:00
Jeremiah Lowin
7aba0df323
Cache OBO credentials on AzureProvider for token reuse (#3212)
* Cache OBO credentials on AzureProvider for token reuse

* chore: Update SDK documentation

* Close evicted OBO credentials properly

* chore: Update SDK documentation

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-18 11:43:36 -05:00
Jeremiah Lowin
9b248a15b3
Update repository references for transfer to prefecthq (#3207)
* Update repository references from jlowin/fastmcp to prefecthq/fastmcp

* Retrigger CI after repo transfer

* chore: Update SDK documentation

* Only run deep triage on bug issues for jlowin

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-18 10:33:56 -05:00
Jeremiah Lowin
3a4d41346c
Overhaul v3.0 upgrade guide (#3205)
* Overhaul v3.0 upgrade guide

Rewrites the upgrade guide with educational context for each breaking
change, adds an LLM migration prompt users can copy into any AI assistant,
and covers previously missing items (removed constructor kwargs, module
path deprecations, import_server deprecation).

* Address CodeRabbit review feedback on upgrade guide

Split message_path from other transport kwargs (env-var only, not a
run() kwarg), move decorator change to breaking changes in the LLM
prompt since accessing component attributes will crash, and add
DiskStore/OAuth storage change to the prompt's numbered list.

* Move decorator change under Breaking Changes in prose

* Add before/after pattern to auth provider section

* Add Warning callout, WSTransport and OpenAPI migration examples

* Add missing imports to FastMCPOpenAPI migration example
2026-02-17 19:51:18 -05:00
Jeremiah Lowin
27be9fe229
Exclude auto-generated python-sdk docs from CodeRabbit reviews (#3206)
* Exclude auto-generated python-sdk docs from CodeRabbit reviews

* Clarify that python-sdk doc changes in PR diffs are expected
2026-02-17 18:28:20 -05:00
Jeremiah Lowin
a8b100eb8c
Add JWT audience validation and RFC 8707 warnings to auth providers (#3204)
* Add JWT audience validation and RFC 8707 warnings to auth providers

* chore: Update SDK documentation

* Update AuthKit example README env var name

* Move RFC 8707 warnings inside default verifier guard

* chore: Update SDK documentation

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-17 18:16:05 -05:00
Jeremiah Lowin
016b9f90e0
Fix confused deputy attack via consent binding cookie (#3201)
* Add consent binding cookie to prevent confused deputy attacks (GHSA-rww4-4w9c-7733)

The OAuthProxy's consent page verified user intent but didn't bind the
consenting browser to the IdP callback. An attacker could intercept the
upstream authorization URL after consent and send it to a victim, whose
browser would complete the flow without having the consent cookie.

This adds a signed consent binding cookie set during consent approval
(both manual and auto-approve paths) and verified in the IdP callback
handler. A different browser won't have this cookie and gets a 403.

* Use startswith for URL assertion in consent binding test

* Store consent bindings as per-transaction map to support parallel flows

* Only accept __Host- consent binding cookie on HTTPS

* chore: Update SDK documentation

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-17 10:24:07 -05:00
Bill Easton
2dcbb944a8
Minimize resolved review threads in PRs (#3200)
* Minimize resolved review threads in PRs

* cancel in progress runs when there are duplicates

* add /tidy comment support
2026-02-17 08:22:31 -05:00
Jeremiah Lowin
5dd8cde2f9
Drop diskcache dependency (CVE-2025-69872) (#3185)
* Drop diskcache dependency (CVE-2025-69872)

Switch default OAuth proxy storage from DiskStore (backed by diskcache,
which uses pickle serialization) to FileTreeStore (file-per-key JSON).
This removes diskcache from the dependency tree entirely, resolving
CVE-2025-69872 for pip-audit and similar scanners.

* chore: Update SDK documentation

* Add comments explaining FileTreeStore warning suppression

* chore: Update SDK documentation

* Isolate storage by encryption key, gracefully handle decryption failures

* chore: Update SDK documentation

* Bump py-key-value-aio lower bound to 0.4.2 for FileTreeStore security hardening

* chore: Update SDK documentation

* Document storage backend change and update DiskStore references

* Bump py-key-value-aio lower bound to 0.4.3

* Bump py-key-value-aio to 0.4.4, remove warning suppression

* chore: Update SDK documentation

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-16 16:43:15 -05:00
Jeremiah Lowin
5dc2432950
Merge pull request #3198 from jlowin/fix/ty-errors
Fix ty 0.0.17 diagnostics
2026-02-16 16:24:42 -05:00
Jeremiah Lowin
564b4c0d5c
Fix ty 0.0.17 diagnostics and bump lockfile 2026-02-16 16:13:38 -05:00
Jeremiah Lowin
85a833a74b
Reorganize docs navigation around Server/Client/Apps pillars (#3197)
* Reorganize docs navigation and add Apps documentation

Collapse Providers, Transforms, and Deployment under Servers. Add Apps
section with overview and low-level API pages. Add card images to welcome
page and README. Add NEW tags to recent features.

* Fix missing imports in Apps low-level API code examples
2026-02-16 15:33:16 -05:00
Jeremiah Lowin
b4eb1adbeb
Merge pull request #3195 from jlowin/fix/stateless-http-disable-get-sse
Return 405 for GET SSE on stateless HTTP servers
2026-02-16 09:32:21 -05:00
Jeremiah Lowin
dab2084c74
Return 405 for GET requests on stateless HTTP servers 2026-02-16 09:08:32 -05:00
Jeremiah Lowin
453dcbe808
Merge pull request #3194 from jlowin/docs/caching-identity-note
Document cache key identity behavior
2026-02-16 08:55:29 -05:00
Jeremiah Lowin
62804e2f06
Add cache key identity note to middleware docs 2026-02-16 08:52:28 -05:00
Jeremiah Lowin
d3032c10a2
Merge pull request #3193 from jfBiswajit/fix/readme-logo
fix: correct dark mode logo file extension in README
2026-02-16 08:40:16 -05:00
Biswajit Biswas
964ba12bc5 fix: correct dark mode logo file extension in README 2026-02-16 15:56:10 +06:00
Jeremiah Lowin
b14b137fdd
Merge pull request #3188 from jlowin/feature/dev-subcommand-group
Move `fastmcp dev` to `fastmcp dev inspector`
2026-02-13 21:47:29 -06:00
marvin-context-protocol[bot]
edac1e5427 chore: Update SDK documentation 2026-02-14 03:43:26 +00:00
Jeremiah Lowin
618195c065
Move fastmcp dev to fastmcp dev inspector 2026-02-13 22:42:38 -05:00
Jeremiah Lowin
ccfe5047bf
Fix include_tags/exclude_tags ignored without tools in MCPConfig (#3186)
* Fix include_tags/exclude_tags ignored without tools in MCPConfig

* chore: Update SDK documentation

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-13 20:38:56 -05:00
dependabot[bot]
bb60287005
chore(deps): bump cryptography in the uv group across 1 directory (#3147)
Bumps the uv group with 1 update in the / directory: [cryptography](https://github.com/pyca/cryptography).


Updates `cryptography` from 46.0.4 to 46.0.5
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/46.0.4...46.0.5)

---
updated-dependencies:
- dependency-name: cryptography
  dependency-version: 46.0.5
  dependency-type: indirect
  dependency-group: uv
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-13 17:38:02 -05:00
Jeremiah Lowin
e5553177d7
docs: update all references from 3.0.0b2 to 3.0.0rc1 (#3173) 2026-02-12 16:39:16 -06:00
Jeremiah Lowin
5f941be5a3
Fix stale request context in StatefulProxyClient handlers (#3172)
* fix: restore request context in StatefulProxyClient handlers

StatefulProxyClient reuses sessions across requests, so its receive-loop
task inherits a stale request_ctx ContextVar from the first request.
Server-initiated messages (elicitation, sampling, etc.) that depend on
related_request_id routing get sent to a closed stream and hang forever.

Closes #3169

* chore: Update SDK documentation

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-12 17:13:23 -05:00
Jeremiah Lowin
b62ed3408b
Support non-serializable values in Context.set_state (#3171)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-12 15:06:42 -06:00
Jeremiah Lowin
88c89248d0
Merge pull request #3157 from jlowin/update-claude-workflows
Updates to github actions / workflows for claude
2026-02-12 14:49:16 -05:00
Jeremiah Lowin
3907a4f9b6
docs: fix stale get_* references, now list_* (#3168) 2026-02-12 13:45:49 -06:00
Jeremiah Lowin
175479f65f
fix: guard client pagination loops against misbehaving servers (#3167)
* fix: guard client pagination loops against misbehaving servers

Treat empty/falsy nextCursor as end-of-pagination and detect cursor
cycles across all client list methods and the server context proxy
helper.

* chore: Update SDK documentation

---------

Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-12 13:19:02 -06:00
Bill Easton
359575b12f
Add helpers for converting FunctionTool and TransformedTool to SamplingTool (#3062)
Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-11 19:43:03 -06:00
Jeremiah Lowin
a307e9c3cc
Expose local_provider property, deprecate FastMCP.remove_tool() (#3155)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-11 19:37:19 -06:00
William Easton
16a85ffcd8
pretty yaml 2026-02-11 16:07:50 -06:00
William Easton
b42f6f9a48
update conditionals 2026-02-11 16:07:22 -06:00
William Easton
c9fbd1f5de
Merge origin/main into update-claude-workflows
Resolve conflict: remove old martian-issue-triage.yml (replaced by martian-triage-issue.yml)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-11 15:56:47 -06:00
William Easton
b3b630ee7b
Updates to github actions / workflows for claude 2026-02-11 15:52:54 -06:00
Jeremiah Lowin
fe57c3d689
Make $ref dereferencing optional via FastMCP(dereference_refs=...) (#3151)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-11 13:45:37 -05:00
Jeremiah Lowin
50b23299f8
Support async auth checks (#3152)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-11 12:48:51 -05:00
Jeremiah Lowin
25e2f4da32
Remove deprecated FastMCP() constructor kwargs (#3148)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-11 11:39:34 -05:00
Jeremiah Lowin
0f95ed72cd
Stop duplicating path parameter descriptions into tool prose (#3149)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-11 11:34:11 -05:00
Guillaume FORTAINE
263e0bf6e0
fix: snapshot access token for background tasks (#3095) (#3138)
Co-authored-by: cristiangreco94 <cristiangreco94@users.noreply.github.com>
2026-02-11 10:20:14 -05:00
dependabot[bot]
f75cd05e50
chore(deps): bump cryptography (#3140)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-11 10:19:38 -05:00
Jeremiah Lowin
3e79572660
docs: remove nonexistent MSALApp from rc1 notes (#3146) 2026-02-11 10:19:02 -05:00
Jeremiah Lowin
f3d33f830c
docs: add v3.0.0rc1 section to v3-features tracking (#3145) 2026-02-11 10:16:54 -05:00
Bill Easton
8e1f662d93
Bump py-key-value-aio to >=0.4.0,<0.5.0 (#3143)
Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-11 10:03:45 -05:00
Chris Guidry
efcc12bc76
Merge pull request #3136 from jlowin/task-elicitation-relay
Relay task elicitation through standard MCP protocol
2026-02-10 16:06:37 -05:00
marvin-context-protocol[bot]
a53030a806 chore: Update SDK documentation 2026-02-10 21:00:34 +00:00
Chris Guidry
6e22914360 Move relay logic to elicitation.py, fix related-task metadata key
Moves relay_elicitation() into elicitation.py so it can reuse
handle_task_input() for the Redis push instead of duplicating that logic.
notifications.py just detects the trigger and calls it.

Also fixes the related-task metadata key from modelcontextprotocol.io/ to
io.modelcontextprotocol/ to match the current spec:
https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 16:00:01 -05:00
Chris Guidry
a95fed3041
Merge branch 'main' into task-elicitation-relay 2026-02-10 15:41:25 -05:00
marvin-context-protocol[bot]
95b4271b3b chore: Update SDK documentation 2026-02-10 20:36:17 +00:00
Chris Guidry
aa4db3d00e Address review: set status key on cancel fallback, prevent relay task GC
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 15:35:50 -05:00
Chris Guidry
95816c5ae2
Merge pull request #2906 from gfortaine/fix/statusMessage-not-forwarded
feat: distributed notification queue + BLPOP elicitation for background tasks
2026-02-10 15:32:55 -05:00
Chris Guidry
361eb08f42 Relay task elicitation through standard MCP protocol
When a background task calls ctx.elicit(), the notification subscriber now
detects the input_required notification and sends a standard elicitation/create
request to the client via session.elicit(). The client's elicitation_handler
fires, and the relay pushes the response to Redis for the blocked worker.

This means clients can respond to background task elicitation using the same
elicitation_handler they'd use for any other elicitation — no need to interact
with Redis or call handle_task_input() directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 14:58:19 -05:00
Chris Guidry
3809f0c77b
Merge branch 'main' into fix/statusMessage-not-forwarded 2026-02-10 14:22:58 -05:00
Jeremiah Lowin
1d0c0adeab
Add validate_output option for OpenAPI tools (#3134)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-10 13:29:45 -05:00
Jeremiah Lowin
a715176499
Fix unhandled exceptions in OpenAPI POST tool calls (#3133)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-10 13:13:45 -05:00
Jeremiah Lowin
40e80d60e5
Fix session visibility marks leaking across sessions (#3132) 2026-02-10 11:42:43 -05:00
Chris Guidry
f7c47f498c
Merge branch 'main' into fix/statusMessage-not-forwarded 2026-02-10 11:34:05 -05:00
Bill Easton
cdad99583e
Fix Windows test timeouts in OAuth proxy provider tests (#3123)
Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-02-09 20:54:16 -05:00
Jeremiah Lowin
eeb17855a6
docs: add pre-registered OAuth clients to v3-features (#3129) 2026-02-09 20:48:31 -05:00
marvin-context-protocol[bot]
a1cf2aff6e
chore: Update SDK documentation (#3116)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-09 20:45:52 -05:00
Jeremiah Lowin
81a7c83c67
CI: Commit generated artifacts back to PR branch instead of opening separate PRs (#3128) 2026-02-09 20:45:28 -05:00
Bill Easton
5bab188106
Add concurrent tool execution with sequential flag (#3022)
Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-02-09 20:43:53 -05:00
Martim Santos
939cf5fcf2
feat: add Static Client Registration (#3085) (#3086)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-02-09 20:40:06 -05:00
Jeremiah Lowin
45af482e73
Add Azure OBO dependencies, auth token injection, and documentation (#2918) 2026-02-09 20:06:57 -05:00
Jeremiah Lowin
dfb857aa08
Scope Martian triage to bug-labeled issues for jlowin (#3124) 2026-02-09 10:35:53 -05:00
Jeremiah Lowin
3b0660a686
Rename ui= to app= and consolidate ToolUI/ResourceUI into AppConfig (#3117)
* Rename ui= to app= and consolidate ToolUI/ResourceUI into AppConfig

* Remove backward-compat aliases for ToolUI/ResourceUI/ui_to_meta_dict

* Add extra=allow to AppConfig model_config for forward compatibility
2026-02-08 20:26:47 -05:00
Jeremiah Lowin
f7cdd20a42
generate-cli: auto-generate SKILL.md agent skill (#3115)
* generate-cli: auto-generate SKILL.md alongside CLI script

generate-cli now produces a SKILL.md agent skill file next to the CLI
script, documenting every tool's exact invocation syntax, parameter
flags, and types. Agents can use the CLI immediately without discovery.

* Use uv run --with fastmcp in generated SKILL.md invocations

* Fix skill generation issues from review

- Escape pipe chars in union type labels so markdown tables render
- Boolean params omit <value> placeholder in example invocations
- Quote YAML frontmatter values to handle special chars in names
- Match cyclopts camelCase→snake_case in flag derivation
- Use four-backtick fence for nested code block in docs

* Replace --skill/--no-skill with just --no-skill

* Escape quotes in YAML frontmatter description

* Strip newlines from param descriptions in skill table rows

* Detect boolean union types for flag placeholder
2026-02-08 17:09:28 -05:00
Guillaume FORTAINE
3d665d48c0 fix: stabilize task notification integration tests
🤖 Generated with Codex
2026-02-08 15:20:22 +01:00
Guillaume FORTAINE
1f84e5055e test: rewrite as integration tests with zero mocks
Replace 1300+ lines of mock-heavy unit tests with 391 lines of integration
tests using real Client(mcp) connections and memory:// Docket backend.

- test_context_background_task.py: 17 tests covering report_progress delta
  tracking, elicitation flow, edge cases, and fail-fast on push failure
- test_notifications.py: 2 E2E tests for notification queue lifecycle
2026-02-08 01:59:07 +01:00
Guillaume FORTAINE
5dafe19a9d fix: store progress in Redis and use BLPOP for elicitation
- context.py: report_progress uses delta tracking via increment() instead
  of set_current() (which doesn't exist), stores progress in Redis for
  background tasks
- elicitation.py: replace polling with BLPOP for efficient blocking wait,
  fail-fast on notification push failure, use get_task_context() for
  authoritative session_id
- handlers.py: subscriber cleanup on session disconnect via
  _exit_stack.push_async_callback()
2026-02-08 01:59:04 +01:00
Guillaume FORTAINE
ecbce07636 feat: distributed notification queue for background task elicitation
Add Redis-backed notification queue (LPUSH/BRPOP) enabling the MCP server
to notify clients about background task events like elicitation requests.

- notifications.py: subscriber management with weakref tracking, retry
  logic, TTL expiration, and graceful shutdown
- __init__.py: export ensure_subscriber_running, push_notification,
  stop_subscriber
2026-02-08 01:59:01 +01:00
Jeremiah Lowin
806aa8c579
Update docs to reference beta 2 (#3112) 2026-02-07 10:33:12 -05:00
SrzStephen
1ed9770892
Updated deprecation URL (#3108)
For V3 this should be https://gofastmcp.com/servers/dependency-injection#using-depends
For V2 this should be https://gofastmcp.com/v2/servers/context#using-depends

current url fails for both v2 and v3 documentation

https://gofastmcp.com/servers/dependencies
https://gofastmcp.com/v2/servers/dependencies
2026-02-07 08:18:39 -05:00
Jeremiah Lowin
25f3b0878e
Add missing beta2 features to v3 release tracking (#3105)
generate-cli, goose integration, response limiting middleware,
background task context, require_auth removal
2026-02-06 20:27:42 -05:00
marvin-context-protocol[bot]
3e3ed76a8c
chore: Update SDK documentation (#3089)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-06 20:27:29 -05:00
Jeremiah Lowin
d12d46b049
Exclude content-type header from get_http_headers() to prevent HTTP 415 errors (#3104)
Fixes #3097

When using FastMCP.from_openapi() with APIs that require specific
Content-Type headers (e.g., application/vnd.api+json), the transport
connection's content-type: application/json was being injected into
downstream API requests, causing HTTP 415 (Unsupported Media Type) errors.

This change adds content-type to the exclude_headers set in get_http_headers(),
similar to how accept is already excluded. The MCP transport's content type
has no relevance to downstream API calls and should not be forwarded.

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2026-02-06 20:19:36 -05:00
Jeremiah Lowin
931d6f878c
Remove require_auth; fix auth docs re: component-level enforcement (#3103)
🤖 Generated with Claude Code

https://claude.ai/code/session_01WWzwcBfLWnxoN9XNs5Fhxr

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-06 20:08:50 -05:00
Jeremiah Lowin
ad3b1b9d1b
Fix CIMD redirect allowlist bypass and cache revalidation (#3098)
* Harden CIMD redirect and cache handling

* Preserve CIMD cache policy on 304 revalidation

* Refresh 304 cache expiry from cached lifetime
2026-02-06 20:08:23 -05:00
Jeremiah Lowin
85eff33b81
Infer MIME types from OpenAPI response definitions (#3101)
* Infer mime_type from OpenAPI response content types for resources

🤖 Generated with Claude Code

https://claude.ai/code/session_01FZD5ZT8WiQqfBu39ybuQis

* Handle media types without schemas in MIME inference

🤖 Generated with Claude Code

https://claude.ai/code/session_01FZD5ZT8WiQqfBu39ybuQis

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-06 20:08:08 -05:00
Jeremiah Lowin
6a358902f2
Fix compress_schema to preserve additionalProperties: false for MCP compatibility (#3102)
Changes:
- Changed default of prune_additional_properties from True to False in compress_schema
- Added test demonstrating MCP client compatibility requirement
- Updated existing tests to explicitly enable pruning when needed
- Added additionalProperties: false to manually constructed schemas in tool_transform
- Updated inline snapshots to reflect new behavior

Fixes #3008

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2026-02-06 18:52:21 -05:00
Jeremiah Lowin
b8d789c1b4
Document token passthrough security in OAuth Proxy docs (#3100) 2026-02-06 18:20:17 -05:00
Jeremiah Lowin
32c6826e13
Add note about output_schema incongruity when responses are truncated (#3099) 2026-02-06 18:15:57 -05:00
Diogo Santos
30832ced1c
Add ResponseLimitingMiddleware for tool response size control (#3072) 2026-02-06 18:13:26 -05:00
Jeremiah Lowin
880d835ccc
Add CIMD (Client ID Metadata Document) support for OAuth (#2871) 2026-02-06 13:44:52 -05:00
Jeremiah Lowin
db650ca7cb
Update Anthropic and OpenAI clients to use Omit instead of NotGiven (#3088)
* Fix ty 0.0.15 type errors: use Omit/omit instead of NotGiven/NOT_GIVEN

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fs5vHiWaUebe826pGq4eCN

* Use kwargs dict to avoid NotGiven/Omit sentinel type issues across SDK versions

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fs5vHiWaUebe826pGq4eCN

* Bump ty minimum to 0.0.15

🤖 Generated with Claude Code

https://claude.ai/code/session_01Fs5vHiWaUebe826pGq4eCN

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-05 11:40:25 -05:00
marvin-context-protocol[bot]
5e0211dd5e
chore: Update SDK documentation (#3069)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-04 18:31:07 -05:00
Diogo Santos
b776089ecc
Add @handle_tool_errors decorator for standardized error handling (#2885)
* Add @handle_tool_errors decorator for standardized error handling

* Add tests for @handle_tool_errors decorator

* Add documentation for @handle_tool_errors decorator

* Fix type checking: use getattr for func.__name__ with fallback

* Add @overload declarations for proper async/sync type checking

* Fix type checking: reorder overloads and use Coroutine for async typing

* Update lockfile and fix test formatting

* Improve error_handling module: add docstrings, fix logging, handle cancellation, and update documentation

* Add auth error mappings, doc tweaks, and doc fix

* Pivot to hybrid approach

* Remove decorator, keep only core 429/timeout handling

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-02-04 18:30:31 -05:00
Richard Michael
d5f5300e63
Add server version to banner (#3076) 2026-02-04 17:40:09 -05:00
Jeremiah Lowin
422384e576
Fix --reload port conflict when using explicit port (#3070)
* Fix --reload port conflict by killing entire process group

* Gate start_new_session on Unix (no-op on Windows)
2026-02-04 17:35:39 -05:00
Nathan
76f054e957
fix: enforce redirect URI validation when allowed_client_redirect_uris is supplied (#3066)
* fix: enforce redirect URI validation when patterns are explicitly configured

Security fix: When allowed_redirect_uri_patterns is explicitly set, reject redirect URIs that don't match the patterns instead of falling back to parent validation. This prevents unauthorized OAuth clients from bypassing the allowlist and accessing protected resources.

* Update models.py

no need to return twice

* fix redirect uri access issue

* update style

* feat: add unit test to enforce fallback not applied when redirect uri's supplied

* fix: improve test case

* apply linter

* refactor: simplify logic and do not exposed allowed redirect patterns

---------

Co-authored-by: Nathan <2381793w@student.gla.ac.uk>
2026-02-04 17:33:33 -05:00
Jeremiah Lowin
4262cfc16a
Add fastmcp generate-cli command (#3065)
* Add `fastmcp generate-cli` command

Connects to any MCP server, reads its tool/resource/prompt schemas,
and writes a standalone Python CLI script with typed subcommands.

* docs: add generate-cli documentation

* docs: add generate-cli documentation; skip Windows executable test

* fix: address PR review feedback

- Sanitize tool and parameter names to valid Python identifiers
- Replace bare except Exception with specific exception types
- Escape server name in generated string literals
- Handle trailing colon edge case in _derive_server_name
- Clarify in docs that generated CLI is a client, not a bundled server

* Fix string escaping issues in generate-cli

- Use single-quoted docstrings to avoid triple-quote escaping issues
- Escape quotes in app_name derived from server_name
- Add tests for descriptions with quotes and server names with quotes

Addresses CodeRabbit review comments about insufficient escaping.

* Implement smart parameter handling for generate-cli

- Simple types (str, int, float, bool): Direct typed flags
- Arrays of simple types (list[str], list[int]): Repeatable flags via cyclopts
- Complex types (objects, nested arrays): Accept JSON strings with parsing
- JSON schema shown in help text for complex parameters
- Proper escaping of newlines and quotes in help text
- Filter out None and empty list defaults when calling tools

This gives typed, discoverable CLIs for common cases while handling
complex schemas via JSON input.

* Update generate-cli docs to explain smart parameter handling

- Document simple types as direct typed flags
- Document arrays of simple types as repeatable flags
- Document complex types as JSON strings with schema in help
- Add examples showing all three patterns

* Fix Codex review issues in generate-cli

High priority fixes:
- Complex type defaults: Serialize dict/list defaults to JSON strings
- List params: Preserve help metadata with Annotated wrapper
- Name collisions: Detect and error on sanitized name conflicts
- JSON parsing: Use isinstance check for safety with defaults

Added tests for:
- Complex types with default values
- Parameter name collision detection
- Updated existing tests to match new format

* Use pydantic_core.to_json for consistency

- Generator now uses pydantic_core.to_json() instead of json.dumps()
- Consistent with rest of fastmcp codebase
- Generated CLI still uses plain json module (standalone script)

* Move local imports to module level in generate-cli

* Handle union item types and Python keyword collisions in generate-cli
2026-02-03 21:08:51 -05:00
marvin-context-protocol[bot]
f6988c9206
chore: Update SDK documentation (#2995)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-02-03 21:08:20 -05:00
Jeremiah Lowin
5fd41b2e15
Remove OpenAPI timeout parameter, make client optional, surface timeout errors (#3067)
* Remove OpenAPI timeout param, make client optional, surface timeout errors

* Close auto-created httpx client via provider lifespan
2026-02-03 21:08:09 -05:00
Jeremiah Lowin
fe432de156
Add release notes for v2.14.4 and v2.14.5 (#3064) 2026-02-03 10:47:06 -05:00
Bill Easton
49707813f4
Mock network calls in CLI tests and use MemoryStore for OAuth tests
Mock network calls in CLI tests and use MemoryStore for OAuth tests
2026-02-02 19:15:06 -06:00
Bill Easton
32017f7fb1
Merge branch 'main' into claude/issue-3049-20260131-2232 2026-02-02 19:08:11 -06:00
Jeremiah Lowin
b076b2154c
Add AzureJWTVerifier for Managed Identity token verification (#3058) 2026-02-02 19:59:13 -05:00
Bill Easton
c09e748a12
Merge branch 'main' into claude/issue-3049-20260131-2232 2026-02-02 18:52:05 -06:00
Guillaume FORTAINE
08974e50d9
feat(context): Add background task support for Context (SEP-1686) (#2905) 2026-02-02 19:28:42 -05:00
Neelay Shah
c8e2c621ef
fix: Preserve metadata in FastMCPProvider component wrappers (#3057) 2026-02-02 10:26:24 -05:00
Bill Easton
b3b26e602c
Merge branch 'main' into claude/issue-3049-20260131-2232 2026-02-01 20:49:43 -06:00
Bill Easton
6fa90fa792
fix: use SkipJsonSchema to exclude callable fields from JSON schema generation (#3048)
Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-01 21:30:48 -05:00
Jeremiah Lowin
186604a8a2
Merge branch 'main' into claude/issue-3049-20260131-2232 2026-02-01 21:29:57 -05:00
Bill Easton
b2f5551d22
Fix Field() handling in prompts (#3050)
Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-02-01 21:28:44 -05:00
Jeremiah Lowin
adf21ac630
Add fastmcp discover and name-based server resolution (#3055) 2026-02-01 21:27:22 -05:00
Jeremiah Lowin
bd37763e98
Add fastmcp list and fastmcp call CLI commands (#3054) 2026-02-01 18:30:14 -05:00
claude[bot]
cec40b378d Use MemoryStore for OAuth proxy tests
Updated all OAuthProxy test instantiations to use MemoryStore instead of defaulting to DiskStore, avoiding SQLite timeout issues on Windows and improving test performance.

Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
2026-02-01 02:30:05 +00:00
claude[bot]
cd085412d1 Mock network call in test_version_command_execution
Fixes #3049 by mocking check_for_newer_version to prevent real network
calls to PyPI during tests, which was causing timeouts on Windows.

Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
2026-01-31 22:35:43 +00:00
Bill Easton
684cbff634
Merge pull request #3047 from jlowin/claude/fix-workflow-link-b4Vrn 2026-01-31 15:43:28 -06:00
Bill Easton
82c9409006
Merge branch 'main' into claude/fix-workflow-link-b4Vrn 2026-01-31 15:23:11 -06:00
Bill Easton
96569177a8
Merge pull request #2996 from didier-durand/fix-typos-b 2026-01-31 15:10:53 -06:00
Bill Easton
6b1394ebf8
Merge branch 'main' into fix-typos-b 2026-01-31 15:01:57 -06:00
Bill Easton
4e10ad6299
Merge branch 'main' into claude/fix-workflow-link-b4Vrn 2026-01-31 15:01:12 -06:00
Bill Easton
70f0fd3a0a
Merge pull request #3014 from jlowin/claude/issue-3011-20260128-0658 2026-01-31 15:00:53 -06:00
William Easton
b7344d7045
chore: Trigger CI build 2026-01-31 14:58:19 -06:00
Bill Easton
c7eb47183d
Merge branch 'main' into claude/issue-3011-20260128-0658 2026-01-31 14:55:13 -06:00
Claude
4544b634c6
fix: correct workflow run link template variable
🤖 Generated with Claude Code

https://claude.ai/code/session_0116NLbUNNQwyQySTawmshQT
2026-01-31 20:51:25 +00:00
Jeremiah Lowin
e17d50e0e8
fix: use MCP spec error code -32002 for resource not found (#3041) 2026-01-31 10:57:05 -05:00
Jeremiah Lowin
8de1470973
chore: upgrade protobuf to 6.33.5 (CVE-2026-0994) (#3043) 2026-01-31 10:56:50 -05:00
Jeremiah Lowin
62f532454e
chore: upgrade python-multipart to 0.0.22 (CVE-2026-24486) (#3042) 2026-01-31 10:56:42 -05:00
Jeremiah Lowin
1bbbee37a6
feat: add fastmcp install goose command (#3040) 2026-01-31 10:53:24 -05:00
Jeremiah Lowin
12cb58f220
MCP Apps: structured CSP/permissions types, resource meta propagation fix, QR example (#3031) 2026-01-30 14:05:25 -05:00
Jeremiah Lowin
2629631536
docs: document expanded reload behavior and restructure beta sections (#3039)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2026-01-30 13:51:24 -05:00
Bill Easton
9a47d30266
Merge pull request #3033 from jlowin/strawgate-patch-1 2026-01-30 09:05:26 -06:00
Bill Easton
80188a1bd0
Update martian-issue-triage.yml 2026-01-29 21:59:54 -06:00
Jeremiah Lowin
003df5b22b
Add fastmcp install stdio command (#3032) 2026-01-29 22:19:15 -05:00
Jeremiah Lowin
d52534ae32
Add MCP Apps Phase 1 — SDK compatibility (SEP-1865) (#3009) 2026-01-29 16:28:03 -05:00
Jeremiah Lowin
d9cbc06ff1
feat: expand --reload to watch frontend file types (#3028) 2026-01-29 09:23:55 -05:00
Jonas Krüger Svensson
8b3010825e
fix: automatically include offline_access as a scope in the Azure provider to enable automatic token refreshing (#3001)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-01-29 09:15:09 -05:00
Jeremiah Lowin
f4842cd3e6
Fix ty 0.0.14 compatibility and upgrade dependencies (#3027) 2026-01-29 09:10:42 -05:00
Jonas Krüger Svensson
46e0e01066
fix: correctly send resource when exchanging code for the upstream to… (#3013) 2026-01-29 08:39:12 -05:00
Bill Easton
17d3f13f24
fix: type narrowing for skills resource contents (#3023)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
2026-01-29 08:36:28 -05:00
Bill Easton
de22bbb6cb
Merge branch 'main' into claude/issue-3011-20260128-0658 2026-01-28 19:37:15 -06:00
Jonas Krüger Svensson
fa5b136205
feat: option to add upstream claims to the FastMCP proxy JWT (#2997) 2026-01-28 15:57:24 -05:00
Samir Amin
6edd5e699e
fix: broaden combine_lifespans type to accept Mapping return types (#3005) 2026-01-28 15:54:59 -05:00
Jeremiah Lowin
d3bb9a0514
Merge branch 'main' into claude/issue-3011-20260128-0658 2026-01-28 15:01:16 -05:00
Bill Easton
05380f23fc
Move tests to a reusable action and enable nightly checks (#3017) 2026-01-28 15:00:54 -05:00
Bill Easton
82a00dde41
Merge branch 'main' into claude/issue-3011-20260128-0658 2026-01-28 09:28:27 -06:00
nate nowack
5da65096fc
fix phue2 import in smart_home example (#2999)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-01-28 10:08:33 -05:00
Bill Easton
311e173b99
Adjust workflow triggers for Marvin (#3010) 2026-01-28 10:06:58 -05:00
claude[bot]
cfba439417 Fix tool_choice reset for structured output sampling
Preserve tool_choice='required' when result_type is set to ensure
LLM calls final_response instead of returning text responses.

Fixes #3011

Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
2026-01-28 07:02:05 +00:00
Chris Guidry
fdc96f323a
Merge pull request #2998 from jlowin/pydocket-3x-validation
Bump pydocket to 0.17.2 (memory leak fix)
2026-01-26 11:30:07 -05:00
Chris Guidry
20e84a9cfb Bump pydocket to >=0.17.2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-26 11:11:41 -05:00
Chris Guidry
61aeffb90c Bump pydocket to >=0.17.2b3
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 20:57:06 -05:00
Chris Guidry
7002fd45c3 Bump pydocket to >=0.17.2b2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 16:14:26 -05:00
Chris Guidry
1345ea7f42 Bump pydocket to >=0.17.2b1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 08:10:05 -05:00
Chris Guidry
e82bb06025 Test pydocket fix-cancellation-handling on 3.x
Points pydocket to the fix-cancellation-handling branch to validate
asyncio cancellation handling works correctly on the 3.x branch.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 16:36:12 -05:00
Didier Durand
96280b3d2c
[Doc]: fixing spelling issues in multiple files 2026-01-23 07:17:38 +01:00
marvin-context-protocol[bot]
fde9068efb
chore: Update SDK documentation (#2990)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-01-22 19:41:15 -05:00
Abhijeeth Padarthi
cee99d1210
Normalize resource URLs before comparison to support RFC 8707 query parameters (#2967)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2026-01-22 19:41:02 -05:00
Jeremiah Lowin
c1f50e49be
Add enabled field to ToolTransformConfig (#2991) 2026-01-22 17:44:38 -05:00
Jeremiah Lowin
b77484e1a7
Run static checks with --upgrade, remove lockfile check (#2988) 2026-01-22 12:31:31 -05:00
Jeremiah Lowin
606ce46db1
Remove uv lockfile monitoring from Dependabot (#2986) 2026-01-22 09:41:21 -05:00
dependabot[bot]
f0c77a32be
Bump prek from 0.2.30 to 0.3.0 (#2982)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 09:27:40 -05:00
Jeremiah Lowin
ea5e8c66c1
Skip label triage for Dependabot PRs (#2985) 2026-01-22 09:27:15 -05:00
dependabot[bot]
783d402038
Bump ty from 0.0.12 to 0.0.13 (#2984)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-22 09:26:40 -05:00
marvin-context-protocol[bot]
1ad3704e17
chore: Update SDK documentation (#2979)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-01-22 09:23:29 -05:00
Jeremiah Lowin
3745810db2
Make duplicate bot less aggressive (#2981) 2026-01-22 09:22:12 -05:00
Jeremiah Lowin
884b81a0f8
Add dependency injection documentation and DI-style dependencies (#2980) 2026-01-22 09:08:20 -05:00
Jeremiah Lowin
41c56481af
Pin httpx < 1.0 and simplify beta install docs (#2975) 2026-01-21 21:08:46 -05:00
Jeremiah Lowin
72373c9d0b
Remove outdated 'FastMCP 3.0 is coming!' CLI banner (#2974) 2026-01-21 20:52:40 -05:00
Jeremiah Lowin
62cc28c7b0
Update FastMCP Cloud references to Prefect Horizon (#2978) 2026-01-21 20:52:08 -05:00
Jeremiah Lowin
6efbc5db1d
Restructure README and docs with motivated narrative (#2964) 2026-01-20 22:30:57 -05:00
Jeremiah Lowin
27d318810f
Fix FastAPI mounting examples in docs (#2962) 2026-01-20 17:58:39 -05:00
Jeremiah Lowin
287ae44ff3
Restructure README and welcome page with motivated narrative (#2963) 2026-01-20 17:58:10 -05:00
Jeremiah Lowin
1a61361764
Update brand images (#2960) 2026-01-20 10:52:30 -05:00
Jeremiah Lowin
abccd53573
Update example MCP version in installation docs (#2959) 2026-01-20 09:24:31 -05:00
Jeremiah Lowin
4b1a7f51b0
Remove pip --pre warning from installation docs (#2958)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-20 08:40:24 -05:00
Jeremiah Lowin
6dba73b69d
Restructure docs: move transforms to dedicated section (#2956) 2026-01-20 00:38:35 -05:00
Jeremiah Lowin
2cf7b0b0d7
Update version dropdown (#2955) 2026-01-20 00:17:48 -05:00
Jeremiah Lowin
0be4cda159
Prepare docs for v3.0 beta release (#2954) 2026-01-20 00:13:18 -05:00
Jeremiah Lowin
c14ef132da
Clarify installation for FastMCP 3.0 beta (#2953) 2026-01-19 23:24:47 -05:00
Jeremiah Lowin
3df38584cf
Fix broken documentation links (#2952) 2026-01-19 23:17:29 -05:00
Jeremiah Lowin
3af9de197a
Restructure documentation for FastMCP 3.0 (#2951) 2026-01-19 21:33:35 -05:00
marvin-context-protocol[bot]
8596c09fdf
chore: Update SDK documentation (#2949)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-01-19 21:22:39 -05:00
Jeremiah Lowin
0e4d519c86
Rename Enabled transform to Visibility (#2950) 2026-01-19 20:46:58 -05:00
Jeremiah Lowin
ba18a716bf
Add client utilities for downloading skills (#2948) 2026-01-19 20:10:39 -05:00
marvin-context-protocol[bot]
7d40b1dd4c
chore: Update SDK documentation (#2945)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-01-19 19:22:38 -05:00
Jeremiah Lowin
16ffc9432f
Add Skills Provider for exposing agent skills as MCP resources (#2944) 2026-01-19 18:29:16 -05:00
Jeremiah Lowin
c0ef90e713
Add PromptsAsTools transform (#2946) 2026-01-19 18:19:53 -05:00
Jeremiah Lowin
15921ca960
Update v3 features that were missed in PRs (#2947)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-19 18:12:38 -05:00
Jeremiah Lowin
d3327269d7
Add ResourcesAsTools transform (#2943) 2026-01-19 17:40:31 -05:00
marvin-context-protocol[bot]
a036ad31e2
chore: Update SDK documentation (#2940)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-01-19 16:49:12 -05:00
Jeremiah Lowin
4d2feb0c29
Refactor transform list methods to pure function pattern (#2942) 2026-01-19 16:21:35 -05:00
Jeremiah Lowin
23bfdf0680
Consolidate test fixtures and refactor large test files (#2941) 2026-01-19 15:18:35 -05:00
Jeremiah Lowin
e376bfcfb2
Refactor server.py into mixins (#2939) 2026-01-19 11:36:00 -05:00
marvin-context-protocol[bot]
c9c8b41bde
chore: Update SDK documentation (#2938)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-01-19 10:17:10 -05:00
marvin-context-protocol[bot]
e98e04fb83
chore: Update SDK documentation (#2932)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-01-19 10:16:14 -05:00
Jeremiah Lowin
4e663593b8
Refactor LocalProvider into mixin modules (#2936) 2026-01-19 10:16:01 -05:00
Jeremiah Lowin
c99e0c6351
Refactor OAuthProxy into focused modules (#2935) 2026-01-19 10:10:07 -05:00
Jeremiah Lowin
352ff82eab
Refactor Client class into mixins and add timeout utilities (#2933) 2026-01-19 09:54:47 -05:00
Jeremiah Lowin
323dc1ef63
Allow marvin-context-protocol bot in label triage workflow (#2934) 2026-01-19 09:53:19 -05:00
Jeremiah Lowin
71d6f49e14
Bump dependencies (#2931) 2026-01-19 08:38:48 -05:00
marvin-context-protocol[bot]
36876c5148
chore: Update SDK documentation (#2930)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-01-19 08:31:18 -05:00
Jeremiah Lowin
b25ac3f5fa
Move session visibility logic to enabled.py (#2924) 2026-01-19 08:29:23 -05:00
marvin-context-protocol[bot]
5c457f2ef4
chore: Update SDK documentation (#2922)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-01-18 22:35:25 -05:00
Jeremiah Lowin
d8ab493664
Add session-specific visibility control via Context (#2917) 2026-01-18 22:31:06 -05:00
Jeremiah Lowin
8b0d016c30
Split transports.py into modular structure (#2921) 2026-01-18 22:11:25 -05:00
Jeremiah Lowin
2d200a887b
Fix custom provider docs to show correct interface (#2920) 2026-01-18 21:37:30 -05:00
marvin-context-protocol[bot]
d9f2e67f70
chore: Update SDK documentation (#2916)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-01-18 21:32:20 -05:00
Jeremiah Lowin
c3111a8978
Unify discovery API: deduplicate at protocol layer only (#2919) 2026-01-18 21:01:53 -05:00
Jeremiah Lowin
ed0587d683
Immutable transform wrapping for providers (#2913) 2026-01-18 19:37:29 -05:00
Jeremiah Lowin
c9a202f27d
Merge pull request #2915 from jlowin/remove-sync-notifications 2026-01-18 17:36:05 -05:00
Jeremiah Lowin
f603fe094a Remove sync notification infrastructure
Remove send_notification_sync() method, notification queue, and background flusher task. Component add/remove operations happen outside sessions and no longer need notifications.
2026-01-18 17:31:11 -05:00
Jeremiah Lowin
e9bae4098f
Merge pull request #2900 from jlowin/marvin/update-sdk-docs 2026-01-18 17:30:26 -05:00
marvin-context-protocol[bot]
197184b7ca chore: Update SDK documentation 2026-01-18 21:18:31 +00:00
Jeremiah Lowin
837038677d
Merge pull request #2914 from jlowin/jlowin/version-spec-in-enabled 2026-01-18 16:18:13 -05:00
Jeremiah Lowin
510738b52e Support VersionSpec in enable/disable for range-based filtering
Change version parameter from str to VersionSpec in enable()/disable()
to support range-based version filtering.

- Add match_none parameter to VersionSpec.matches() for controlling
  whether unversioned components match (defaults to True for backward
  compatibility, False for enable/disable filtering)
- Update Enabled transform to use VersionSpec and call matches() with
  match_none=False so unversioned components don't match version specs
- Update enable()/disable() signatures to accept VersionSpec
- Add comprehensive tests for version range matching

Examples:
- disable(version=VersionSpec(eq="v2")) - disable only v2
- disable(version=VersionSpec(gte="v2")) - disable v2 and later
- disable(version=VersionSpec(gte="v1", lt="v3")) - disable v1, v2
2026-01-18 16:00:05 -05:00
Jeremiah Lowin
44a47e1638
Merge pull request #2912 from jlowin/jlowin/mark-based-visibility 2026-01-18 15:19:35 -05:00
Jeremiah Lowin
19526cad65 Fix meta mutation bug; update docs to reflect mark/override semantics 2026-01-18 15:18:15 -05:00
Jeremiah Lowin
84eb35bcb4 Simplify disable calls to use keys only; fix filtering docs 2026-01-18 15:08:39 -05:00
Jeremiah Lowin
c4f506b097 Update test_local_provider.py 2026-01-18 15:01:51 -05:00
Jeremiah Lowin
84d7d3b281 Support plural names/keys in enable/disable API
Change enable() and disable() to accept sets: names, keys, tags.
Use key-based disable for decorator enabled=False to scope exactly.
2026-01-18 14:58:29 -05:00
Jeremiah Lowin
50ba6ea5a4 Refactor visibility to mark-based enabled system
Rename Visibility to Enabled, collapse VisibilityRule into the transform,
and move enabled filtering from Provider to Server level so server-level
transforms can override provider-level disables.
2026-01-18 14:36:33 -05:00
Jeremiah Lowin
2eac7ab1d1
Merge pull request #2903 from jlowin/feature/pagination-support 2026-01-17 20:03:03 -05:00
Jeremiah Lowin
17d43d7991 Address review feedback: validation and edge cases 2026-01-17 19:50:33 -05:00
Jeremiah Lowin
96d4b1b606 Add pagination documentation 2026-01-17 19:30:33 -05:00
Jeremiah Lowin
dada8893a9 Add MCP-compliant pagination for list operations
Server authors opt-in by setting list_page_size on FastMCP.
Client convenience methods auto-fetch all pages transparently.
Use _mcp methods with cursor parameter for manual pagination.
2026-01-17 19:30:24 -05:00
Jeremiah Lowin
96782c0330
Merge pull request #2902 from jlowin/swap-methods 2026-01-17 15:25:46 -05:00
Jeremiah Lowin
9e91ab0f96 Fix docstring example to use private method names 2026-01-17 15:20:34 -05:00
Jeremiah Lowin
6abac8269d Fix reversed comments describing method inheritance 2026-01-17 15:04:36 -05:00
Jeremiah Lowin
cdb098ad6e Merge branch 'main' into swap-methods 2026-01-17 14:48:02 -05:00
Jeremiah Lowin
de1bf9864a fix: update remaining test to use get_prompt 2026-01-17 14:46:58 -05:00
Jeremiah Lowin
850f0a0050 fix: update all tests to use get_prompt instead of _get_prompt 2026-01-17 14:46:21 -05:00
Jeremiah Lowin
2c9cae9a98 refactor: reverse visibility for get_prompt/_get_prompt methods 2026-01-17 14:42:45 -05:00
Jeremiah Lowin
24cf45bb64 refactor: reverse visibility for list_prompts/_list_prompts methods 2026-01-17 14:42:06 -05:00
Jeremiah Lowin
676e43a939 refactor: reverse visibility for get_resource_template/_get_resource_template methods 2026-01-17 14:41:40 -05:00
Jeremiah Lowin
b9e32ca6a6 refactor: reverse visibility for list_resource_templates/_list_resource_templates methods 2026-01-17 14:41:08 -05:00
Jeremiah Lowin
5d12afdbf6
Merge pull request #2901 from jlowin/refactor-provider-inheritance-v2 2026-01-17 14:40:22 -05:00
Jeremiah Lowin
61d98b41d1 refactor: reverse visibility for get_resource/_get_resource methods 2026-01-17 14:40:16 -05:00
Jeremiah Lowin
d6a334257c refactor: reverse visibility for list_resources/_list_resources methods 2026-01-17 14:39:41 -05:00
Jeremiah Lowin
b13e1c7caf refactor: reverse visibility for get_tool/_get_tool methods 2026-01-17 14:38:07 -05:00
Jeremiah Lowin
5fe471dc60 refactor: reverse visibility for list_tools/_list_tools methods 2026-01-17 14:32:53 -05:00
Jeremiah Lowin
3d98862995 Fix auth test to expect None instead of AuthorizationError 2026-01-17 14:25:29 -05:00
Jeremiah Lowin
6dd1de62e4 Address PR review feedback
- Filter task-eligible components in FastMCPProvider.get_tasks()
- Catch AuthorizationError in get_* methods and return None for consistency
- Remove AggregateProvider from top-level exports
2026-01-17 14:20:10 -05:00
Jeremiah Lowin
78ab933dc5 Fix transform application in FastMCPProvider and MCP handlers
FastMCPProvider now calls _get_* methods instead of get_* to ensure
nested server transforms are applied during lookups. Also converts
string versions to VersionSpec in MCP handlers.
2026-01-17 12:37:37 -05:00
Jeremiah Lowin
5b24ea393d Refactor FastMCP to use inherited _get_* methods from Provider
- get_*() now does aggregation + component auth (raises AuthorizationError)
- Deleted _get_*() overrides - inherited from Provider applies transforms
- Simplified AuthMiddleware to global auth only
- Changed version params to VersionSpec | None (not str | None)
- Updated tests to use _get_*() where visibility filtering is expected
2026-01-17 12:15:01 -05:00
Jeremiah Lowin
5ade5a4838 Simplify Provider transform architecture
Replace _get_all_transforms() with a .transforms property that returns
[*self._transforms, self._visibility]. This cleanly separates user transforms
from visibility filtering while keeping visibility applied last (outermost).

Also:
- AuthMiddleware now uses get_* instead of _get_* for proper component auth
- Remove redundant _is_component_enabled checks (visibility is a transform)
- Delete dead code (get_component method)
- Add versions field to FastMCPMeta
- Replace asserts with NotFoundError in component_service
2026-01-17 11:35:47 -05:00
Jeremiah Lowin
ddd2058f97 Merge remote-tracking branch 'origin/main' into refactor-provider-inheritance-v2
# Conflicts:
#	src/fastmcp/server/server.py
2026-01-17 08:41:58 -05:00
Jeremiah Lowin
33c371cb37
Add version discovery and calling a certain version for components (#2897) 2026-01-17 08:27:39 -05:00
Jeremiah Lowin
6ba5e5d194 Fix type errors and add type ignore comments
- Add None checks in auth and tool transform tests
- Add assertions in component_service.py for None returns
- Add type ignore comments for max() with version_sort_key
2026-01-17 07:23:29 -05:00
Jeremiah Lowin
d3ae0ed450 Fix transform application and update tests for None return values
- Override _get_tool/resource/template/prompt in FastMCP to apply
  server transforms over provider aggregation
- Update FastMCPProvider get_* methods to check for None (not
  NotFoundError since get_* now returns None)
- Update versioning tests to expect None instead of NotFoundError
  when requesting filtered/nonexistent versions
2026-01-16 22:21:40 -05:00
Jeremiah Lowin
66d713b367 Merge main into refactor-provider-inheritance-v2
Resolved conflicts to combine Provider inheritance refactor with versioning feature:
- FastMCP now properly inherits from Provider
- get_tool/resource/template/prompt return None instead of raising NotFoundError
- Deduplication uses version_sort_key to keep highest version per name/URI
- _source_* methods eliminated in favor of inherited _* methods
2026-01-16 22:13:29 -05:00
marvin-context-protocol[bot]
c64c4a1b3c
chore: Update SDK documentation (#2896)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-01-16 21:36:46 -05:00
Jeremiah Lowin
53e220a99e
Rename _fastmcp metadata namespace to fastmcp and make non-optional (#2895) 2026-01-16 21:35:53 -05:00
marvin-context-protocol[bot]
1700bc360b
chore: Update SDK documentation (#2834)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-01-16 21:28:11 -05:00
Jeremiah Lowin
a6cd764b5f
Add component versioning and VersionFilter transform (#2894) 2026-01-16 20:53:18 -05:00
dependabot[bot]
39ad1dab8c
Bump the uv group across 1 directory with 2 updates (#2890)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-16 20:30:26 -05:00
dependabot[bot]
8e45bafd3d
Bump the uv group across 1 directory with 4 updates (#2891)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-16 20:30:12 -05:00
Bill Easton
81cbb9bee2
Add enable_rich_logging setting to disable rich formatting (#2893)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
2026-01-16 20:29:29 -05:00
Jeremiah Lowin
85f32959b1 Refactor FastMCP to inherit from Provider
FastMCP now properly inherits from Provider, eliminating ~200 lines of
duplicated _source_* methods. Key changes:

- get_tool/resource/prompt return None instead of raising NotFoundError
- Visibility filter separated from transforms (applied last)
- Nested server middleware runs on both list and execution operations
- Resource auth failure doesn't fall back to templates
- AggregateProvider kept as user-facing utility class
2026-01-16 16:50:56 -05:00
Jeremiah Lowin
c8c84ff911
Add session-scoped state persistence (#2873) 2026-01-16 14:11:21 -05:00
Jeremiah Lowin
266abef49e
Add missing v3 features to tracking docs (#2888) 2026-01-16 13:42:08 -05:00
Marcus Shu
083999ca14
Add client_secret_post authentication to IntrospectionTokenVerifier (#2884) 2026-01-16 13:07:33 -05:00
Chris Guidry
7b9a01c789
Merge pull request #2886 from jlowin/mcp-semconv-2083
Adopt OpenTelemetry MCP semantic conventions
2026-01-15 14:14:18 -05:00
Chris Guidry
eaf2906916 Adopt OpenTelemetry MCP semantic conventions
Updates FastMCP's telemetry to align with the new MCP semantic conventions
from open-telemetry/semantic-conventions#2083. This gives us interoperability
with other MCP implementations while keeping fastmcp.* attributes for things
unique to our framework.

Changes:
- Span names now follow `{method} {target}` format (e.g., `tools/call greet`)
- Added `mcp.method.name` and `mcp.resource.uri` attributes
- Renamed `fastmcp.session.id` to standard `mcp.session.id`
- Kept fastmcp.* attributes for server name, component info, provider details

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 14:08:15 -05:00
Chris Guidry
24467ea197
Merge pull request #2869 from jlowin/otel-2813
Add OpenTelemetry tracing support
2026-01-14 10:26:02 -05:00
Chris Guidry
b94b7e9664 Update loq limits for telemetry additions
🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 10:14:11 -05:00
Chris Guidry
e19cc17cbe Add tests for session ID capture on telemetry spans
Validates that session_id is captured on both client and server
spans when using HTTP transport, and that they share the same ID.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 10:01:57 -05:00
Chris Guidry
07b056b0ab Address CodeRabbit feedback
- Fix potential None session_id in span attributes
- Add return type annotation to _get_parent_trace_context
- Fix type checker issue with ClientFactoryT await pattern

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 09:55:11 -05:00
Chris Guidry
33f952fc48 Address CodeRabbit suggestions
- Add return type annotation to main() in run_with_tracing.py
- Use spread operator for argv construction
- Add type annotations to docs test example
- Use async httpx client and asyncio.sleep in diagnostics server
- Improve subprocess termination handling with timeout fallback

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 09:48:07 -05:00
Chris Guidry
473b0e36ec Address review feedback on telemetry docs
- Be specific about which operations are traced (tools, prompts, resources, resource templates)
- Remove "(not the SDK)" parenthetical
- Consolidate attribute documentation - remove redundancy in Tracing section
- Delete unnecessary examples/diagnostics/__init__.py

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 09:45:41 -05:00
Chris Guidry
ad660c0c43 Address Jeremiah's telemetry feedback
A few improvements based on code review:

- Don't override existing trace context in `extract_trace_context` - if we're
  already in a valid trace (e.g., from HTTP propagation), preserve it rather
  than extracting from MCP meta
- Add exception recording to `delegate_span` to match `server_span` pattern
- Remove unused `get_meter` function (metrics not implemented yet)
- Return `None` instead of `{}` from `inject_trace_context` when nothing to inject
- Clean up trivial tests that were just testing OpenTelemetry's own API

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 09:45:41 -05:00
Chris Guidry
88182d2dae Add otel-desktop-viewer to telemetry docs
Lightweight single-binary alternative to Jaeger for local development.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 09:45:41 -05:00
Chris Guidry
5a94c8810a Update telemetry docs for accuracy
- Lead with opentelemetry-instrument as the default approach
- Move programmatic configuration lower in the page
- Remove unimplemented metrics section
- Fix attribute values (resource_template not template)
- Add auth attributes (enduser.id, enduser.scope)
- Add provider-specific delegation attributes
- Link to OpenTelemetry Python docs

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 09:45:41 -05:00
Chris Guidry
0d513d76b0 Fix test_multi_client_with_logging to filter OpenTelemetry logs
The test was checking caplog.records length but OpenTelemetry emits
internal warning logs that were getting captured. Filter to only the
test's logger to avoid flaky failures.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 09:45:41 -05:00
Chris Guidry
046f845ddf Add OpenTelemetry tracing support
Adds opt-in distributed tracing via OpenTelemetry for observability into
FastMCP server and client operations.

Server spans are created for tool calls, resource reads, and prompt
renders with attributes like component key, component type, provider
type, session ID, and auth context. Client spans wrap outgoing calls
with trace context propagation via W3C headers in request meta.

Components provide their own span attributes through a `get_span_attributes()`
method that subclasses override - this lets LocalProvider, FastMCPProvider,
and ProxyProvider each include relevant context (original names, backend URIs).

To enable: configure an OpenTelemetry SDK with a TracerProvider before
importing fastmcp. Traces export to any OTLP-compatible backend.

Closes ENG-2813

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 09:45:41 -05:00
Chris Guidry
1a12374260
Merge pull request #2875 from jlowin/fix-task-capabilities-main
Fix task capabilities location (issue #2870)
2026-01-14 09:40:07 -05:00
Chris Guidry
91df79d43c Fix run() call indentation to be inside stdio_server context
The run() call was outside the async with stdio_server() block, meaning
the streams would be closed before being used.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 09:33:59 -05:00
Jeremiah Lowin
4028b648bc Add type ignores for extra_data kwargs 2026-01-13 22:54:28 -05:00
Jeremiah Lowin
3c3a62ee7d Fix type: use TasksCallCapability instead of dict 2026-01-13 22:51:58 -05:00
Jeremiah Lowin
b61c76b19b Fix task capabilities location (issue #2870)
Tasks belong in capabilities.tasks (first-class field) per SEP-1686,
not capabilities.experimental.tasks. This fixes VS Code Copilot 1.107+
integration which checks capabilities.tasks?.requests?.tools?.call.

Changes:
- Update get_task_capabilities() to return ServerTasksCapability types
- Override get_capabilities() in LowLevelServer to set tasks field
- Remove experimental_capabilities parameter usage
- Update test to verify correct location

Fixes #2870
2026-01-13 22:26:13 -05:00
Jeremiah Lowin
2d838315f0
Add timeout parameter for tool foreground execution (#2872) 2026-01-13 21:05:34 -05:00
Jeremiah Lowin
7ee3bec727
Add missing 3.0.0 version badges and document tasks extra (#2866) 2026-01-13 12:04:52 -05:00
Jeremiah Lowin
8c471a499d
Run sync tools/resources/prompts in threadpool automatically (#2865) 2026-01-13 11:03:05 -05:00
Jeremiah Lowin
2b6a0faf1c
Add loq file size limits and clean up type ignores (#2859) 2026-01-13 07:29:12 -05:00
Jeremiah Lowin
84a43da289
Fix decorator error messages to link to correct doc pages (#2858) 2026-01-12 22:22:28 -05:00
Jeremiah Lowin
07d89c4038
Add transform system for modifying components in provider chains (#2836)
* Consolidate tool transformation logic into TransformingProvider

Tool transformations were previously scattered across LocalProvider,
ProxyProvider, and MCPConfig. This consolidates all transformation
logic into TransformingProvider via with_transforms(tool_transforms={...}).

- Add tool_transforms parameter to TransformingProvider
- Add tool_transforms to Provider.with_transforms()
- Remove transformation storage from LocalProvider and ProxyProvider
- Remove add_tool_transformation() and remove_tool_transformation() from FastMCP
- Add tool_transforms parameter to factory methods (from_openapi, from_fastapi, create_proxy)
- Update tests to use new patterns

* Fix: reject tool lookups by pre-transform name

* Add collision validation for tool_transforms and fix docstring examples

- Validate duplicate target names in tool_transforms raise ValueError
- Fix docstring examples to use arguments/ArgTransformConfig (not args/ArgTransform)
- Add test for collision validation

* Add server-level tool transform APIs and fix task registration

- Add AggregateProvider to present multiple providers as one
- Add _get_root_provider() to apply server-level transforms uniformly
- Fix _docket_lifespan to use root provider (ensures renamed tools
  register with correct keys for background execution)
- Add tool_transforms kwarg to __init__ (non-deprecated)
- Add add_tool_transform(), remove_tool_transform(), tool_transforms property
- Deprecate old API names (tool_transformations, add_tool_transformation, etc.)
- Update tests to use new API

* Add graceful degradation for provider errors in AggregateProvider

* Match original behavior: parallel queries with DEBUG logging

* Refactor transforms to middleware-style call_next pattern

Replaces the ad-hoc transformation system with a unified Transform
abstraction using the same call_next pattern as server middleware.

Key changes:
- New src/fastmcp/server/transforms/ module with Transform base class
- Namespace, ToolTransform, Visibility all implement the same interface
- Transforms compose via functools.partial chain building
- Visibility is now just the first transform in provider._transforms
- Server-level transforms apply after provider aggregation
- Task registration now applies full transform chain

Removes TransformingProvider, _BoundTransform, ComponentSource protocol.
User-facing API unchanged: mount(), add_transform(), enable/disable all
work as before.

* Add comprehensive transforms and visibility documentation

New docs/servers/providers/transforms.mdx covering:
- Mental model for middleware-style transform pattern
- Built-in transforms (Namespace, ToolTransform)
- Server vs provider-level transforms and ordering
- Tool modification (immediate vs deferred)
- Custom transform creation

New docs/servers/visibility.mdx covering:
- Enable/disable API for runtime visibility control
- Keys and tags for targeting components
- Allowlist mode with only=True
- Server vs provider visibility layering

Updates existing docs to reference new pages and simplifies
redundant content. Visibility is documented as a user feature,
not as an implementation detail.

* Restructure transforms docs and delete tool-transformation pattern

* Cleanup: simplify get_tasks and remove unused Provider.get_component

* Update loq

* Update loq limits and add loq note to AGENTS.md

* Deprecate add_tool_transformation and tool_transformations param

* Address PR review feedback: remove redundant imports, fix path reference

* Add missing imports to code examples in v3-features.mdx
2026-01-12 22:11:16 -05:00
Jeremiah Lowin
1b723f302d
Decorators return functions instead of component objects (#2856) 2026-01-12 21:58:07 -05:00
Jeremiah Lowin
766641a1a5
Add authorization checks to components and servers (#2855) 2026-01-12 19:56:52 -05:00
Jeremiah Lowin
ebf6e3dd53
Add release notes for v2.14.2 and v2.14.3 (#2852) 2026-01-12 16:52:47 -05:00
Jeremiah Lowin
bf3df4dcd5
Add transport property to Context (#2850) 2026-01-12 15:33:59 -05:00
Jeremiah Lowin
9b18db2379
Fix HTTP transport timeout defaulting to 5 seconds (#2849) 2026-01-12 14:47:21 -05:00
Jeremiah Lowin
18c71f9f34
Add loq pre-commit hook for file size enforcement (#2847) 2026-01-12 11:37:45 -05:00
Jeremiah Lowin
16cd343ec5
Add agent skills for testing and code review (#2846) 2026-01-12 11:24:39 -05:00
Chris Guidry
4ae0cc205c
Merge pull request #2844 from jlowin/fix-asgi-contextvar-propagation
Fix ContextVar propagation for ASGI-mounted servers with tasks
2026-01-12 10:38:31 -05:00
Chris Guidry
923ff5ecc8 Fix ContextVar propagation for ASGI-mounted servers with tasks
Fixes background tasks failing with "Background tasks require a running
FastMCP server context" when FastMCP is mounted to another ASGI app
(FastAPI, Starlette) or deployed to serverless environments (Lambda).

Root cause: ContextVars set during lifespan don't propagate to request
handlers in ASGI environments because they run in sibling async contexts.

Fix: Context.__aenter__ now sets _current_docket and _current_worker from
server instance attributes at request time, ensuring they're available
regardless of async context hierarchy.

Changes:
- server.py: Store self._worker on server instance (self._docket was already stored)
- context.py: Set docket/worker ContextVars from server instance in __aenter__

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 10:31:58 -05:00
Jeremiah Lowin
797297a22d
Add CLI update notifications (#2840) 2026-01-10 18:27:03 -05:00
Jeremiah Lowin
8e1fd1d700
Make pydocket optional and unify DI systems (#2835) 2026-01-10 16:23:32 -05:00
Jeremiah Lowin
4765f5725a
Add PingMiddleware for keepalive connections (#2838) 2026-01-10 16:12:35 -05:00
Jeremiah Lowin
daa2dace2f
Add standalone decorators and eliminate fastmcp.fs module (#2832) 2026-01-10 12:16:35 -05:00
marvin-context-protocol[bot]
24d500d384
chore: Update SDK documentation (#2761)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2026-01-10 11:49:29 -05:00
Jeremiah Lowin
c9a9df17f4
Add FastMCP 4.0 preview notice to installation guide (#2831)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-10 11:41:44 -05:00
Jeremiah Lowin
3e46b8cb4a
Add docs-broken-links command and fix docstring markdown parsing (#2830) 2026-01-10 11:41:23 -05:00
Jeremiah Lowin
3163e61ee4
Replace FastMCP.as_proxy() with create_proxy() function (#2829) 2026-01-10 11:39:06 -05:00
Jeremiah Lowin
d8351993f7
Add composable lifespans (#2828) 2026-01-10 11:16:50 -05:00
Jeremiah Lowin
401b315786
Fix timeout not propagating to proxy clients in multi-server MCPConfig (#2809) 2026-01-09 20:01:16 -05:00
Jeremiah Lowin
b8d6f89e19
Remove deprecated WSTransport (#2826) 2026-01-09 19:42:35 -05:00
Jeremiah Lowin
87f2edd9c0
Add FileSystemProvider for filesystem-based component discovery (#2823) 2026-01-09 16:31:29 -05:00
Jeremiah Lowin
43811cc16b
Fix redirect URI validation docs to match implementation (#2824) 2026-01-09 12:36:34 -05:00
Jeremiah Lowin
dbd800acf8
Add v3.0 feature tracking document (#2822) 2026-01-09 09:34:53 -05:00
Jeremiah Lowin
42dcb8918d
Add --reload flag for auto-restart on file changes (#2816) 2026-01-09 08:44:27 -05:00
nate nowack
aea8f3b0dc
fix smart_home example: HueAttributes schema and deprecated prefix (#2818)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 08:44:03 -05:00
Jeremiah Lowin
8dfa51329a
Dereference $ref in tool schemas for MCP client compatibility (#2814) 2026-01-08 21:16:01 -05:00
Jeremiah Lowin
b1f9c77dd9
Fix Claude Code installation lock contention in CI workflows (#2815) 2026-01-08 20:43:22 -05:00
Chris Guidry
08caf77ef6
Prefix Redis keys with docket name for ACL isolation (#2811)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 19:04:33 -05:00
Jeremiah Lowin
627c6cdad4
Dereference $ref in tool schemas for MCP client compatibility (#2808) 2026-01-07 18:30:08 -05:00
Jeremiah Lowin
a117316ba1
Fix keep_alive passthrough in StdioMCPServer.to_transport() (#2791) 2026-01-07 16:37:19 -05:00
Jeremiah Lowin
da965db5cb
Fix unawaited coroutine warning and treat as test error (#2806) 2026-01-07 14:29:56 -05:00
Jeremiah Lowin
befaad8a08
Configure ty to fail on warnings (#2804) 2026-01-07 11:17:49 -05:00
Jeremiah Lowin
4dca48e980
Turn off negative CLI flags by default (#2801) 2026-01-07 09:50:57 -05:00
Jeremiah Lowin
517a860f53
Fix client hanging on HTTP 4xx/5xx errors (#2803) 2026-01-07 09:50:41 -05:00
Jeremiah Lowin
10fb217f73
Return 401 for invalid_grant token errors per MCP spec (#2800) 2026-01-06 17:51:33 -05:00
Jeremiah Lowin
151b0ae46f
Use consistent refresh_ttl for JTI mapping store (#2799) 2026-01-06 17:46:39 -05:00
Jeremiah Lowin
945dac57e4
Add debug logging for OAuth token expiry diagnostics (#2797) 2026-01-06 17:18:39 -05:00
Jeremiah Lowin
5a95050762
Fix OAuth token storage TTL calculation (#2796) 2026-01-06 16:55:33 -05:00
Adam Azzam
38ec476f2e
Merge pull request #2786 from jlowin/conference
add link to py ai conf in banner
2025-12-30 20:04:13 -06:00
Adam Azzam
cffde77560 Update docs.json 2025-12-30 20:37:24 -05:00
Adam Azzam
7173e33b03 Update docs.json 2025-12-30 20:18:22 -05:00
Jeremiah Lowin
ad50f73bdc
Lazy import DiskStore to avoid sqlite3 dependency on import (#2784) 2025-12-30 19:20:21 -05:00
Taisei Mima
df3d4e19ec
Fix base_url fallback when url is not set (#2776) 2025-12-29 18:43:51 -05:00
Bryan Thompson
30a239637b
Add MCP tool annotations to smart_home example (#2777)
Co-authored-by: triepod-ai <noreply@github.com>
2025-12-29 18:40:51 -05:00
Shea Parkes
6ce967643e
Enhance documentation on tool transformation (#2781) 2025-12-29 14:30:34 -05:00
Jeremiah Lowin
9c83482f4c
Make FASTMCP_SHOW_SERVER_BANNER apply to all server startup methods (#2771) 2025-12-29 08:29:15 -05:00
Jeremiah Lowin
3d485f0c51
Fix titled enum elicitation schema to comply with MCP spec (#2773) 2025-12-29 08:28:36 -05:00
Danglewood
ef84f84d1e
[BugFix] Fix openapi_version Check So 3.1 Is Included (#2768) 2025-12-27 23:07:19 -05:00
Jeremiah Lowin
c5a61a7e19
Update CLI banner with FastMCP 3.0 notice (#2766) 2025-12-26 22:12:58 -05:00
Jeremiah Lowin
7cb00c9686
Fix OAuth Proxy resource parameter validation (#2764) 2025-12-26 21:23:31 -05:00
Jeremiah Lowin
2e8adafdf5
Add regression tests for caching with mounted server prefixes (#2762) 2025-12-26 16:33:03 -05:00
marvin-context-protocol[bot]
74eb8a0645
chore: Update SDK documentation (#2717)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2025-12-26 16:15:08 -05:00
Eloi Zalczer
fb11282e9b
Feature/supabase custom auth route (#2632)
Co-authored-by: Eloi Zalczer <eloi@entropia.io>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-12-26 16:14:56 -05:00
Jeremiah Lowin
a3123176b3
Deprecate tool_serializer parameter (#2753) 2025-12-26 16:03:04 -05:00
Jeremiah Lowin
e454294ea8
Add tests for OAuth generator cleanup and use aclosing (#2759) 2025-12-26 16:01:27 -05:00
Jeremiah Lowin
5e0e6bbc49
fix: set pytest-asyncio default fixture loop scope to function (#2758) 2025-12-26 15:56:30 -05:00
Jeremiah Lowin
4519ef734d
Fix rate limit detection during teardown phase (#2757) 2025-12-26 15:50:11 -05:00
Luke
650b103dbf
fix: Client OAuth async_auth_flow() method causing MCP-SDK self.context.lock error. (#2644)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-12-26 15:41:41 -05:00
Jeremiah Lowin
568300e8b5
Remove automatic environment variable loading from auth providers (#2752) 2025-12-26 15:37:59 -05:00
Jeremiah Lowin
b796f6e14a
Parallelize provider access when executing components (#2744) 2025-12-26 15:37:27 -05:00
Jeremiah Lowin
7d0b49bfb4
Remove unused include_tags/exclude_tags settings (#2756) 2025-12-26 15:04:41 -05:00
Jeremiah Lowin
7b0de84ba7
Add task_meta to prompts and centralize fn_key enrichment (#2751) 2025-12-26 15:04:25 -05:00
Jeremiah Lowin
fc19f1f8de
Add task_meta parameter to read_resource() for explicit task control (#2750) 2025-12-26 12:59:01 -05:00
Jeremiah Lowin
bf1c222e20
Add explicit task_meta parameter to FastMCP.call_tool() (#2749) 2025-12-26 11:38:35 -05:00
Jeremiah Lowin
5f1aa274cc
Convert provider tests to use direct server calls (#2748) 2025-12-26 08:42:47 -05:00
Jeremiah Lowin
a39ab0734e
Update classes to inherit from FastMCPBaseModel instead of BaseModel (#2739) 2025-12-26 08:33:23 -05:00
Jeremiah Lowin
5a6f71503b
Align prompt handler with resource pattern (#2740) 2025-12-25 22:47:50 -05:00
Jeremiah Lowin
70fac26f9f
Fix prompt return type documentation (#2741) 2025-12-25 22:34:11 -05:00
Jeremiah Lowin
1ed1032ca5
Fix Proxy provider to return all resource contents (#2742) 2025-12-25 22:33:45 -05:00
Jeremiah Lowin
61ebc64730
Introduce Message and PromptResult as canonical prompt types (#2738) 2025-12-25 22:16:26 -05:00
Jeremiah Lowin
7da06025e2
Bump ty to 0.0.7+ (#2737) 2025-12-25 21:22:24 -05:00
Jeremiah Lowin
648684d2bb
Introduce ResourceResult as canonical resource return type (#2734) 2025-12-25 21:21:55 -05:00
Jeremiah Lowin
04cf7bc458
Make ToolResult a BaseModel for serialization support (#2736) 2025-12-25 21:06:37 -05:00
Jeremiah Lowin
4de06d7f73
Fix documentation to use 'meta' instead of '_meta' for MCP spec field (#2735) 2025-12-25 21:05:11 -05:00
Jeremiah Lowin
9f5a177126
Consistent decorator-based MCP handler registration (#2732) 2025-12-25 17:48:54 -05:00
Jeremiah Lowin
9f72096c67
Parallelize list_* calls in Provider.get_tasks() (#2731) 2025-12-25 12:16:44 -05:00
Jeremiah Lowin
9d8a608c37
Add documentation check to required PR workflow (#2730) 2025-12-25 10:52:37 -05:00
Jeremiah Lowin
e7128b0295
Fix Provider.get_tasks() to include custom component subclasses (#2729) 2025-12-25 10:49:48 -05:00
Jeremiah Lowin
b05801d66f
Consolidate execution method chains into single public API (#2728) 2025-12-25 10:38:12 -05:00
Jeremiah Lowin
1d17334803
Reorganize docs around provider architecture (#2723) 2025-12-25 09:00:20 -05:00
Jeremiah Lowin
1abd986c80
Revert "Enable parallel tests on Windows (#2715)" (#2726) 2025-12-25 08:54:05 -05:00
lif
8d80299f82
Fix: resolve root-level $ref in outputSchema for MCP spec compliance (#2720) 2025-12-25 08:42:29 -05:00
Jeremiah Lowin
46aeb6b197
Add changelog entries for v2.13.1 through v2.14.1 (#2725) 2025-12-25 08:23:49 -05:00
Jeremiah Lowin
125f79ff39
Consolidate get_* and _list_* methods into single API (#2719) 2025-12-24 22:26:52 -05:00
Jeremiah Lowin
3e67f9fd23
Parallelize provider operations (#2716) 2025-12-24 20:54:08 -05:00
Jeremiah Lowin
dab72007a9
Enable parallel tests on Windows (#2715) 2025-12-24 16:45:21 -05:00
Jeremiah Lowin
e821446044
Skip test_multi_client on Windows (#2714) 2025-12-24 16:37:35 -05:00
marvin-context-protocol[bot]
9d5ffa86b3
chore: Update SDK documentation (#2604)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2025-12-24 16:21:31 -05:00
Jeremiah Lowin
ae3c2abbf6
Consolidate notification system with unified API (#2710) 2025-12-24 16:05:08 -05:00
Jeremiah Lowin
66be7c203b
Add v3 breaking changes notice to README (#2712) 2025-12-24 15:57:16 -05:00
Jeremiah Lowin
dccd25af4f
Skip parallel MCP config test on Windows (#2711) 2025-12-24 15:55:50 -05:00
Jeremiah Lowin
7a1796fbdf
Add VisibilityFilter for hierarchical enable/disable (#2708) 2025-12-24 14:43:41 -05:00
A Ashif
f2233baa87
docs(server): add http to transport options in run method docstring (#2707) 2025-12-24 13:51:26 -05:00
Jeremiah Lowin
aef1ccc026
Add type-prefixed keys for globally unique component identification (#2704) 2025-12-24 08:39:43 -05:00
Mathew Goldsborough
5c49f7a919
Fix Prefect website URL in docs footer (#2701)
Co-authored-by: Mathew Goldsborough <1759329+mgoldsborough@users.noreply.github.com>
2025-12-24 08:26:11 -05:00
Jeremiah Lowin
f36b147f60
Unify component storage in LocalProvider (#2680) 2025-12-23 19:24:09 -05:00
Jeremiah Lowin
9147518c01
Fix uvicorn 0.39+ test timeouts and FastMCPError propagation (#2699) 2025-12-23 19:02:47 -05:00
Chris Guidry
fec24a9eea
Merge pull request #2683 from jlowin/2679-task-cancel-fix
Bump pydocket to 0.16.3 for task cancellation support
2025-12-23 18:45:36 -05:00
Chris Guidry
18538e872b Bump pydocket to 0.16.3 to fix worker cleanup race condition
pydocket 0.16.3 fixes a race condition in `_worker_loop` where cancellation
arriving between `_worker_done.clear()` and the try block would cause
`_worker_done.set()` to never run, blocking `Worker.__aexit__` forever.

Also fixes:
- Simplified `_docket_lifespan` cleanup (timeout wrapper no longer needed)
- Fixed `nested_server` test fixture to use graceful uvicorn shutdown
- Fixed uv transport tests to use local fastmcp in dev mode

Closes #2679

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 18:40:07 -05:00
Chris Guidry
7aae52d18d Bump pydocket to 0.16.2 for task cancellation support
Adds a test that verifies task cancellation actually interrupts running
coroutines (they receive CancelledError) rather than just marking the task
as cancelled in Redis while the coroutine continues to completion.

This requires pydocket >= 0.16.2 which added best-effort cancellation via
Redis pub/sub signaling to workers.

Closes #2679

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 14:43:09 -05:00
Jeremiah Lowin
7dcccef82b
Remove execution methods from Provider base class (#2681) 2025-12-23 09:55:14 -05:00
Jeremiah Lowin
4177d8358d
Fix ty 0.0.5 type errors (#2676) 2025-12-22 19:47:20 -05:00
Jeremiah Lowin
bca310cdde
Use ergonomic provider initialization pattern (#2675)
Updates examples and convenience methods to pass providers directly to FastMCP's __init__ instead of calling add_provider() after instantiation.
2025-12-22 14:49:39 -05:00
Rafi Olaverria
820b69aa2e
fix: check the cause of the tool error (#2674) 2025-12-22 11:45:17 -05:00
Jeremiah Lowin
96c7a74b27
Move OpenAPI to providers/openapi submodule (#2672)
* Move OpenAPI to providers/openapi/ submodule

Migrates the OpenAPI implementation to the provider pattern:
- Creates src/fastmcp/server/providers/openapi/ submodule with provider.py,
  components.py, and routing.py
- Converts server/openapi/ to deprecated re-export stubs
- FastMCPOpenAPI remains as deprecated wrapper that uses OpenAPIProvider
- Updates experimental/server/openapi to import from new canonical location

* Update tests and FastMCP methods to use OpenAPIProvider

- Update FastMCP.from_openapi() and from_fastapi() to use OpenAPIProvider
  directly, returning FastMCP instead of deprecated FastMCPOpenAPI
- Move tests from tests/server/openapi/ to tests/server/providers/openapi/
- Update all test imports to use new canonical location
- Add tests/deprecated/openapi/ for backward compatibility testing
- Update tests/client/test_openapi.py to use new imports

* Fix deprecation warning tests and address CodeRabbit feedback

- Use importlib.reload() to ensure deprecation warnings fire in tests
- Change logger.error to logger.exception for better stack traces
2025-12-22 11:13:33 -05:00
Jeremiah Lowin
748d25eede
Refactor FastMCPProxy into ProxyProvider (#2669)
* Refactor FastMCPProxy into ProxyProvider

Move proxy functionality from custom manager classes to the Provider pattern:

- Create ProxyProvider that implements the Provider interface
- Move all proxy code to src/fastmcp/server/providers/proxy.py
- Keep FastMCPProxy as a convenience wrapper using ProxyProvider
- Add deprecation warning when importing from old location
- Convert handler classmethods to module-level functions
- Remove redundant get_* methods (base class defaults work)

* Remove unused Components class, simplify TaskComponents()
2025-12-21 21:30:03 -05:00
Jeremiah Lowin
3afdb3f7b3
Refactor task module: rename protocol.py to requests.py and reduce redundancy (#2667)
Renamed protocol.py to requests.py for clarity - it handles MCP task
request endpoints (tasks/get, tasks/result, tasks/cancel, tasks/list).

Consolidated constants to config.py as single source of truth:
- DEFAULT_POLL_INTERVAL_MS now derived from DEFAULT_POLL_INTERVAL
- TaskConfig uses constant instead of hardcoded timedelta(seconds=5)

Extracted _lookup_task_execution() helper to eliminate ~50 lines of
duplicated Redis lookup code. Uses redis.mget() for single round-trip
instead of 3 separate calls (performance improvement).
2025-12-21 17:21:22 -05:00
Jeremiah Lowin
7ff23a06f4
Add poll_interval to TaskConfig (#2666)
* Add poll_interval to TaskConfig

Allow users to configure polling interval per component via
TaskConfig(poll_interval=timedelta(...)). Default is 5 seconds.

* Update snapshots for poll_interval field

* Add version badge to poll_interval docs

* Add defensive handling for Redis data and align default poll intervals
2025-12-21 16:59:43 -05:00
Jeremiah Lowin
049bd22827
Replace type: ignore[attr-defined] with isinstance assertions in tests (#2665)
* Replace type: ignore[attr-defined] with isinstance assertions in tests

* Fix isinstance assertions in failing tests

- Fix enum test to check for ResponseEnum instead of str
- Fix binary resource test to check for BlobResourceContents instead of TextResourceContents
- Fix Root type tests to check attributes directly instead of isinstance checks

* Fix type errors without using type: ignore

- Remove execution methods from TransformingProvider (only handles transformations)
- Add execution methods to base Provider class with default implementations
- Fix type narrowing in tests using cast() instead of type: ignore
- Fix PromptResult type handling in prompt render tests
- Fix type narrowing in middleware test for arguments and structured_content
2025-12-21 16:37:20 -05:00
Jeremiah Lowin
d6654a2379
Add supports_tasks() method to replace string mode checks (#2664)
* Add supports_tasks() method to replace string mode checks

Consolidates task config mode checks into a readable method on TaskConfig.
Instead of `task_config.mode == "forbidden"` or `task_config.mode != "forbidden"`,
code now uses `task_config.supports_tasks()` for clearer intent.

Updated 20 instances across the codebase and added type assertions in tests
to resolve type checker warnings.

* Update test to match new error message
2025-12-21 15:41:46 -05:00
Jeremiah Lowin
19fdac7b02
Refactor provider execution: components own their execution (#2663)
* Add test_custom_subclass_tasks.py

* Refactor provider execution: delegate to middleware via wrapper components

- Remove execution methods (call_tool, read_resource, etc.) from Provider base
- Add FastMCPProvider* wrapper classes that delegate to child server middleware
- Move task routing to Tool._run() using contextvars (_task_metadata, _tool_call_key)
- Add convert_to_tool_result(result, output_schema) utility for Docket results
- Add convert_to_prompt_result() utility for prompt task results
- Pass namespaced key via add_to_docket(name=) for mounted tool lookup

* Standardize add_to_docket() with fn_key/task_key parameters

All components now use explicit fn_key (function lookup) and task_key
(result storage) parameters instead of relying on implicit key handling.
This fixes mounted component task execution where the MCP-visible key
differs from the Docket-registered function name.

* Add middleware chain tests for three-level mount hierarchy

Tests verify middleware runs at parent, child, and grandchild levels
for tools, resources, prompts, and resource templates.

* WIP: Provider refactor - unified submit_to_docket, template _read() in progress

Work in progress on refactoring execution to use component _read()/_run()/_render() methods.
Template background tasks not yet working - needs fix for Docket key lookup.

* Fix conversion functions to take full component for attribute access

Pass Tool/Prompt/Resource/Template to conversion functions instead of
individual attributes, ensuring access to serializer, output_schema,
mime_type, etc. Also fixes mixed-content output schema validation.

* Refactor: unified convert_result() methods and check_background_task helper

- Add convert_result() instance methods to all component types (Tool, Prompt, Resource, ResourceTemplate)
- Extract duplicated task routing logic into check_background_task() helper
- Fix type annotations on FastMCPProviderResource.read() and FastMCPProviderPrompt.render()
- Update protocol.py to use component.convert_result() uniformly

* Update tests to use namespace= instead of deprecated prefix= parameter
2025-12-21 15:03:24 -05:00
Jeremiah Lowin
fc8ba728a8
Use CreateTaskResult for background task creation (#2660)
* Use CreateTaskResult for background task creation

Move result conversion logic to components (convert_result methods) and
return proper CreateTaskResult SDK type from task handlers. Consolidates
MCP protocol handler overrides into server.py with documentation.

* Address CodeRabbit nitpicks

- Add type annotation for resource parameter in handle_resource_as_task
- Move RootModel import to module level in client.py

* Document intentionally unused task_meta parameters

Prefix with underscore to suppress lint warnings. Client TTL will be
configurable via TaskConfig in the future; keeping parameter for API stability.
2025-12-20 20:19:11 -05:00
Jeremiah Lowin
9f486e5e0c
Add register_with_docket() and add_to_docket() to all components (#2657)
Refactors docket/background task support to be encapsulated within each
component rather than requiring external coordination:

- Move task_config to FastMCPComponent base class (default: forbidden)
- Add register_with_docket(docket) method that components use to register
  themselves, checking task_config internally
- Add add_to_docket() method that handles component-specific calling
  conventions (splatted kwargs vs positional dict)
- Simplify server registration to just call component.register_with_docket()
- Update task handlers to use component.add_to_docket()

This enables custom Tool/Resource/Prompt subclasses to support background
tasks by setting task_config and optionally overriding the docket methods.
2025-12-20 07:47:25 -05:00
ivan
790ea92eb5
Undocumented McpError exceptions (#2656)
Minor documentation fix addressing missing MCPError raise event. Given there are no documentation guidelines I proceeded to modify docstrings of methods that both
- MAY raise `McpError`
- have a docstring that contains a `Raises` section
2025-12-19 21:41:16 -05:00
Jeremiah Lowin
3a6b1ba894
Refactor MountedProvider into FastMCPProvider + TransformingProvider (#2653)
* Refactor MountedProvider into FastMCPProvider + TransformingProvider

Split the monolithic MountedProvider into two focused components:
- FastMCPProvider: wraps a FastMCP server as a provider
- TransformingProvider: applies namespace/rename transformations to any provider

Add with_transforms() method to Provider base class for fluent API.
Rename mount() prefix parameter to namespace (deprecate prefix).

* Reuse compiled URI_PATTERN in deprecated import_server
2025-12-19 11:42:59 -05:00
Jeremiah Lowin
caeaa86394
Simplify .key as computed property (#2648)
* Simplify .key as computed property

Keep .key as the standard lookup interface for all components but
implement it as a computed property instead of a stored field.

- Remove _key private attribute and custom model_copy(key=...)
- .key returns .name for tools/prompts, str(.uri) for resources,
  .uri_template for templates
- Use .key universally for component lookups in managers
- MountedProvider: prefix URIs only for resources/templates, not names
- Docket registration: tools/prompts use .key, resources use .name
  (matches fn.__name__ for function lookup)

* Simplify .key as computed property

Keep .key as the standard lookup interface for all components but
implement it as a computed property instead of a stored field.

- Remove _key private attribute and custom model_copy(key=...)
- .key returns .name for tools/prompts, str(.uri) for resources,
  .uri_template for templates
- Use .key universally for component lookups and Docket registration
- MountedProvider: prefix URIs only for resources/templates, not names
- Add _backend_* fields to proxy classes to preserve original identifiers
  for backend calls when prefixed via import_server

* Standardize .key as computed property

- .key is now a read-only computed property:
  - Tools/Prompts: returns .name
  - Resources: returns str(.uri)
  - Templates: returns .uri_template
- Prefixing uses model_copy(update={...}) to change underlying field
- Resource/template names are NOT prefixed, only URIs
- Move import_server tests to tests/deprecated/
2025-12-18 14:48:47 -05:00
Chris Guidry
8d758baeda
Merge pull request #2645 from jlowin/2642-task-custom-name
Fix task execution for tools with custom names
2025-12-18 11:45:07 -05:00
Chris Guidry
a5064cc007 Add end-to-end tests for custom-named resources and templates
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 11:37:06 -05:00
Chris Guidry
22e135fa5a Fix task execution for tools with custom names
When a tool was registered with a custom name different from its function
name, task execution would fail because Docket registered the function by
its `__name__` but the handler looked it up by the tool's configured name.

This switches to using Docket's new `names=` parameter (pydocket 0.16.0)
to register functions with their proper lookup keys, and removes the
`_create_named_fn_wrapper` hack that was used for mounted servers.

Closes #2642

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 11:36:34 -05:00
Chris Guidry
097fbae1fa
Merge pull request #2646 from jlowin/2633-toolerror-in-depends
Let FastMCPError propagate from dependencies
2025-12-18 11:29:20 -05:00
Chris Guidry
63bc9cb921 Let FastMCPError propagate from dependencies
When a dependency raises ToolError or other FastMCPError subclasses, they
were getting wrapped in RuntimeError with a generic "Failed to resolve
dependency" message. This made it hard to use ToolError for validation
in dependencies.

Now FastMCPError subclasses propagate unchanged, matching the pattern
used elsewhere in the codebase.

Closes #2633

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 10:59:24 -05:00
Jeremiah Lowin
ede8ff6703
Convert mounted servers to MountedProvider (#2635)
* Simplify Provider interface and consolidate docket registration

- Remove get_http_routes from Provider (unused)
- Remove ProviderLifespanConfig, _base_lifespan, _register_tasks
- Remove supports_tasks flag from Provider.__init__
- Consolidate all docket registration in server._docket_lifespan()
- Simplify lifespan() to take no parameters
- Move MountedProvider to separate module

* Fix control flow in ComponentService resource methods

* Move providers to server/providers

* Ensure MountedProvider get_* methods go through middleware

* Fix get_resource to only return concrete resources

Reverts template-checking in get_resource that broke task execution.
Tasks need access to the original template, not instantiated resources.

* Move prefix utilities into mounted.py, deprecate import_server

- Add resource prefix functions (add/remove/has_resource_prefix) to mounted.py
- Deprecate import_server with warning to use mount() instead
- Add tool_names uniqueness validation in MountedProvider

* Fix provider iteration order and remove dead _is_mounted flag

- Remove unused _is_mounted flag (MountedProvider.lifespan() calls _lifespan
  not _lifespan_manager, so the flag was never checked)
- Fix provider iteration: change reversed() to forward order in execution
  methods (_call_tool, _read_resource_middleware, _get_prompt_content_middleware)
  to match documented "first non-None wins" semantics
- Fix ComponentService to handle prefix-less mounted servers using
  _strip_tool_prefix()/_strip_resource_prefix() methods
- Update conflict resolution tests to expect first-registered provider wins
- Add regression tests for Docket behavior and prefix-less ComponentService

* Add TaskComponents type and exception handling for provider task registration

- Create TaskComponents dataclass with FunctionTool/FunctionResource/etc. types
  for proper typing of get_tasks() return value
- Add try/except wrapper around provider.get_tasks() in _docket_lifespan for
  consistent error handling (warn + continue or raise based on settings)
- Remove type: ignore comments from server.py task registration loop
2025-12-17 22:21:51 -05:00
dependabot[bot]
0186121f4b
Bump peter-evans/create-pull-request from 7 to 8 (#2623)
Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7 to 8.
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v7...v8)

---
updated-dependencies:
- dependency-name: peter-evans/create-pull-request
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-17 21:59:18 -05:00
Jeremiah Lowin
0e5a8fed6c
feat: Provider abstraction for dynamic MCP components (#2622)
* feat: add Provider abstraction for dynamic components

Introduces a `Provider` base class that allows dynamic provision of
tools, resources, and prompts at runtime. Providers are queried after
static components, enabling database-backed tools, external integrations,
and other dynamic component sources.

```python
from fastmcp import FastMCP, Provider

class DatabaseProvider(Provider):
    async def list_tools(self, context):
        return await db.fetch_tools()

mcp = FastMCP("Server", providers=[DatabaseProvider()])
```

* Add execution methods to Provider interface

Enable providers to customize how components are executed, not just listed.
This unlocks MountedProvider (future) invoking wrapped server middleware.

- call_tool, read_resource, read_resource_template, render_prompt
- Server uses provider execution methods after filter checks
- Default implementations delegate to component methods

* Apply unified error handling to provider execution

- Document error semantics: list_* gracefully degrades, execution propagates
- Wrap provider call_tool/read_resource/render_prompt with masking logic
- ToolError/ResourceError/PromptError pass through, others wrapped
2025-12-16 22:05:43 -05:00
Jeremiah Lowin
8abd84bf1e
Revert "Revert "Refactor prompt behavior and add meta support (#2600)" (#2608)" (#2610)
This reverts commit f9e29cf58e.
2025-12-14 21:52:20 -05:00
Jeremiah Lowin
5f3bb05f99
Revert "Revert "Refactor resource behavior and add meta support (#2598)" (#2609)" (#2611)
This reverts commit 1efc4bc3ff.
2025-12-14 21:36:40 -05:00
Jeremiah Lowin
2f561ec98d
Deprecate exclude_args in favor of Depends() (#2621)
* Deprecate exclude_args in favor of Depends()

* Add version badge to Hiding Parameters section
2025-12-14 21:24:08 -05:00
Jeremiah Lowin
076ec0c84b
Adopt streamable_http_client API from MCP SDK (#2620)
* Adopt streamable_http_client API from MCP SDK

- Update import to use new streamable_http_client function
- Convert httpx_client_factory to httpx.AsyncClient before passing to new API
- Maintain backward compatibility by continuing to accept factories
- Add deprecation warning for sse_read_timeout parameter

The new API accepts httpx.AsyncClient directly instead of factories.
We continue accepting factories for OAuth compatibility, converting
them to clients at the boundary with the MCP SDK.

* Fix timeout type conversion for streamable_http_client

Convert read_timeout_seconds from timedelta to float before passing
to httpx, matching the pattern used in the SSE transport.

* Enable redirect following in httpx client

* Fix httpx client resource leak
2025-12-14 21:18:01 -05:00
Jeremiah Lowin
b8ae95a4bb
Add tool_names parameter to mount() for name overrides (#2619)
* Add tool_names parameter to mount() for name overrides

* Fix Docket registration to use tool_names overrides
2025-12-14 20:56:42 -05:00
Jeremiah Lowin
577f4d1bdf
Consolidate sampling examples and fix tool_choice bug (#2618)
* Fix tool_choice to always require tools when result_type is set

* Consolidate sampling examples with rich output

* Replace eval() with explicit add/multiply tools
2025-12-14 20:47:00 -05:00
Jeremiah Lowin
c91c43e280
Add AnthropicSamplingHandler (#2617)
* Add AnthropicSamplingHandler

Adds a sampling handler for the Anthropic API at
fastmcp.client.sampling.handlers.anthropic, alongside the existing
OpenAI handler. Includes full support for tool calling.

Install with: pip install fastmcp[anthropic]

* Update default model

* Update sampling docs to cover both OpenAI and Anthropic handlers

* Use AsyncAnthropic, fix falsy value handling, handle tool_choice none

* Propagate isError to Anthropic, join multiple text blocks, fix docs
2025-12-14 16:07:22 -05:00
Jeremiah Lowin
da77cfa73f
Unify SamplingHandler and promote OpenAI handler (#2616)
* Unify SamplingHandler and promote OpenAI handler

Consolidates ServerSamplingHandler and ClientSamplingHandler into a single
SamplingHandler type alias. Moves OpenAISamplingHandler from experimental
to fastmcp.client.sampling.handlers.openai as the canonical location.

Backwards compatibility maintained for imports from experimental.

* Remove unreachable code paths in OpenAI handler

* Fix docstring and use elif for mutually exclusive branches
2025-12-14 15:22:43 -05:00
Jeremiah Lowin
41ec7ee06d
SEP-1577: Sampling with tools (#2551)
* MCP → SDK (vocab change only)

* WIP: Sampling API with SamplingResult[T] and result_type

* SEP-1577: Sampling with tools

- Add tools and result_type parameters to ctx.sample()
- Update OpenAI handler for tool content types
- Client advertises sampling.tools capability by default
- Collect tool results into single message with list content

* Fix tool result content handling in OpenAI handler

* Remove @sampling_tool decorator - pass functions directly to sample()

Functions passed to ctx.sample(tools=[...]) are now auto-converted
via SamplingTool.from_function(). Users can still use that method
directly for custom name/description overrides.

* Remove auto-conversion of MCP tools to sampling tools

Users want MCP tools passed to ctx.sample() to go through the full MCP
machinery (middleware, native responses) rather than being auto-converted
to direct function calls. Now only SamplingTool and plain callables are
accepted - passing a FastMCP Tool raises a clear TypeError.

Also bumps mcp dependency to >=1.24.0 for required sampling features.

* Refactor sampling API: replace sample_iter() with sample_step()

Replace the mutable SampleRun/sample_iter() pattern with a simpler stateless
sample_step() function. sample_step() makes a single LLM call and returns a
SampleStep with the response and history. sample() now loops sample_step()
internally.

Key changes:
- Add sample_step() for fine-grained control over the sampling loop
- Remove SampleRun class and sample_iter() method
- Structured output uses tool description only (no prompt modification)
- execute_tools parameter controls automatic vs manual tool execution

* Address CodeRabbit nitpicks

* Address CodeRabbit review feedback for sampling tools

- Fix temperature=0.0 being dropped due to falsy evaluation
- Add ToolChoice.name support for forcing specific tools
- Replace assert statements with explicit RuntimeError checks
- Add mask_error_details parameter to sample()/sample_step() with ToolError escape hatch
- Fix hasattr patterns with proper isinstance checks
- Document mask_error_details and add OpenAI prerequisites to docs

* Address additional CodeRabbit review feedback

- Catch ValidationError specifically instead of bare Exception
- Update result_type docs to mention dataclasses and basic types
- Raise ValueError for unknown tool_choice modes
- Validate sampling_handler_behavior to catch typos
- Remove ToolChoice.name handling (not part of MCP spec)
- Validate tool_choice string in sample_step()

* Review fixes for sampling tools PR

- Remove internal functions from sampling __init__.py exports
- Remove fragile is_text property, use not is_tool_use instead
- Inline call_client into context.py, remove from run.py
- Fix SamplingMessage docs to use TextContent
- Handle result.text being None in doc examples
- Simplify client sampling docs to recommend OpenAISamplingHandler
- Add sampling_capabilities override documentation
- Raise iteration limit from 50 to 100
- Remove _parse_model_preferences duplication
- Use AsyncOpenAI in OpenAISamplingHandler
- Fix tool_choice docstring

* Fix OpenAI handler tests to use AsyncOpenAI

* Address remaining CodeRabbit review comments

- Fix message ordering in OpenAI handler: tool results now correctly
  follow assistant message with tool_calls
- sample_step() now always includes assistant message in history
- Raise ValueError on JSON parse errors instead of silent {}
- Add has_sampling capability check when behavior is None
- Raise RuntimeError when structured output receives text response
- Wrap primitive result_type schemas in object wrapper
- Fix docs example using invalid SamplingMessage construction
- Add comprehensive client_sampling_test.py example

* Add return type annotation to OpenAISamplingHandler.__init__

* Use explicit 'is not None' check for sampling_capabilities defaulting

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>
2025-12-14 13:51:05 -05:00
Jeremiah Lowin
0cd3690aef
Consolidate session state reset logic and improve cancellation cleanup (#2615) 2025-12-14 09:11:30 -05:00
Shawn Thapa
d26b04f80e
Clean up cancelled connection startup (#2614) 2025-12-14 08:45:05 -05:00
Jeremiah Lowin
08d092be57
Use WindowsSelectorEventLoopPolicy to fix Windows test warnings (#2607)
* fix: skip TextIO log file test on Windows

Avoids PytestUnraisableExceptionWarning caused by ProactorEventLoop
cleanup timing issues with subprocess pipe transports.

* Use WindowsSelectorEventLoopPolicy to fix Windows test warnings

Re-add the SelectorEventLoop fix from bcd2e594 that was inadvertently
removed in cf101c2a. This fixes ProactorEventLoop cleanup warnings
on Windows CI without needing to skip individual tests.
2025-12-14 08:39:44 -05:00
Jeremiah Lowin
ff4e3bb131
Remove legacy _task_capable_initialize() workaround (#2612)
The function set experimental={"tasks": {}} but per the MCP spec:
1. Tasks belong in capabilities.tasks, not capabilities.experimental
2. Clients only need to declare task capabilities if receiving task-augmented
   requests from the server (bi-directional support)

For client→server task requests (tools/call with task=True), only the server
needs to declare task capabilities. The SDK's native session.initialize()
handles this correctly.
2025-12-13 20:29:06 -05:00
Jeremiah Lowin
1efc4bc3ff
Revert "Refactor resource behavior and add meta support (#2598)" (#2609)
This reverts commit 12f2422e18.
2025-12-13 15:02:18 -05:00
Jeremiah Lowin
f9e29cf58e
Revert "Refactor prompt behavior and add meta support (#2600)" (#2608)
This reverts commit edb9d5ab65.
2025-12-13 14:57:50 -05:00
Jeremiah Lowin
edb9d5ab65
Refactor prompt behavior and add meta support (#2600)
* feat: add PromptResult as canonical internal type for prompts

Applies the same pattern as ResourceContent to prompts. PromptResult
wraps messages with description and meta. Public render() can return
either list[PromptMessage] or PromptResult (backwards compatible),
while private _render() always returns PromptResult.

* docs: fix incorrect PromptResult return type in example

* feat: add PromptResult canonical type with meta support

* fix: address PR #2600 review comments

Fixes test failures and code quality issues identified in PR review:

- Update 3 tests in test_server_interactions.py to access PromptResult.messages[0] instead of indexing directly
- Fix ProxyPromptManager to preserve meta field when converting GetPromptResult to PromptResult
- Fix ProxyPrompt.render() to return PromptResult instead of deprecated list[PromptMessage], preventing fastmcp tags from leaking into runtime meta
- Fix mask_error_details initialization to respect explicit False values
- Fix exception re-raising to preserve tracebacks (use bare raise instead of raise e)
- Update testing documentation to use pytest -n auto for parallel execution
2025-12-13 12:08:41 -05:00
Jeremiah Lowin
d73f05d7e2
chore: add Python 3.13 to Ubuntu CI tests (#2606) 2025-12-13 12:03:46 -05:00
Jeremiah Lowin
12f2422e18
Refactor resource behavior and add meta support (#2598)
* feat: make ResourceContent the canonical internal type for resources

Add Resource._read() private method that always returns ResourceContent,
maintaining backwards compatibility for custom resources returning str/bytes
from read(). Includes deprecation warning when str/bytes is returned.

* fix: address review feedback for ResourceContent

- Remove ResourceContent from root exports (import from fastmcp.resources)
- Fix FunctionResource.read() return type to str | bytes | ResourceContent
- Decode base64 blobs in proxy when receiving from remote servers
- Preserve meta in ProxyResource cached content

* fix: add empty result guards in proxy resource reads
2025-12-13 11:45:38 -05:00
Chris Guidry
896eb12374
Merge pull request #2605 from jlowin/bump-pydocket-0.15.5
chore: bump pydocket to >=0.15.5
2025-12-12 17:37:29 -05:00
Chris Guidry
ece38b8d04 chore: bump pydocket to >=0.15.5
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 17:31:08 -05:00
Chris Guidry
be726d743f
Merge pull request #2603 from jlowin/fix-prompt-task-to-mcp
fix: prompt tasks returning mcp.types.PromptMessage now work
2025-12-12 14:11:28 -05:00
Chris Guidry
f2006804d2 fix: prompt tasks returning mcp.types.PromptMessage now work
When a prompt function returned `mcp.types.PromptMessage` objects directly
and was executed as a task, the result serialization failed with
"'PromptMessage' object has no attribute 'to_mcp'".

The task result converter was calling `.to_mcp()` on what it thought was a
FastMCP wrapper type, but the import actually pulls in `mcp.types.PromptMessage`
directly, which is already the final MCP type. Removed the unnecessary
conversion.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-12 12:57:04 -05:00
marvin-context-protocol[bot]
9ddea52d93
chore: Update SDK documentation (#2592)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2025-12-11 21:43:51 -05:00
Jeremiah Lowin
c2ab2c3db4
docs: simplify Docket tip wording (#2599) 2025-12-11 21:08:37 -05:00
Jeremiah Lowin
3d6fd4650c
chore: remove tests/test_examples.py (#2593) 2025-12-10 15:52:07 -05:00
Xing
03b62d2e3d
feat: handle error from the initialize middleware (#2531)
* feat: handle error from the initialize middleware

In some situation, the initialize middleware can check the status of the
server and decide to raise an error.

Example use case: in a FastMCPProxy, an initialization middleware
overrides the on_initialize method and connect to the underlying proxied
client. When client respond with error, I want to pass this error to the
client.

* docs update

* test: use McpError assertions now that exception propagation is fixed

- Update tests to catch McpError specifically instead of generic Exception
- Remove commented-out code in low_level.py

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-12-10 15:51:51 -05:00
Jeremiah Lowin
95e58e87aa
fix: preserve exception propagation through transport cleanup (#2591)
anyio task groups suppress exceptions when cancel_scope.cancel() is
called during cleanup. Capture exceptions before cleanup and re-raise
after task group exits cleanly.

Also preserve McpError type in client _connect() so callers can catch
protocol-level errors specifically.
2025-12-10 15:37:03 -05:00
marvin-context-protocol[bot]
855e01ed21
chore: Update SDK documentation (#2588)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2025-12-09 22:08:03 -05:00
Jeremiah Lowin
d56f55a12a
Add smart fallback for missing access token expiry (#2587)
When upstream OAuth providers don't return expires_in (like GitHub OAuth
Apps), use smart defaults: 1 hour if refresh token available, 1 year if
not. Adds fallback_access_token_expiry_seconds parameter to override.
2025-12-09 21:31:17 -05:00
marvin-context-protocol[bot]
d35b867b10
chore: Update SDK documentation (#2517)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2025-12-09 21:07:20 -05:00
Jeremiah Lowin
080ffa578a
Fix nested server mount routing for 3+ levels deep (#2586)
Tools, resources, and prompts from servers mounted more than 2 levels
deep failed to invoke even though they were correctly listed.

The bug was in the routing methods which used manager methods that only
search locally, not through nested mounted servers. Changed to use
server-level methods that search recursively.

Fixes #2583
2025-12-09 21:06:47 -05:00
Jeremiah Lowin
0bcd69cd98
Remove overly restrictive MIME type validation from Resource (#2585) 2025-12-09 20:40:00 -05:00
Jeremiah Lowin
9b41d16dc0
Remove deprecated mount/import argument order and separator params (#2582) 2025-12-09 13:58:51 -05:00
Dusty Herrman
95fb8b4894
Fix proxy tool result meta attribute forwarding (#2526)
Proxied tool results now properly forward the meta attribute from upstream servers through ProxyToolManager and ProxyTool.

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-12-09 13:58:38 -05:00
Bill Easton
be6b1670d3
[Draft] Add documentation for read-only tool patterns (#2536)
* Add comprehensive documentation for read-only tool patterns

Created new patterns guide explaining readOnlyHint annotation usage,
including practical examples, client-specific behavior, and best
practices for marking tools as read-only.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: William Easton <strawgate@users.noreply.github.com>

* Reorganize read-only tools docs into tools section

Moved read-only tools documentation from standalone patterns page into the tools.mdx file as a "Using Annotation Hints" subsection. Condensed from 218 lines to ~50 lines focusing on practical usage while maintaining essential information about readOnlyHint and other annotations.

Changes:
- Added "Using Annotation Hints" subsection in tools.mdx after MCP Annotations
- Removed docs/patterns/read-only-tools.mdx
- Updated docs.json navigation to remove patterns entry
- Content now positioned as core tool feature rather than advanced pattern

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Bill Easton <strawgate@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: William Easton <strawgate@users.noreply.github.com>
2025-12-09 13:47:29 -05:00
Jeremiah Lowin
7b845a81cf
Remove deprecated from fastmcp.settings import settings (#2581) 2025-12-09 13:39:34 -05:00
Jeremiah Lowin
b20de51512
Remove TaskConfig and client from root exports (#2580) 2025-12-09 12:28:01 -05:00
Mathew Han
54156d6b93
Expose get_session_id callback (#2486)
* [feat] expose get_session_id callback

* [test] add test for session id callback

* [fix] add test for uninitialized case and default to None

* [fix] add in changes based on reviewers
2025-12-09 12:24:11 -05:00
Jeremiah Lowin
c3ffef677b
SEP-1330 enum schema support (#2549)
* SEP-1330 enum schema support for elicitation

* Add version badges for 2.14.0 elicitation features

* Fix Context.elicit() to handle SEP-1330 enum syntaxes

* Guard against empty list in elicit response_type

* Add guards for empty dict/list edge cases in elicit

* Refactor elicit: extract parsing and response handling to elicitation.py
2025-12-09 12:22:30 -05:00
Chris Guidry
9ea57f9ef8
Merge pull request #2578 from jlowin/remove-enable-tasks
Remove enable_tasks setting, enable task protocol by default
2025-12-09 10:16:52 -05:00
Chris Guidry
46965d2d80
Merge branch 'main' into remove-enable-tasks 2025-12-09 10:04:39 -05:00
Chris Guidry
dc00f5c2bb Centralize task capabilities, add component filtering
Addresses code review feedback:
- Extract `get_task_capabilities()` to avoid duplicating the SEP-1686
  capability structure across transports
- Add `_should_enable_component()` check before task routing for tools,
  resources, and prompts to respect enable/tag filtering
- Simplify tasks/__init__.py to avoid circular import issues

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 10:02:38 -05:00
Jeremiah Lowin
a39b3534e4
SEP-1699: Add SSE polling support with EventStore (#2564)
* Add EventStore and SSE polling support (SEP-1699)

* Add close_sse_stream() method to Context

* Add SSE polling documentation

* Fix missing Context import in docs example

* Remove EventStore from root __init__.py, update docs imports

- Removed EventStore import and export from src/fastmcp/__init__.py
- Updated docs to import EventStore from fastmcp.server.event_store
- Resolves merge conflict by not exporting EventStore from root package
2025-12-09 09:55:51 -05:00
Jeremiah Lowin
cf67152616
Update test workflow to conditionally run pytest on Windows. Added logic to handle test execution differently for Windows and other OS environments, ensuring compatibility and performance optimization. (#2579) 2025-12-09 09:50:20 -05:00
Jeremiah Lowin
413a5a1319
Fix OAuth client to preserve full URL path for metadata discovery (#2577) 2025-12-09 09:47:07 -05:00
Chris Guidry
350f723592 Remove enable_tasks setting, enable task protocol by default
The task protocol (SEP-1686) is now always enabled - server always
registers task handlers and advertises task capabilities. Users still
opt into background execution at the server level (tasks=True) or
component level (task=True on tools, prompts, resources).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 09:34:42 -05:00
Chris Guidry
515e8e832c
Merge pull request #2576 from jlowin/tool-execution-field
Add execution field to base Tool class
2025-12-08 17:11:33 -05:00
Chris Guidry
17520ec5d5 Add execution field to base Tool class
The base Tool class now has an optional `execution` field for storing
task execution metadata (SEP-1686). This lets gateways/proxies preserve
execution info when forwarding tools from backends - previously this
metadata was lost because Tool had no way to store it.

FunctionTool continues to derive execution from task_config as before.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-08 17:07:49 -05:00
Chris Guidry
659ec38d01
Merge pull request #2575 from jlowin/multi-mount-docket-isolation
Prefix Docket function names to avoid collisions in multi-mount setups
2025-12-08 16:10:28 -05:00
Chris Guidry
4a6f8730f5 Prefix Docket function names to avoid collisions in multi-mount setups
When multiple servers with task-enabled tools are mounted into a parent,
their functions were all registered with Docket using `fn.__name__`. This
meant two mounted servers each having a function named `add` would both
register under `"add"`, with the second overwriting the first.

Now mounted functions use prefixed names matching their client-facing tool
names (e.g., `c1_add`, `c2_add`). Root server functions still use their
original names with no prefix.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-08 15:12:40 -05:00
Chris Guidry
11ae63807a
Merge pull request #2574 from jlowin/task-proxy-mount-tests
Forbid task execution through proxies, add mount/proxy task tests
2025-12-08 13:46:49 -05:00
Chris Guidry
a335a9dd89 Fix race condition with mounted server task results
When a server is mounted on another, both were creating their own
Docket and Worker instances. With memory:// URLs they share the same
FakeServer queue but have separate result_storage instances. This
caused a race condition where results could be stored in one Docket's
storage but looked up in another's, returning None.

The fix marks mounted servers with `_is_mounted=True` flag so they
skip creating their own Docket/Worker. The parent's Docket handles
all task execution for mounted servers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-08 13:33:57 -05:00
Chris Guidry
54fcdad85f Fix ruff isinstance style (X | Y instead of tuple)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-08 11:48:23 -05:00
Chris Guidry
82301773cf Proxy tasks forbidden, mount tasks working
Simplifies the task support story for proxies and mounts:
- Mounts get full SEP-1686 task support (unchanged)
- Proxies explicitly forbid task execution

The cross-session task forwarding for proxies turned out to be complex
since each client connection creates a new server lifespan with a new
Docket context, and task keys include session_id. Rather than introduce
that complexity, proxies now explicitly refuse task-augmented execution.

Key changes:
- All proxy components (ProxyTool, ProxyPrompt, ProxyResource,
  ProxyTemplate) now have task_config.mode="forbidden"
- Proxy tests verify forbidden behavior (sync execution works,
  task=True returns error/raises McpError)
- Fixed prompt task handler to check hasattr(prompt, "task_config")
  instead of isinstance(prompt, FunctionPrompt) so it applies to
  ProxyPrompt too
- Added test suites for both proxy and mount task behavior

Also includes minor fixes:
- Fixed result.meta_ -> result.meta in ProxyTool.run()
- Fixed client handling of returned_immediately without taskId
- Bumped pydocket>=0.15.2

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-08 11:44:28 -05:00
Chris Guidry
61108a0bba
Merge pull request #2570 from jlowin/add-task-config-modes
Add TaskConfig for SEP-1686 execution modes
2025-12-08 09:41:59 -05:00
Chris Guidry
5d04b3a217
Merge branch 'main' into add-task-config-modes 2025-12-08 09:38:00 -05:00
Jeremiah Lowin
597286a633
Update VersionBadge to use Mintlify native Badge component (#2571)
Replace custom VersionBadge implementation with Mintlify's native Badge component while preserving custom color and border styling via CSS. This leverages Mintlify's built-in sizing, typography, and icon support while maintaining the original visual design.
2025-12-06 21:23:53 -05:00
Jeremiah Lowin
b291abb460 Fix inline snapshots for task_config rename 2025-12-06 21:05:11 -05:00
Jeremiah Lowin
a231ea4c3c Add TaskConfig for SEP-1686 execution modes
Expose the full MCP task execution modes (forbidden/optional/required)
via TaskConfig instead of just boolean task=True/False.
2025-12-06 21:01:11 -05:00
Jeremiah Lowin
8771f290bd
Rewrite background tasks documentation (#2567)
- Lead with concepts instead of code
- Explain MCP background tasks vs general Python concurrency
- Document Docket's Prefect origins and battle-tested infrastructure
- Add sections on graceful degradation and embedded workers
- Fix version badge to 2.14.0
- Link to SEP-1686 spec
2025-12-06 19:28:14 -05:00
Chris Egersdoerfer
6147c27995
add client kwargs to proxy clients and meta to proxy tool calls (#2520)
* add client kwargs to proxy clients and meta to proxy tool calls

* Update src/fastmcp/client/transports.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update src/fastmcp/server/proxy.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Update src/fastmcp/utilities/mcp_config.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* update formats and fix lint errors

* remove client callback passthrough

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-12-06 16:21:59 -05:00
Jeremiah Lowin
0cf12fa30c
Add regression tests for functools.wraps + Context (#2524) (#2566)
Follow-up to PR #2563 which fixed the signature handling in
create_function_without_params. These tests ensure the fix
works end-to-end for all object types that support Context injection.
2025-12-06 12:32:39 -05:00
alex
bc5c1bfbd7
docs: document stateless_http for horizontal scaling (#2547) 2025-12-06 11:59:01 -05:00
Aidan Allchin
b9cde3bdb6
Fix: Include signature modification in create_function_without_params (#2563)
* Fix: Include signature modification in create_function_without_params

When excluding parameters via create_function_without_params(), only
__annotations__ was being updated but not __signature__. This caused
Pydantic's _arguments_schema() to fail when it iterated over signature
parameters that didn't exist in the type hints dictionary.

The fix adds proper signature reconstruction matching the pattern used
in without_injected_parameters().
Fixes KeyError: 'ctx' when using @mcp.tool() with Context parameters.

* fix: add regression tests for create_function_without_params

The test_pydantic_typeadapter_compatibility test specifically reproduces the issue from #2562 and verifies the fix.

* fix: linter for test function

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-12-06 11:57:36 -05:00
Jeremiah Lowin
8d3deeecc9
Add v2.14.0 upgrade guide (#2565)
* Add v2.14.0 upgrade guide section

* Update upgrade guide to use CodeGroup tabs for all migration examples
2025-12-06 11:40:42 -05:00
Jeremiah Lowin
07750efaab
Fix type errors for ty 0.0.1-alpha.31 upgrade (#2561)
* Fix type errors for ty 0.0.1-alpha.31 upgrade

Add type ignores and fixes for ty's stricter checking:
- Path(None) guards in cli.py
- isinstance checks for ElicitRequestFormParams (URL elicitation support)
- TODO(ty) comments for match/isinstance narrowing bugs
- Method override type ignores for generic covariance
- Starlette Middleware typing workarounds
- Dynamic type construction ignores in json_schema_type.py

* Fix remaining type errors for ty 0.0.1-alpha.31

- Add asserts for optional attribute access in tests
- Add type ignores for dynamic httpx transport internals
- Add TODO(ty) comments for `in` operator on str|bytes
- Add TODO(ty) comments for Starlette Middleware typing
- Use cast for prompt.fn async validation in server.py

* Upgrade ty to 0.0.1-alpha.31

Fixes additional test file type errors discovered after upgrade.
2025-12-05 21:29:14 -05:00
Jeremiah Lowin
1ed976b1d9
refactor: move task attribute to function-based variants only [SEP-1686] (#2560)
* refactor: move task attribute to function-based variants only

* fix: update snapshot test for ResourceTemplate without task field
2025-12-05 21:05:42 -05:00
Chris Guidry
960ce77c7e
Merge pull request #2558 from jlowin/unify-enable-tasks
Remove enable_docket setting; Docket is now always on
2025-12-05 12:40:04 -05:00
Chris Guidry
e63855ccf3 fix: use simple sleep for server readiness, restore Windows parallelism 2025-12-05 12:36:20 -05:00
Chris Guidry
bc587208aa fix: wait for HTTP server readiness via httpx request 2025-12-05 12:33:29 -05:00
Chris Guidry
7e4c854625 fix: use simple sleep instead of _wait_for_port on Windows 2025-12-05 12:32:49 -05:00
Chris Guidry
faa897b7d7 fix: skip wait_closed() on Windows to avoid socket shutdown hang 2025-12-05 12:30:27 -05:00
Chris Guidry
0d96f88d59 test: skip redis connection pool disconnect to fix Windows hangs 2025-12-05 12:27:53 -05:00
Chris Guidry
cf101c2a82 test: run Windows tests without xdist parallelism 2025-12-05 12:23:37 -05:00
Chris Guidry
c02c73f556 Use fresh FakeServer per test to prevent shared state issues
Docket stores a shared FakeServer as a class attribute (_memory_server).
When many tests run in parallel, shared state can cause issues on Windows.

Add autouse fixture to reset the shared server before each test.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 12:18:42 -05:00
Chris Guidry
55297a06a3 Disable Docket strike monitoring in tests (fakeredis busy-loop fix)
Fakeredis doesn't implement blocking xread properly - it returns
immediately instead of waiting. This causes Docket._monitor_strikes
to busy-loop, overwhelming pytest-xdist workers on Windows.

Mock the method to just sleep, since strike coordination isn't useful
with in-memory backends anyway.

See: https://github.com/cunla/fakeredis-py/issues/274

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 12:12:11 -05:00
Chris Guidry
bcd2e594f0 Use SelectorEventLoop on Windows to fix pytest crashes
Windows ProactorEventLoop has known memory corruption issues that cause
pytest-xdist worker crashes with "node down: Not properly terminated".

Setting WindowsSelectorEventLoopPolicy in conftest.py avoids this issue.

See: https://github.com/python/cpython/issues/116773

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 12:03:05 -05:00
Chris Guidry
53efaeba5b Add timeout to worker cleanup to prevent hanging on Windows
When pytest-xdist terminates worker processes on Windows, the event loop
may close before cleanup can complete. Adding a 2-second timeout to the
worker cancellation prevents indefinite hanging.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 11:52:43 -05:00
Chris Guidry
a34b680bb4 Add missing asyncio import
Lost during rebase conflict resolution.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 11:42:09 -05:00
Chris Guidry
47d3044b06 Remove enable_docket setting; Docket is now always on
Docket provides background task execution and is now always available
for all FastMCP servers. Only `enable_tasks` remains to control the
SEP-1686 task protocol support.

Changes:
- Remove `enable_docket` setting and related validation
- Docket/Worker lifecycle is always active in server lifespan
- CurrentDocket and CurrentWorker dependencies work without config
- Add server readiness signaling via `_started` event
- Fix test timing issues with proper port probing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 11:40:04 -05:00
Jeremiah Lowin
e47abb4bd7
[SEP-1686] Raise ValueError when sync functions have task=True (#2554)
* Raise ValueError when sync functions have task=True

Move validation from server.py decorators to the from_function() class
methods on FunctionTool, FunctionPrompt, FunctionResource, and
FunctionResourceTemplate. This ensures the check runs regardless of how
the objects are created.

* Fix async check for callable classes and staticmethods

Move the task=True async validation to run AFTER callable classes and
staticmethods are unwrapped, preventing false positives for async
callable classes with sync-looking signatures.
2025-12-05 11:31:28 -05:00
Chris Guidry
66aaf420c9
[2.14] SEP-1686 tasks (#2378)
* Implement MCP background tasks (SEP-1686) using Docket

Adds support for background task execution via the MCP task protocol,
powered by Docket for task queue management.

- Tools, resources, and prompts can be marked with `task=True` to run async
- Progress dependency for tracking task progress
- CurrentDocket and CurrentWorker dependencies for advanced use cases
- Client API with `.call_tool(..., task=True)` returns task handles
- Task status notifications via subscriptions
- CLI worker command for distributed task processing

Configuration via environment:
- FASTMCP_ENABLE_DOCKET=true
- FASTMCP_ENABLE_TASKS=true
- FASTMCP_DOCKET_URL=redis://... (or memory:// for single-process)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Fix tasks example import (TaskStatusResponse → GetTaskResult)

The example was using a non-existent TaskStatusResponse type.
Updated to use mcp.types.GetTaskResult which is what the
on_status_change callback actually receives.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Fix env var name in Docket error messages

The error messages referenced FASTMCP_EXPERIMENTAL_ENABLE_DOCKET but the
actual setting is FASTMCP_ENABLE_DOCKET.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* Remove deprecated code re-added from pre-#2329 branch

- Remove ExtendedEnvSettingsSource (FASTMCP_SERVER_ prefix support)
- Remove dependencies parameter from FastMCP.__init__

* Replace fakeredis git pin with PyPI release

* Remove redundant fakeredis dev dep (pulled via pydocket)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-12-04 20:10:35 -05:00
SEONG SEOB KIM
b109cf05cb
Update FastMCP server documentation link (#2529)
The link to FastMCP Server Documentation was pointing to /servers/fastmcp which returns 404. Changed to /servers/server.

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-12-04 19:22:26 -05:00
William Easton
ffb8607e24
Reduce Marvin Test Failure noise
Reduce Marvin Test Failure noise
2025-12-04 18:01:47 -06:00
William Easton
b130c0161e
Small additional changes 2025-12-04 17:57:08 -06:00
William Easton
5838e90105
Purposefully fail the workflow 2025-12-04 17:40:58 -06:00
William Easton
8c2675447f
Update martian-test-failure.yml 2025-12-04 17:39:51 -06:00
William Easton
177eeaee50
Less noise 2025-12-04 17:37:41 -06:00
Jeremiah Lowin
707d664541
Add supabase docs (#2030)
* Add supabase docs

* Update supabase version badge to 2.13.0
2025-12-04 18:25:57 -05:00
Chris Guidry
9ea76e8e01
Merge pull request #2550 from jlowin/deflake-integration-tests-2
Improve rate limit detection for integration tests
2025-12-04 13:48:41 -05:00
Chris Guidry
6faef9d96f Add timeout to client cleanup to prevent hangs
When the MCP SDK's transport tries to terminate a session during cleanup,
it can hang if the server is unresponsive (e.g., rate-limited). Adding a
5-second timeout ensures we don't block forever during `__aexit__`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 12:05:55 -05:00
Chris Guidry
a8bbd745fe Improve rate limit detection for integration tests
When GitHub's API rate limits cause asyncio shutdown issues, the test
times out rather than failing with the underlying 429 error. Updated the
detection logic to check for 429 indicators in the captured output when
a timeout occurs.

Also increased the per-test timeout from 15s to 30s to give remote API
calls more breathing room.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 11:52:40 -05:00
Jeremiah Lowin
acf4db0bf5
Add SEP-1034 default values support for elicitation (#2545)
- Add comprehensive tests for default values in elicitation schemas
- Document default values support in elicitation docs
- Confirms FastMCP automatically supports defaults via Pydantic

Closes #2544
2025-12-04 10:43:48 -05:00
Jeremiah Lowin
7f8a010798
Move TokenHandler to OAuthProvider for consistent error codes (#2538)
All OAuth providers now return correct invalid_client error codes
instead of unauthorized_client for auth failures. Previously only
OAuthProxy had this fix; now OAuthProvider (and InMemoryOAuthProvider)
also benefit.
2025-12-04 10:35:00 -05:00
Chris Guidry
97438db0ac
Deflake GitHub MCP remote integration tests (#2543)
The pytest-retry plugin was causing teardown crashes due to a bug with
pytest's tmp_path fixture stash. Removing `@pytest.mark.flaky` and instead
improving the rate limit detection to properly skip tests on 429 errors.

Also fixed a brittle error message regex - GitHub changed their error
format from "tool not found" to "unknown tool".

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 10:34:43 -05:00
Jeremiah Lowin
fe2ec99cc0
Validate tool names at registration time (SEP-986) (#2540) 2025-12-03 20:12:06 -05:00
Paulo Costa
e1e0553ecb
Make fastapi.cli a runnable module (#2532)
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-12-03 19:23:22 -05:00
Jeremiah Lowin
9cade6c8c8
Fix RFC 8414 path-aware authorization server metadata discovery (#2533)
* Fix RFC 8414 path-aware authorization server metadata discovery

Override get_well_known_routes() in OAuthProvider to rewrite the
authorization server metadata route to be path-aware based on issuer_url,
matching how protected resource metadata already works.

Closes #2527

* Update readme
2025-12-03 19:16:59 -05:00
Chris Guidry
ee63405262
Merge pull request #2507 from jlowin/sdk-auth-updates
[2.14] Update for MCP SDK auth changes
2025-12-03 17:14:57 -05:00
Jeremiah Lowin
e3b103de40 Fix test to use handle() instead of deleted response() method 2025-12-03 16:46:01 -05:00
Jeremiah Lowin
c1e8aea578 Remove dead response() override from TokenHandler
The SDK now handles auth errors in handle() directly, so the response()
override checking for 'Invalid client_id' is never reached.
2025-12-03 15:59:55 -05:00
Jeremiah Lowin
8e57fe43a0 Update tests for client_secret=None behavior 2025-12-03 15:48:47 -05:00
Jeremiah Lowin
6c895aeb32 Fix client_secret validation for token_endpoint_auth_method=none
The MCP SDK now validates that client_secret is provided if it's set,
regardless of token_endpoint_auth_method. Since the proxy uses 'none'
for client auth (handling upstream auth itself), we must also set
client_secret=None to be consistent.
2025-12-03 15:42:25 -05:00
Chris Guidry
654442bfd3 Update FastMCP for MCP SDK 1.23.1 auth changes
- Bump mcp SDK to >=1.23.1
- Add `client_secret_basic` authentication support (SDK PR #1334)
- TokenHandler now wraps SDK's handle() to transform `unauthorized_client`
  to `invalid_client` on 401 responses per OAuth 2.1 spec
- Update `sample()` return type to use SDK's SamplingMessageContentBlock
- Update test expectations for new SDK fields (`task`, `_meta`)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-03 11:50:08 -05:00
Jeremiah Lowin
54692c361e
Expose InitializeResult to middleware (#2516)
Wrap responder.respond() to capture the InitializeResult before it's
sent to the write stream, then return it through the middleware chain.
This allows middleware (e.g., logging) to access the server's initialize
response, not just the client's request.
2025-12-01 20:55:29 -05:00
Jeremiah Lowin
d5ef4132b7
Ensure oracle is in docs.json (#2515) 2025-12-01 20:34:04 -05:00
Jeremiah Lowin
b501f05794
Switch to new OpenAPI parser as default (#2513)
* Switch to new OpenAPI parser as default

Remove the legacy OpenAPI parser and make the experimental parser the
default. The experimental parser (introduced in 2.11) offers better
performance, improved compatibility, and a more maintainable architecture.

- Delete legacy parser (server/openapi.py, utilities/openapi.py)
- Move experimental parser to main locations
- Remove enable_new_openapi_parser feature flag
- Update documentation to remove experimental references

* Add deprecation stubs for experimental OpenAPI imports

* Add deprecated enable_new_openapi_parser setting and deprecation tests

* SDK docs

* REview comments

* Fix docstrings

* Update docstring

* Review comments

* Fix broken links
2025-12-01 20:29:18 -05:00
Jeremiah Lowin
ee5f465a82
2.14 deprecation removals (#2329)
* Initialize 2.14 deprecation removal branch

* Remove deprecated FASTMCP_SERVER_ environment variable prefix (#2330)

* Remove deprecated Context.get_http_request method (#2332)

* Remove fastmcp.Image top-level import (deprecated 2.8.1) (#2334)

* Remove test warnings (#2331)

* Create new branch and fix issue

* Remove deprecated client parameter from FastMCPProxy (#2333)

* Remove deprecated run_streamable_http_async method (#2338)

* Remove deprecated sse_app method (#2337)

* Remove deprecated run_sse_async method (#2335)

* Remove deprecated run_sse_async method

* Update CLI and tests to use run_http_async(transport="sse")

- Change CLI to call run_http_async with transport="sse" instead of run_sse_async
- Update test to mock run_http_async with create=True for v1 servers

* Revert CLI changes - v1 servers do have run_sse_async

- Keep CLI calling run_sse_async() for v1 compatibility
- Update test to mock run_sse_async (which exists on v1)

* Remove unnecessary type ignore for run_sse_async

Method exists on v1 FastMCP class, no type error

* Remove unused imports after test deletion

* Remove deprecated streamable_http_app method (#2336)

* Remove deprecated dependencies parameter from FastMCP constructor (#2340)

* Remove output_schema=False support (deprecated 2.11.4) (#2339)

* Remove deprecated client parameter from FastMCPProxy (#2333)

* Delete deprecated test_output_schema_false.py

Tests functionality that has been removed

* Remove deprecated BearerAuthProvider module (#2341)

* Remove resource_prefix_format="protocol" support (deprecated 2.4.0) (#2342)

* Remove resource_prefix_format="protocol" support (fixes #2195)

Removes deprecated protocol format (prefix+resource://path) and keeps only
path format (resource://prefix/path). Since only one format remains:

- Removed resource_prefix_format from settings, FastMCP.__init__, and helpers
- Simplified add_resource_prefix, remove_resource_prefix, has_resource_prefix
- Removed MountedServer.resource_prefix_format field
- Deleted tests for protocol format

All resource prefixes now use path format exclusively.

* Clean up resource_prefix_format references

- Remove from test files
- Update documentation to remove protocol format section
- Move custom HTTP routes note to mounting section
- Remove resource_prefix_format from settings docs

* Use inline version note instead of badge for prefix format

* Remove obsolete test functions and update docs

- Delete test functions that no longer assert anything
- Remove proxy.mdx reference to deleted prefix format section

* Format error messages per ruff

* Remove from_client classmethod (deprecated 2.8.0) (#2343)

* Remove deprecated from_client classmethod (fixes #2192)

* Remove unused Client import

* Remove add_resource_fn method (deprecated 2.7.0) (#2345)

* Update SDK

* Add missing imports for exclude_args deprecation warning
2025-12-01 14:11:00 -05:00
marvin-context-protocol[bot]
4d22d9c4d7
chore: Update SDK documentation (#2494)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2025-12-01 13:51:12 -05:00
Jeremiah Lowin
9c21754a45
Fix Azure provider OIDC scope handling (#2506)
* Fix Azure provider to handle OIDC scopes correctly

OIDC scopes (openid, profile, email, offline_access) were being
incorrectly prefixed with identifier_uri, causing Azure to reject
authorization requests. This fix:

- Detects OIDC scopes and sends them unprefixed to Azure
- Filters OIDC scopes from token validation (Azure doesn't include
  them in access token scp claims)
- Still advertises OIDC scopes to clients via valid_scopes
- Also handles dot-notation scopes (e.g., User.Read) correctly

Fixes #2451, #2420

* Fix dot-notation scopes to be prefixed (custom scopes can have dots)

* Improve Azure scope handling docs with clear examples
2025-12-01 13:42:36 -05:00
Jeremiah Lowin
83085c3cd3
Fix version badges for icons and website_url; add Discord example (#2509)
* Fix version badges and remove redundant badges from Discord doc

* Add Discord OAuth example
2025-12-01 13:39:43 -05:00
Jeremiah Lowin
aa53bdf53e
Add Discord OAuth integration documentation (#2508) 2025-12-01 13:34:33 -05:00
Ayesha Shafique
e1d41f5e3b
Add Discord OAuth provider and corresponding tests (#2428)
* Add Discord OAuth provider and corresponding tests

* Update DiscordProvider client_secret and required_scopes documentation

* Add Discord to authentication support list in README

* Fix Discord token verifier to match actual API response format

Discord's /api/oauth2/@me endpoint returns:
- "scopes" as a list, not "scope" as a space-separated string
- "expires" as ISO timestamp, not "expires_in" as seconds
- "user" data directly in the response (no need for extra API call)

* Simplify Discord token verifier

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-12-01 12:58:29 -05:00
Jeremiah Lowin
246a0adefd
Fix get_access_token() returning stale token after OAuth refresh (#2505)
* Fix get_access_token() returning stale token after OAuth refresh

Fixes #1863

* Update dependencies.py
2025-12-01 10:51:02 -05:00
Jeremiah Lowin
01ecc91807
Fix OAuth proxy refresh token storage for multi-instance deployments (#2483)
* Fix OAuth proxy refresh token storage for multi-instance deployments

- Use pluggable client_storage instead of local dict for refresh tokens
- Store refresh tokens by SHA-256 hash for defense in depth
- Remove unused access token and relationship mapping stores
- Simplify revocation logic

* Address review feedback for refresh token storage

- Use calculated refresh_expires_in for TTL instead of hardcoded 30 days
- Populate expires_at field with actual expiry timestamp
- Add client_id validation in load_refresh_token to prevent cross-client token usage
2025-12-01 09:26:27 -05:00
Jeremiah Lowin
3341c0c893
Add icons support to proxy classes (#2502) 2025-12-01 08:55:51 -05:00
Muspi Merol
adbb7d6e53
Add title attribute to ProxyTool, ProxyResource, … (#2497)
* fix: add title attribute to ProxyTool, ProxyResource, ProxyTemplate, and ProxyPrompt

* test: add title assertions for proxy tools, resources, and prompts
2025-12-01 08:48:42 -05:00
Jeremiah Lowin
ba69fba305
Add consent_csp_policy parameter for CSP customization (#2484)
* Add consent_csp_policy parameter to OAuthProxy

Allows customization or disabling of CSP directives on the consent page.
Fixes #2476.

* Add consent_csp_policy to OIDCProxy and update docs

* Fix HTML injection vulnerability in CSP policy

HTML-escape the CSP policy value before inserting into meta tag to prevent HTML injection when CSP policies contain quotes.
2025-11-26 16:53:40 -05:00
jason
d770a76c79
fix: prevent $defs mutation in Tool.from_tool transforms (#2493)
Deep copy parent_defs before passing to compress_schema to prevent
mutation from affecting parent tool schemas when child tools hide
parameters that remove all $ref usage.
2025-11-26 16:52:01 -05:00
SHAIK AYESHA
256f376988
Fix version number in VersionBadge: change 2.14.0 to 2.13.0 (#2491)
Corrects the typo in the VersionBadge component in docs/servers/icons.mdx. The version number was incorrectly displayed as 2.14.0 but should be 2.13.0 to match the actual current version of FastMCP.

Fixes issue #2487
2025-11-26 16:51:18 -05:00
dependabot[bot]
25166afe98
Bump actions/checkout from 5 to 6 (#2474)
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-26 16:50:49 -05:00
marvin-context-protocol[bot]
fd0297014f
chore: Update SDK documentation (#2365)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2025-11-22 12:30:09 -05:00
Akshay Parihar
a57a155c2e
Scalekit provider updates (#2413)
* sk-provider updates - aud not enforce, scopes enforce if present

* updating env_prefix, adding debug logs

* updating docs

* ruff formatting

* not changing prefix for backward compatiblity

* backward compatibility changes

* give more preference to base_url than mcp_url if both passed

* updating docs

* refactor

* updating example server

* updating readme of example

* updating docs

* updating tests to reflect what should ideally go in the parameter
2025-11-22 12:22:13 -05:00
Kevin J Gao
05db547d86
feat: Made Changes to DescopeProvider to Support New Well Known URLs (#2392)
* added new DescopeProvider for Agentic Identity Hub

* fixed formatting with ruff

* made coderabbit change

* changed wording in descope.mdx

* added backwards compatibility

* ran ruff

* added scopes_supported to protected metadata resource

* empty commit to re-run tests
2025-11-22 12:20:50 -05:00
William Easton
08a0503f63
Merge pull request #2437 from jlowin/latest-py-key-value
Bump py-key-value-aio to 0.3.0
2025-11-22 11:10:13 -06:00
William Easton
16562c918e
Merge branch 'main' into latest-py-key-value 2025-11-22 11:07:28 -06:00
Jeremiah Lowin
e37951eb5d
Merge pull request #2446 from Shengshenlan/patch-1
Update http.mdx
2025-11-22 12:04:49 -05:00
Jeremiah Lowin
3592af524a
Merge pull request #2462 from Neet-Nestor/main
Fix Azure OAuth token refresh with unprefixed scopes
2025-11-22 12:04:04 -05:00
Jeremiah Lowin
f06193850a
Merge pull request #2440 from jlowin/fix-exclude-args-serialization
Fix exclude_args with non-serializable types
2025-11-22 12:02:29 -05:00
Nestor Qin
4d7af38d3f
Merge branch 'main' into main 2025-11-20 15:34:35 -08:00
Nestor Qin
2852f2cb13 Use lazy logging 2025-11-20 15:31:40 -08:00
Nestor Qin
119bf445ea Resolve comments 2025-11-20 15:16:50 -08:00
Nestor Qin
463652a878 Add unit tests 2025-11-20 15:08:50 -08:00
Nestor Qin
9e28da11f2 fix Azure token refresh issue 2025-11-19 19:02:31 +00:00
William Easton
b3bce0c231
Merge pull request #2442 from jlowin/patch-test-failure-analysis
Update martian-test-failure.yml
2025-11-18 08:41:24 -06:00
Shengshenlan
8b546fd294
Update http.mdx
add a hint
2025-11-18 17:10:50 +08:00
William Easton
73d0f6abf0
Update martian-test-failure.yml 2025-11-17 18:34:31 -06:00
William Easton
6a462827a4
Merge branch 'main' into latest-py-key-value 2025-11-17 12:12:58 -06:00
William Easton
1b5e3cb272
Actually update lockfile, ignore caching diskstore cleanup 2025-11-17 12:03:24 -06:00
Jeremiah Lowin
52100b08ff Fix exclude_args with non-serializable types
Fixes issue #2431 where exclude_args fails when excluded parameters have
non-serializable types (e.g., ServerSession). The fix excludes parameters
from function annotations before Pydantic tries to serialize them.

Also adds deprecation notice that exclude_args will be deprecated in
FastMCP 2.14 in favor of dependency injection.
2025-11-17 12:25:43 -05:00
Jeremiah Lowin
dd876b5833
Merge pull request #2439 from jlowin/oidc-proxy-extra-params
Add extra_authorize_params and extra_token_params to OIDCProxy
2025-11-17 12:23:11 -05:00
Jeremiah Lowin
24204cdafc
Merge pull request #2438 from jlowin/google-provider-refresh-token-defaults
Add refresh token support defaults to GoogleProvider
2025-11-17 11:58:58 -05:00
Jeremiah Lowin
e0f0f816b7 Add extra_authorize_params and extra_token_params to OIDCProxy 2025-11-17 11:57:58 -05:00
Jeremiah Lowin
be90d6eb60
Merge pull request #2432 from jlowin/dependabot/github_actions/actions/setup-python-6
Bump actions/setup-python from 5 to 6
2025-11-17 11:56:37 -05:00
Jeremiah Lowin
cf7ae12a65
Merge pull request #2433 from jlowin/dependabot/github_actions/actions/checkout-5
Bump actions/checkout from 4 to 5
2025-11-17 11:56:10 -05:00
Jeremiah Lowin
ea31747572 Add refresh token support defaults to GoogleProvider
Set access_type=offline and prompt=consent by default to ensure
refresh tokens are returned by Google. Also expose extra_authorize_params
so users can customize authorization behavior.
2025-11-17 11:54:00 -05:00
William Easton
64c3796d31
Bump py-key-value versions 2025-11-17 10:49:11 -06:00
dependabot[bot]
e5c6f04e0f
Bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 04:25:24 +00:00
dependabot[bot]
065a4d520b
Bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-17 04:25:13 +00:00
Jeremiah Lowin
b5f88bfe42
Merge pull request #2426 from jlowin/simplify-object-schema-check
Simplify _is_object_schema helper
2025-11-15 12:39:04 -05:00
Jeremiah Lowin
e9667b6624 Fix test regex pattern for updated error message 2025-11-15 12:32:37 -05:00
Jeremiah Lowin
0af1c78cc9 Simplify _is_object_schema helper function 2025-11-15 12:27:37 -05:00
Jeremiah Lowin
e6936d0bf1
Merge pull request #2422 from jlowin/exclude-mcp-1.21.1-add-scope-validation
Exclude MCP SDK 1.21.1 and add scope validation to InMemoryOAuthProvider
2025-11-15 12:22:44 -05:00
Jeremiah Lowin
533cfb66fa
Merge pull request #2424 from jlowin/fix-bug-report-dI2C9
Fix self-referencing types not being recognized as object schemas
2025-11-15 12:21:21 -05:00
Jeremiah Lowin
f6635aa15b Fix type errors in oauth_proxy and test_auth_integration
- Add null checks for client_id before using in OAuthTransaction, AuthorizationCode, AccessToken, RefreshToken
- Add null check for redirect_uris before len() call
- Import AuthorizeError from mcp.server.auth.provider
2025-11-15 12:21:15 -05:00
Jeremiah Lowin
3826f1a240 Fix typos and formatting 2025-11-15 12:21:15 -05:00
Jeremiah Lowin
c7f4ff2cbe Add scope validation to InMemoryOAuthProvider and remove debug breakpoint
- Add scope validation to InMemoryOAuthProvider.register_client() to match MCP SDK behavior
- Ensures unit tests catch scope-related bugs like the MCP SDK 1.21.1 issue
- Remove debug breakpoint from OAuth client redirect_handler
2025-11-15 12:21:15 -05:00
Jeremiah Lowin
ec38507f9b Exclude MCP SDK 1.21.1 and update OAuth client tests
- Exclude MCP SDK 1.21.1 due to bug adding metadata URL to scopes
- Update OAuth client tests to use valid scopes
2025-11-15 12:21:15 -05:00
Jeremiah Lowin
e3ed74b794
docs: fix run_server_async documentation (#2423)
Remove incorrect task_group parameter and AnyIO references. Update to show correct async context manager usage matching actual implementation.

Fixes #2395

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2025-11-15 12:07:22 -05:00
Jeremiah Lowin
d40f22b3e6 Fix type errors in oauth_proxy and test_auth_integration
- Add null checks for client_id before using in OAuthTransaction, AuthorizationCode, AccessToken, RefreshToken
- Add null check for redirect_uris before len() call
- Import AuthorizeError from mcp.server.auth.provider
2025-11-15 12:06:33 -05:00
Jeremiah Lowin
6df54c0729 Fix typos and formatting 2025-11-15 12:04:51 -05:00
Jeremiah Lowin
1895f86323 Add scope validation to InMemoryOAuthProvider and remove debug breakpoint
- Add scope validation to InMemoryOAuthProvider.register_client() to match MCP SDK behavior
- Ensures unit tests catch scope-related bugs like the MCP SDK 1.21.1 issue
- Remove debug breakpoint from OAuth client redirect_handler
2025-11-15 12:02:28 -05:00
Jeremiah Lowin
a98838c158 Exclude MCP SDK 1.21.1 and update OAuth client tests
- Exclude MCP SDK 1.21.1 due to bug adding metadata URL to scopes
- Update OAuth client tests to use valid scopes
2025-11-15 11:58:14 -05:00
Kiran Thakkar
ab683f178b
OCI Provider with Docs (#2389)
* OCI Provider with Docs

* Addressing feedback and adding improvements

* Addressing feedback and adding improvements

* Fixed Step Typo and Images

* Fixed Language Identifier

* Fixed Code Check Issues

* Fixed Identity Propagation Trust Command

* Renamed ociprovider to oci and fixed documentation

* Renamed ociprovider to oci

* Fixed Config URL

* Fixed TokenExchange variable

* Fixed Environment Variables

* Fixed Environment Variables

* Fixed Ruff Check

* Fixed Code Rabbit Comments

* Fixed Code Rabbit Comments
2025-11-15 11:04:47 -05:00
Jeremiah Lowin
bc076cba12
Handle request_context availability during MCP initialization (#2400)
The on_initialize hook (2.13.0) runs before the MCP session is established, causing request_context
to be unavailable. Changed request_context to return None instead of raising, allowing middleware to
check availability and use HTTP helpers when needed. Updated docs to guide this pattern.

Closes #2393
2025-11-15 10:50:24 -05:00
ChristophNetsch
67f9329b83
fix(OpenAPIParser): Fix missing $defs for response schemas in experimental OpenAPI parser (#2398)
* fix(OpenAPIParser): Fix missing  for response schemas in experimental OpenAPI parser

* chore: run linting

---------

Co-authored-by: Christoph Netsch <netsch@alpamayo.ch>
2025-11-15 10:50:00 -05:00
William Easton
be9c27fec4
Claude triage for test failures (#2407) 2025-11-15 10:35:27 -05:00
William Easton
8e0c6c8685
docs: clarify pytest-asyncio dependency and asyncio mode configuration (#2399)
* docs: clarify pytest-asyncio dependency and asyncio mode configuration

Added a Prerequisites section to the testing documentation explaining:
- pytest-asyncio is required for async test functions and fixtures
- Recommended configuration: asyncio_mode = 'auto' in pyproject.toml
- This eliminates need for @pytest.mark.asyncio decorators

Resolves #2372

Co-authored-by: William Easton <strawgate@users.noreply.github.com>

* feat: add testing_demo example with comprehensive test suite

Add a standalone example project demonstrating FastMCP testing patterns:
- Tools, resources, and prompts with full test coverage
- pytest-asyncio configuration in pyproject.toml
- 18 passing tests showing async fixtures, parametrized tests, and more
- Documentation explaining testing best practices

Co-authored-by: William Easton <strawgate@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: William Easton <strawgate@users.noreply.github.com>
2025-11-15 10:34:29 -05:00
EdenTrainorCDL
dffaa0bfce
fix(oauth_proxy): 🐛 add _extra_token_params as kwargs in refresh_token call (#2387)
custom oauth providers sometimes require extra parameters in their token calls, hence the need for the _extra_token_params in the inital token request. This PR includes those extra token params in the token refresh request too.
2025-11-15 10:33:51 -05:00
Sean McGrath
f7c89e2630
fix: upstream token cache expires when refresh expires (#2410)
* fix: upstream token cache expires when refresh expires

* fix: handle case where no refresh token provided
2025-11-15 10:32:34 -05:00
Lawrence
d191995840
Update CSP to allow data URI images on OAuth screens (#2405)
* Update CSP to allow data URI images

* Fix formatting issue
2025-11-15 10:30:31 -05:00
Patrick Stöckle
18d4a6ab63
chore(typos): fix additional typos (#2396) 2025-11-08 10:47:56 -05:00
Jeremiah Lowin
0fa3097153
Add version badge for DebugTokenVerifier in documentation (#2390)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2025-11-06 18:25:26 -05:00
William Easton
05ac9457b8
Fix duplicate keyword argument error in configure_logging (#2381)
Allow traceback-related kwargs to override defaults by building a dict
with defaults first, then updating with user-provided values.

Fixes #2356

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: William Easton <strawgate@users.noreply.github.com>
2025-11-06 10:11:16 -05:00
Jeremiah Lowin
f8b896490e
Fix Windows test timeout and restore parallel testing (#2383)
OAuth consent tests were timing out on Windows due to SQLite database
locking when OAuthProxy instances used the default DiskStore without
explicit MemoryStore configuration. Added explicit client_storage=MemoryStore()
to three tests in TestConsentPageServerIcon.

Also restored parallel testing on Windows (--numprocesses auto) which
was previously disabled but is now safe with proper test isolation.
2025-11-05 20:25:10 -05:00
Cemal Kılıç
f540385b58
feat: add algorithm configuration to Supabase auth provider (#2376) 2025-11-05 20:21:10 -05:00
Jeremiah Lowin
48fc8cbc9a
Fix consent form action for subpath mounting (#2382)
* Fix consent form action for subpath mounting

When a FastMCP server is mounted at a subpath (e.g., /api/v1), the consent
form used a hardcoded absolute path (/consent/submit) that didn't include
the mount prefix, causing 404 errors on submission.

Changed the form to use an empty action (action="") which submits to the
current URL, making it work correctly regardless of mount path. Also
consolidated the consent endpoints to handle both GET and POST at /consent.

Fixes #2380

* Update integration test to use /consent instead of /consent/submit
2025-11-05 20:20:38 -05:00
Giovanna Zanardini
a6ddde27df
Allow OAuth instance to use the same httpx factory as the Transport (#2324)
* Allow OAuth instance to use the same httpx factory as the Transport

* Fix test

* Update SSL verification mode assertion in tests

* This is actually not needed

* Creating a Client instance is not needed for this test

* Fix test

* Apply httpx_client_factory fix to SSETransport

Extends the OAuth httpx_client_factory changes to SSETransport.
SSETransport had the same issues as StreamableHttpTransport where it
wasn't passing the custom httpx client factory to OAuth, causing
certificate verification settings to be ignored during OAuth flows.

Changes:
- Set httpx_client_factory before calling _set_auth()
- Pass httpx_client_factory to OAuth constructor
- Add test for SSETransport OAuth client factory propagation

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-11-04 12:14:28 -05:00
Jeremiah Lowin
a7563a39b0
Fix get_http_headers() returning empty dict in on_initialize middleware (#2370) 2025-11-04 11:56:43 -05:00
Jeremiah Lowin
a359b0b28d
Document client meta parameter for sending ancillary request data (#2367) 2025-11-04 11:32:31 -05:00
Jeremiah Lowin
80d10d23f4
Fix: URL-encode server name in Cursor deeplinks (#2369)
Server names with special characters (&, ?, #, etc.) were creating
malformed deeplink URLs. Now properly percent-encoded.
2025-11-04 11:26:57 -05:00
Jeremiah Lowin
c6a9b3df22
Fix Windows test timeouts from SQLite locking (#2368) 2025-11-04 10:58:49 -05:00
Jeremiah Lowin
5747cb6237
Fix OAuth metadata endpoint URLs when base_url differs from issuer_url (#2353)
* Fix OAuth metadata endpoint URLs when base_url differs from issuer_url

OAuth operational endpoints (/authorize, /token) are mounted at base_url,
but metadata was incorrectly declaring them at issuer_url. This caused
clients following the documented mounting pattern to receive incorrect
endpoint URLs in /.well-known/oauth-authorization-server.

Fixes #2287

* Update auth.py

* Remove unnecessary assertion from OAuthProvider init

* Add info log when issuer_url differs from base_url
2025-11-04 10:38:41 -05:00
Antonio Iorga
7e6610b7b6
Add meta to call tool (#2206)
* feat: Add optional meta parameter to Client tool call methods

* fix: Add support for mcp<1.19

* chore: cleaner solution

* Refactor call_tool to directly accept meta parameter and add tests for meta functionality

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-11-04 10:25:52 -05:00
William Easton
7d0c3700ed
Merge pull request #2361 from jlowin/fix-marvin
Switch marvin to prek from pre-commit
2025-11-03 21:29:55 -06:00
strawgate
49259ae938 switch marvin to prek from pre-commit 2025-11-03 21:22:22 -06:00
marvin-context-protocol[bot]
7ccaa8ac6d
chore: Update SDK documentation (#2265)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2025-11-03 18:49:06 -05:00
Jeremiah Lowin
dcfd9ee387
Pin Cyclopts to v4.0.0 + compliance note (#2354)
* Pin cyclopts to v5.0.0a1+

* Document cyclopts/docutils licensing

* Update docs
2025-11-03 18:37:12 -05:00
Maxi Fernandez
8ad2dfc00c
fix: on_initialize is not using request params but the whole request (#2357)
* fix initialize type

* undo for clarity

* fix type hint
2025-11-03 16:04:02 -05:00
Jeremiah Lowin
9c861b232b
Add manual initialization control to Client (#2355)
* Add manual initialization control to Client

- Add auto_initialize parameter (default True) to control automatic initialization
- Make initialize() method public with idempotent caching
- Add comprehensive test suite for initialization behavior

* Document client initialization control and server instructions

- Expand documentation to cover auto_initialize parameter
- Show manual initialization for advanced use cases
- Document accessing server instructions via initialize_result

* Update client.mdx
2025-11-03 16:01:30 -05:00
Jeremiah Lowin
6cc9559f84
Bump ty to ==0.0.1a25 (#2350)
* Bump ty to >=0.0.1a25 with type fixes

Follow-up to #2295. Updates ty and fixes compatibility issues with alpha 25, including:

- Updated ignore comment syntax (possibly-unbound-attribute → possibly-missing-attribute)
- Fixed async generator type handling with anext()
- Fixed type narrowing for timeout parameters
- Converted base_url assignments to AnyHttpUrl after string manipulation
- Added CallToolResult to return type annotations
- Removed redundant type casts
- Fixed test form data to use strings instead of bytes

ty alpha 25 has limitations with isinstance() narrowing on unions (see pyproject.toml for details), requiring some targeted type ignores.

* Pin ty to ==0.0.1a25

Alpha releases can have breaking changes, so pin to the tested version.
2025-11-02 20:02:45 -05:00
Cole Murray
0e97a261cf
Security: Validate Cursor deeplink URLs and use safer Windows API (#2348)
* 🤖 Security: Validate Cursor deeplink URLs and replace cmd.exe on Windows

- Add URL scheme validation to reject non-cursor:// URLs
- Replace subprocess cmd.exe call with os.startfile() on Windows
- Add tests for scheme validation and error handling

* 🤖 Fix tests for cross-platform deeplink validation
2025-11-02 19:49:07 -05:00
Copilot
5499cda9d0
Fix lowest-direct dependency tests to actually test minimum versions (#2295)
* Initial plan

* Pin dev dependencies and use --resolution lowest-direct in pytest commands

Co-authored-by: strawgate <6384545+strawgate@users.noreply.github.com>

* Update uv.lock

* bump ty version

* ty upper bound

* Update uv.lock

* Update ty lock

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: strawgate <6384545+strawgate@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-11-02 17:30:31 -05:00
nate nowack
aba7d762de
switch from pre-commit to prek (#2309)
* switch from pre-commit to prek

🤖 Generated with [Claude Code](https://claude.com/claude-code)

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

* fix prek tools list in contributing.mdx - include prettier, not pytest

* Use prek-action for caching

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-11-02 17:08:36 -05:00
Jeremiah Lowin
bbf3050ba7
Improve ToolResult and structured output documentation (#2349)
- Restructured confusing sections: 'Object-like Results' → 'Dictionaries and Objects', 'Non-object Results' → 'Primitives and Collections', 'Complex Type Example' → 'Typed Models'
- Simplified CodeGroup examples to show Tool Definition + MCP Result instead of 3-4 confusing tabs
- Split Primitives/Collections into separate CodeGroups for clarity
- Renamed 'Full Control with ToolResult' → 'ToolResult and Metadata' for better TOC visibility
- Flattened ToolResult documentation with inline field descriptions instead of nested headings
- Added version badge for ToolResult meta field (2.13.1)
- Added clarification that ToolResult meta is separate from @mcp.tool meta
- Improved example server with realistic metadata (execution time, character/word counts)
- Fixed code formatting (multi-line objects, trailing commas)
2025-11-02 17:07:48 -05:00
Brandon Shar
c18782f02b
Add meta support to ToolResult (#2283)
* Add meta to ToolResult

* add this at the client level and test the full integration

* add example

* slipped through linting somehow

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-11-02 16:46:52 -05:00
Cole Murray
c8ddbff488
Security: Update authlib to 1.6.5 (CVE-2025-61920) (#2347)
Updates authlib from 1.6.1 to 1.6.5 to address CVE-2025-61920, which
fixes a denial of service vulnerability in JOSE implementation that
accepts unbounded JWS/JWT header and signature segments.
2025-11-02 11:46:55 -05:00
Jeremiah Lowin
c9ec1459e1
Mark flaky Windows test for retry (#2344)
* Mark flaky Windows test for retry

test_multi_client_transform_with_filtering occasionally times out on Windows CI during exception formatting in linecache.checkcache(). Add @pytest.mark.flaky with 3 retries.

* Remove unnecessary delay from flaky marker
2025-11-01 16:52:08 -04:00
Jeremiah Lowin
dab125e069
Remove test warnings (#2331) 2025-11-01 14:53:51 -04:00
Harshith Thota
321f404046
Added to_data_uri method for Image class. (#2227)
* Added to_data_uri method and path_to_data_uri classmethod for Image class.

* Removed path_to_data_uri classmethod and modified _get_mime_type to use mimetypes.guess_type function instead of hardcoded dictionary.

* Register image/webp with mimetypes before guess_type to support WEBP mimetype detection on Python 3.10.

* Improved branch coverage for Image.to_data_uri.

* Added Image._to_data_uri example in the docs.
2025-11-01 11:40:08 -04:00
Josh Thomas
08c49e62e8
Fix query-only resource templates not matching URIs without query strings (#2323)
* Fix query-only resource templates not matching URIs without query strings

* apply the same fix to `has_resource`
2025-11-01 11:27:37 -04:00
Jeremiah Lowin
443c44c507
Configure Marvin to auto-create PRs and label issues (#2319)
* Update marvin.yml

* Configure Marvin to auto-create PRs and label issues

* Make Marvin instructions more assertive with MUST
2025-10-31 10:38:15 -04:00
Jeremiah Lowin
de58bb0e6c
Add DebugTokenVerifier with custom sync/async validation (#2296)
* Add DebugTokenVerifier with custom sync/async validation

* move import
2025-10-31 10:38:01 -04:00
Jeremiah Lowin
87adacfc8b
Require uvicorn>=0.35 for websockets-sansio support (#2307)
Fixes #2299

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2025-10-31 10:22:55 -04:00
William Easton
d12f51d22c
Merge pull request #2311 from jlowin/claude/issue-2310-20251029-2141
Add maturity warnings for py-key-value backends
2025-10-30 13:01:35 -05:00
claude[bot]
237f0decd2 Add maturity warnings for py-key-value backends
Add warning notes to documentation directing users to review
py-key-value documentation for backend maturity and limitations
before production use.

Co-authored-by: William Easton <strawgate@users.noreply.github.com>
2025-10-29 21:43:31 +00:00
Jeremiah Lowin
e2d317eeb6
Add OIDCProxy to auth module exports (#2308)
Fixes #2298

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2025-10-29 14:28:26 -04:00
Jeremiah Lowin
1ca53b4134
Add base_authority parameter to AzureProvider for Azure Government support (#2306) 2025-10-29 14:06:21 -04:00
Jeremiah Lowin
463b336941
Fix Chrome CSP blocking OAuth consent form with custom protocol redirects (#2305)
* Fix Chrome CSP blocking OAuth consent form with custom protocol redirects

* Fix Chrome CSP blocking OAuth consent form with custom protocol redirects

Dynamically include custom protocol schemes in CSP form-action directive
when redirect URIs use custom protocols like cursor://
2025-10-29 14:04:56 -04:00
Jake Kaplan
9c750fe5e4
Merge pull request #2294 from jlowin/exec-info-from-run
Switch to `logger.exception` for `fastmcp run/inspect`
2025-10-28 11:22:40 -04:00
jake@prefect.io
4262468b44 use logger.exeception 2025-10-28 10:18:21 -04:00
jake@prefect.io
a2f710fa9d add exc_info to fastmcp run 2025-10-28 10:14:53 -04:00
Jeremiah Lowin
887e72b39b
Replace openapi-core with jsonschema-path (#2291) 2025-10-28 06:46:52 -07:00
Jeremiah Lowin
318da83f6b
Fix py-key-value-aio minimum version to 0.2.8 (#2288)
* Fix py-key-value-aio minimum version to 0.2.8

FernetEncryptionWrapper was introduced in 0.2.8, not 0.2.6.

Fixes #2284

* Update lockfile
2025-10-28 06:30:03 -07:00
coderabbitai[bot]
4ea896c246
📝 Add docstrings to enhancement/support-jwt-multiple-issuers (#2282)
* 📝 Add docstrings to `enhancement/support-jwt-multiple-issuers`

Docstrings generation was requested by @jlowin.

* https://github.com/jlowin/fastmcp/pull/2233#issuecomment-3453446122

The following files were modified:

* `src/fastmcp/server/auth/providers/jwt.py`

* Fix formatting issues in JWT provider docstrings

Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2025-10-27 19:26:25 -04:00
mhassaninmsft
b24d771457
Supporting Multiple Issuers For JWTVerifier Oauth Workflow (#2233)
* multiple issuers

* Update tests/server/auth/test_jwt_provider.py

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Fix static checks

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-10-27 18:13:40 -04:00
Jeremiah Lowin
15dbe7ecf0
Add custom token verifier support to OIDCProxy (#2279)
* Add custom token verifier support to OIDCProxy

OIDCProxy now accepts an optional token_verifier parameter to support
non-JWT token formats like opaque tokens from providers such as Clerk.

When provided, the custom verifier is used instead of creating a default
JWTVerifier. Parameters that only apply to JWTVerifier creation (algorithm,
required_scopes) raise clear errors when specified alongside a custom
verifier. Parameters with other purposes (audience for OAuth flow,
timeout_seconds for config fetch) remain allowed.

The custom verifier's required_scopes are automatically loaded and
advertised through OAuth discovery endpoints.

* Document custom token verifier support in OIDC proxy
2025-10-27 13:38:20 -04:00
Jeremiah Lowin
6d600e36db
Remove trailing slashes from MCP endpoint URLs in docs (#2277) 2025-10-27 10:36:47 -04:00
William Easton
7999dcbf2c
Merge pull request #2276 from JonZeolla/fixdocs/key-value-project
fix(docs): correct the key_value repo link
2025-10-27 07:52:55 -05:00
Jon Zeolla
11277f6e21 fix(docs): correct the key_value repo link 2025-10-27 07:35:10 -04:00
Jeremiah Lowin
8a48146aad
Cleanly render oauth errors from proxy (#2268) 2025-10-26 21:08:05 -04:00
Jeremiah Lowin
5ceafe425c
Fix OAuth token storage documentation (#2272)
Correct imports (DiskStore not FileStore) and simplify structure.
2025-10-26 21:07:54 -04:00
Jeremiah Lowin
9a7c04873c
Add note about docs version (#2271) 2025-10-26 21:00:49 -04:00
Jeremiah Lowin
f5bdf8f6d3
Improve OAuth client token storage security documentation (#2270)
* Update docs for required scopes

* add scopes

* Fix Azure scope validation

Azure returns unprefixed scopes in JWT tokens but requires prefixed scopes in authorization requests. The previous implementation incorrectly validated tokens against prefixed scopes, causing "invalid_token" errors.

Simplified AzureProvider to use standard JWTVerifier with unprefixed scopes for validation. Scopes are only prefixed when building the Azure authorization URL via _build_upstream_authorize_url() override.

Closes #2263

* Improve OAuth client token storage security documentation

Updated warning message and documentation to address security concerns
around storing OAuth credentials for multiple MCP servers.
2025-10-26 20:04:54 -04:00
Jeremiah Lowin
8bd3a308c9
Update Azure sidebar title to include Entra ID (#2266)
* Update Azure sidebar title to include Entra ID

Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>

* Update Azure title to emphasize Microsoft Entra ID

Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>

* Update Azure title to emphasize Azure over Entra ID

Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2025-10-26 19:49:40 -04:00
Jeremiah Lowin
ba47db9b8c
Fix Azure scope validation (#2269)
* Update docs for required scopes

* add scopes

* Fix Azure scope validation

Azure returns unprefixed scopes in JWT tokens but requires prefixed scopes in authorization requests. The previous implementation incorrectly validated tokens against prefixed scopes, causing "invalid_token" errors.

Simplified AzureProvider to use standard JWTVerifier with unprefixed scopes for validation. Scopes are only prefixed when building the Azure authorization URL via _build_upstream_authorize_url() override.

Closes #2263
2025-10-26 11:41:22 -04:00
William Easton
9d4c378e1b
Add "High Value" Ruff Rules (#2255)
* Safe Fixes from ruff

* Fix remaining issues

* lint/check

* Fix mysterious ty check errors

* small cleanup

* pr fixes
2025-10-26 10:20:31 -04:00
Copilot
e74918a544
Add CI test job for lowest-direct dependency resolution (#2261)
* Initial plan

* Add test job for lowest-direct dependency resolution

Co-authored-by: strawgate <6384545+strawgate@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: strawgate <6384545+strawgate@users.noreply.github.com>
2025-10-26 10:16:54 -04:00
marvin-context-protocol[bot]
716e50dae0
chore: Update SDK documentation (#2214)
Co-authored-by: marvin-context-protocol[bot] <225465937+marvin-context-protocol[bot]@users.noreply.github.com>
2025-10-25 08:52:43 -04:00
Jeremiah Lowin
5896daf6a1
Stage 2.13.0 updates (#2252)
* Add 2.13.0 updates

* Small tweaks
2025-10-25 08:52:21 -04:00
Jeremiah Lowin
b57a39c69c
Remove redundant None checks from Context methods (#2251)
The fastmcp property already raises RuntimeError if None, making these checks unreachable.
2025-10-24 19:32:45 -04:00
William Easton
380835593c
Async FileResource and DirectoryResource (#2241)
* Improve DirectoryResource exception logging and async implementation

- Add exception logging before raising ResourceError in read() method
- Convert list_files() to async-native using anyio.Path
- Update read() to await async list_files() and use async is_file() check
- Remove synchronous thread wrapper in favor of native async I/O

Co-authored-by: William Easton <strawgate@users.noreply.github.com>

* Clean-up DirectoryResource

* Update src/fastmcp/resources/types.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: William Easton <strawgate@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-24 19:25:15 -04:00
William Easton
1e5776f69c
Add list_resources, list_prompts, and get_prompt methods to Context (#2249)
* Add list_resources, list_prompts, and get_prompt methods to Context

- Add Context.list_resources() to list all available resources
- Add Context.list_prompts() to list all available prompts
- Add Context.get_prompt() to get a specific prompt with arguments
- Update ToolInjectionMiddleware to use new Context methods instead of creating temporary Client instances
- Remove unused Client and FastMCPTransport imports from tool_injection.py

This improves API consistency by allowing middleware/tools to use Context methods directly without needing to create temporary Client instances.

Fixes #2245

Co-authored-by: William Easton <strawgate@users.noreply.github.com>

* Update docs

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: William Easton <strawgate@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-10-24 19:24:02 -04:00
William Easton
2f13119684
Add Documentation for FastMCP Server Testing (#2244)
* Add doc with recommendations for server testing

* Updates to testing doc

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* add to sidebar under patterns

* Update docs/patterns/testing.mdx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-24 19:11:21 -04:00
William Easton
c002bc389c
Small Clean-up (#2247)
* Project Clean-up

* Move pytest-asyncio to dev dependencies and update agents.md
2025-10-24 19:11:01 -04:00
Jeremiah Lowin
19fbc8435e
delete ratio test (#2250) 2025-10-24 19:10:19 -04:00
William Easton
063ffe9f64
Derive jwt_signing_key from Client Secret, default to Encrypted Disk Store (#2223)
* Checkpoint progress

* Checkpoint progress

* add derive b64 method

* PR clean-up

* refactor da proxy

* Updates to tests

* Make jwt_signing_key required for oauth proxy

* use typing_extensions and fix tests

* PR Cleanup

* also adjust integration tests

* Update docs, use client secret to derive jwt signing key

* You win some you lose some, gg claude

* check for both in derive

* update documentation / clean up

* Update http.mdx

---------

Co-authored-by: Jeremiah Lowin <153965+jlowin@users.noreply.github.com>
2025-10-24 19:08:58 -04:00
Jeremiah Lowin
577ed6e9a3
Fix middleware example: add context parameter to call_next() (#2215)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2025-10-24 18:41:00 -04:00
Jeremiah Lowin
65260c91d5
Add version badge for run_server_async in tests docs (#2237)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Jeremiah Lowin <jlowin@users.noreply.github.com>
2025-10-24 18:40:30 -04:00
Adam Azzam
69b273ee8e
Add exc_info=True to inspect command error logging (#2232)
Fixes #2231

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-24 18:20:45 -04:00
William Easton
f5dbabff4e
Add ToolInjectionMiddleware + Tools for Read/List Resource/Prompt for Client Compat (#2142)
* Add compatibility tools contrib module

Implements four standalone tools that expose resources and prompts
as callable tools for clients that only support the tools capability.

Features:
- list_resources: List all available resources
- get_resource: Read a resource by URI
- list_prompts: List all available prompts
- get_prompt: Get a prompt with optional arguments

The tools use Context to access the server instance and can be easily
added to any FastMCP server using the add_compatibility_tools helper
or by adding individual tool instances directly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: William Easton <strawgate@users.noreply.github.com>

* Simplify compatibility tools to return raw MCP protocol objects

Return raw MCP protocol objects (ListResourcesResult, ReadResourceResult,
ListPromptsResult, GetPromptResult) instead of custom dictionaries. This
makes the tools simpler and more predictable by directly exposing what
the client methods return.

Co-authored-by: William Easton <strawgate@users.noreply.github.com>

* Add tool injection middleware

* cleanup contrib module

* More clean-up

* Clean up tool injection middleware.

* Update src/fastmcp/server/middleware/tool_injection.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Add tool injection docs

* Small cleanup of prompt middleware

* PR Feedback

* Fix tool injection tests

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: William Easton <strawgate@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-24 18:06:58 -04:00
William Easton
c1e154ad66
Use abstract types for FastMCP class instantiation (#2219)
* feat: use abstract collection types in FastMCP.__init__

Use Sequence, Collection, and Mapping from collections.abc for more
flexible typing in FastMCP.__init__ parameters. This allows downstream
developers to pass tuples, sets, and other collection types instead of
being restricted to list and dict.

Changes:
- middleware: list -> Sequence (converted to list internally)
- tools: list -> Sequence
- tool_transformations: dict -> Mapping (ToolManager updated)
- include_tags: set -> Collection
- exclude_tags: set -> Collection
- dependencies: kept as list per maintainer request

Closes #2212

Co-authored-by: William Easton <strawgate@users.noreply.github.com>

* Concrete types in class inits

* Small imports cleanup

* Fix include/exclude tag handling

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: William Easton <strawgate@users.noreply.github.com>
2025-10-24 17:50:07 -04:00
Jeremiah Lowin
a1a5cca765
Fix Azure scope mismatch causing MCP client validation errors (#2243)
* Fix Azure scope mismatch causing MCP client validation errors

The AzureProvider was prefixing scopes during authorization but not in
token validation or Protected Resource Metadata, causing MCP clients to
reject tokens with "Server granted unauthorized scopes".

Changes:
- Prefix required_scopes once during __init__ and use consistently
- Pass prefixed scopes to JWTVerifier for token validation
- PRM now advertises prefixed scopes to MCP clients
- Remove unnecessary idempotent prefixing logic in authorize()
- Update comprehensive documentation explaining scope handling
- Update tests to reflect corrected behavior

Closes #2151

* Clarify that identifier_uri is optional in docstring
2025-10-24 17:45:49 -04:00
William Easton
fe0b49484b
Merge pull request #2242 from jlowin/claude/issue-2239-20251024-2057
Mark test_github_api_schema_performance as integration test
2025-10-24 16:12:22 -05:00
William Easton
f614b20150
Merge branch 'main' into claude/issue-2239-20251024-2057 2025-10-24 16:08:28 -05:00
William Easton
9e964a4c92
Fix martian concurrency controls and dedupe issues mcp servers
Fix martian concurrency controls and dedupe issues mcp servers
2025-10-24 16:07:56 -05:00
claude[bot]
638111990d Mark test_github_api_schema_performance as integration test
This test makes external HTTP requests to GitHub and is subject to
network latency, causing CI timeouts. Marking it as an integration test
excludes it from default test runs while keeping it available for
explicit integration testing.

Co-authored-by: William Easton <strawgate@users.noreply.github.com>
2025-10-24 20:59:55 +00:00
William Easton
da4a969d72
WS diff 2025-10-24 15:58:51 -05:00
William Easton
bd4b6e6db3
Fix martian concurrency controls and dedupe issues mcp servers 2025-10-24 15:55:32 -05:00
Jeremiah Lowin
980d0516a7
Expose OAuth token management parameters in all providers (#2222)
All OAuth providers and OIDCProxy now expose jwt_signing_key,
token_encryption_key, and client_storage parameters for production
deployments requiring persistent token management across server restarts.
2025-10-22 21:51:37 -04:00
Jeremiah Lowin
562e51b378
Improve OAuth error messages with custom handlers and middleware (#2221)
* Enhance OAuth Proxy error responses with branded HTML pages

OAuth Proxy authentication errors now show styled HTML error pages in browsers
instead of raw JSON, with content negotiation for API clients. Enhanced error
messages explain common causes (ephemeral storage, server restarts) and provide
clear remediation steps.

Changes:
- Created enhanced authorization handler that extends SDK's AuthorizationHandler
- Created enhanced auth middleware that extends SDK's RequireAuthMiddleware
- HTML error pages use server branding (icon, name) from FastMCP instance
- Added comprehensive troubleshooting section to OAuth Proxy docs
- Added FAQ entry linking to detailed troubleshooting

* Add comprehensive tests for enhanced OAuth error responses

Tests cover:
- HTML error pages for browser requests with server branding
- Enhanced JSON responses with registration endpoint hints
- Content negotiation between HTML and JSON
- Enhanced middleware error messages for invalid_token
- WWW-Authenticate header format consistency with SDK

* Update language for new storage defaults

* update docs

* Update tests for simplified error messages

* Clean up messages
2025-10-22 21:29:14 -04:00
Jeremiah Lowin
e3d1750412
Update CLI logo (#2220) 2025-10-22 21:29:04 -04:00
Jeremiah Lowin
686082a5b5
Add platform-aware OAuth token persistence (#2218)
* Add comprehensive keyring integration tests

Prevents OS keyring pollution during testing by adding a global mock in
conftest.py. Tests verify keyring behavior across platforms and fallback
scenarios without writing to the actual system keyring.

- Add global mock_keyring fixture to tests/conftest.py
- Add TestOAuthProxyKeyring class with 6 keyring-specific tests
- Remove try/except ImportError for keyring (now required dependency)
- Add keyring extra to py-key-value-aio dependency
- Clean up extraneous implementation comments in oauth_proxy.py

* Update OAuth keyring documentation

Update all OAuth-related documentation to reflect keyring-based key management:
- Add version badges to jwt_signing_key, token_encryption_key, and client_storage parameters
- Standardize "Default behavior (`None`):" formatting with backticks
- Ensure consistent messaging about development-only defaults across all docs
- Update oauth-proxy.mdx, oidc-proxy.mdx, http.mdx, storage-backends.mdx, and upgrade-guide.mdx
2025-10-22 20:42:24 -04:00
William Easton
df7a485869
Merge pull request #2217 from jlowin/claude/issue-2216-20251022-2311
Fix dead links to py-key-value repository
2025-10-22 19:09:35 -05:00
claude[bot]
f127e33bbe Fix dead links to py-key-value repository
Update documentation links from py-key-value-aio to py-key-value repository.
The py-key-value-aio package lives in the py-key-value monorepo.

Co-authored-by: William Easton <strawgate@users.noreply.github.com>
2025-10-22 23:15:03 +00:00
Jeremiah Lowin
8099201979
Use platformdirs for settings.home (#2213)
Changes settings.home from `Path.home() / ".fastmcp"` to use platformdirs.user_data_dir(), following platform conventions (~/Library/Application Support on macOS, ~/.local/share on Linux, %APPDATA% on Windows).
2025-10-22 16:06:46 -04:00
1625 changed files with 296806 additions and 73168 deletions

26
.claude/hooks/session-init.sh Executable file
View file

@ -0,0 +1,26 @@
#!/bin/bash
set -e
# Only run in remote/cloud environments
if [ "$CLAUDE_CODE_REMOTE" != "true" ]; then
exit 0
fi
command -v gh &> /dev/null && exit 0
LOCAL_BIN="$HOME/.local/bin"
mkdir -p "$LOCAL_BIN"
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
VERSION=$(curl -fsSL https://api.github.com/repos/cli/cli/releases/latest | grep '"tag_name"' | cut -d'"' -f4)
TARBALL="gh_${VERSION#v}_linux_${ARCH}.tar.gz"
echo "Installing gh ${VERSION}..."
TEMP=$(mktemp -d)
trap 'rm -rf "$TEMP"' EXIT
curl -fsSL "https://github.com/cli/cli/releases/download/${VERSION}/${TARBALL}" | tar -xz -C "$TEMP"
cp "$TEMP"/gh_*/bin/gh "$LOCAL_BIN/gh"
chmod 755 "$LOCAL_BIN/gh"
[ -n "$CLAUDE_ENV_FILE" ] && echo "export PATH=\"$LOCAL_BIN:\$PATH\"" >> "$CLAUDE_ENV_FILE"
echo "gh installed: $("$LOCAL_BIN/gh" --version | head -1)"

15
.claude/settings.json Normal file
View file

@ -0,0 +1,15 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-init.sh",
"timeout": 120
}
]
}
]
}
}

View file

@ -0,0 +1,101 @@
---
name: reviewing-code
description: Review code for quality, maintainability, and correctness. Use when reviewing pull requests, evaluating code changes, or providing feedback on implementations. Focuses on API design, patterns, and actionable feedback.
---
# Code Review
## Philosophy
Code review maintains a healthy codebase while helping contributors succeed. The burden of proof is on the PR to demonstrate it adds value. Your job is to help it get there through actionable feedback.
**Critical**: A perfectly written PR that adds unwanted functionality must still be rejected. The code must advance the codebase in the intended direction. When rejecting, provide clear guidance on how to align with project goals.
Be friendly and welcoming while maintaining high standards. Call out what works well. When code needs improvement, be specific about why and how to fix it.
## What to Focus On
### Does this advance the codebase correctly?
Even perfect code for unwanted features should be rejected.
### Dependency version compatibility
When a PR adapts code to a new version of a dependency (e.g., removing a parameter that was dropped upstream, using a new API):
- **The version pin in `pyproject.toml` must match.** If the change breaks compatibility with the previously-pinned minimum version, the minimum version must be bumped. Otherwise users on the old version get a regression.
- **If backwards compatibility with the old version is desired**, the code must handle both versions (e.g., try/except, version check). Simply deleting the old API usage without bumping the pin is always wrong — it silently breaks users on the old version.
- **Lock file (`uv.lock`) changes should be scoped to the PR's purpose.** A PR fixing a ty compatibility issue should not also include unrelated dependency version bumps (anthropic, google-auth, etc.) from running `uv sync --upgrade`. These create noise and make the diff harder to review.
### API design and naming
Identify confusing patterns or non-idiomatic code:
- Parameter values that contradict defaults
- Mutable default arguments
- Unclear naming that will confuse future readers
- Inconsistent patterns with the rest of the codebase
### Specific improvements
Provide actionable feedback, not generic observations.
### User ergonomics
Think about the API from a user's perspective. Is it intuitive? What's the learning curve?
## For Agent Reviewers
1. **Read the full context**: Examine related files, tests, and documentation before reviewing
2. **Check against established patterns**: Look for consistency with codebase conventions
3. **Verify functionality claims**: Understand what the code actually does, not just what it claims
4. **Consider edge cases**: Think through error conditions and boundary scenarios
## What to Avoid
- Generic feedback without specifics
- Hypothetical problems unlikely to occur
- Nitpicking organizational choices without strong reason
- Summarizing what the PR already describes
- Star ratings or excessive emojis
- Bikeshedding style preferences when functionality is correct
- Requesting changes without suggesting solutions
- Focusing on personal coding style over project conventions
## Tone
- Acknowledge good decisions: "This API design is clean"
- Be direct but respectful
- Explain impact: "This will confuse users because..."
- Remember: Someone else maintains this code forever
## Decision Framework
Before approving, ask:
1. Does this PR achieve its stated purpose?
2. Is that purpose aligned with where the codebase should go?
3. Would I be comfortable maintaining this code?
4. Have I actually understood what it does, not just what it claims?
5. Does this change introduce technical debt?
If something needs work, your review should help it get there through specific, actionable feedback. If it's solving the wrong problem, say so clearly.
## Comment Examples
**Good comments:**
| Instead of | Write |
|------------|-------|
| "Add more tests" | "The `handle_timeout` method needs tests for the edge case where timeout=0" |
| "This API is confusing" | "The parameter name `data` is ambiguous - consider `message_content` to match the MCP specification" |
| "This could be better" | "This approach works but creates a circular dependency. Consider moving the validation to `utils/validators.py`" |
## Checklist
Before approving, verify:
- [ ] All required development workflow steps completed (uv sync, prek, pytest)
- [ ] Changes align with repository patterns and conventions
- [ ] API changes are documented and backwards-compatible where possible
- [ ] Error handling follows project patterns (specific exception types)
- [ ] Tests cover new functionality and edge cases
- [ ] The change advances the codebase in the intended direction

View file

@ -0,0 +1,220 @@
---
name: testing-python
description: Write and evaluate effective Python tests using pytest. Use when writing tests, reviewing test code, debugging test failures, or improving test coverage. Covers test design, fixtures, parameterization, mocking, and async testing.
---
# Writing Effective Python Tests
## Core Principles
Every test should be **atomic**, **self-contained**, and test **single functionality**. A test that tests multiple things is harder to debug and maintain.
## Test Structure
### Atomic unit tests
Each test should verify a single behavior. The test name should tell you what's broken when it fails. Multiple assertions are fine when they all verify the same behavior.
```python
# Good: Name tells you what's broken
def test_user_creation_sets_defaults():
user = User(name="Alice")
assert user.role == "member"
assert user.id is not None
assert user.created_at is not None
# Bad: If this fails, what behavior is broken?
def test_user():
user = User(name="Alice")
assert user.role == "member"
user.promote()
assert user.role == "admin"
assert user.can_delete_others()
```
### Use parameterization for variations of the same concept
```python
import pytest
@pytest.mark.parametrize("input,expected", [
("hello", "HELLO"),
("World", "WORLD"),
("", ""),
("123", "123"),
])
def test_uppercase_conversion(input, expected):
assert input.upper() == expected
```
### Use separate tests for different functionality
Don't parameterize unrelated behaviors. If the test logic differs, write separate tests.
## Project-Specific Rules
### No async markers needed
This project uses `asyncio_mode = "auto"` globally. Write async tests without decorators:
```python
# Correct
async def test_async_operation():
result = await some_async_function()
assert result == expected
# Wrong - don't add this
@pytest.mark.asyncio
async def test_async_operation():
...
```
### Imports at module level
Put ALL imports at the top of the file:
```python
# Correct
import pytest
from fastmcp import FastMCP
from fastmcp.client import Client
async def test_something():
mcp = FastMCP("test")
...
# Wrong - no local imports
async def test_something():
from fastmcp import FastMCP # Don't do this
...
```
### Use in-memory transport for testing
Pass FastMCP servers directly to clients:
```python
from fastmcp import FastMCP
from fastmcp.client import Client
mcp = FastMCP("TestServer")
@mcp.tool
def greet(name: str) -> str:
return f"Hello, {name}!"
async def test_greet_tool():
async with Client(mcp) as client:
result = await client.call_tool("greet", {"name": "World"})
assert result[0].text == "Hello, World!"
```
Only use HTTP transport when explicitly testing network features.
### Inline snapshots for complex data
Use `inline-snapshot` for testing JSON schemas and complex structures:
```python
from inline_snapshot import snapshot
def test_schema_generation():
schema = generate_schema(MyModel)
assert schema == snapshot() # Will auto-populate on first run
```
Commands:
- `pytest --inline-snapshot=create` - populate empty snapshots
- `pytest --inline-snapshot=fix` - update after intentional changes
## Fixtures
### Prefer function-scoped fixtures
```python
@pytest.fixture
def client():
return Client()
async def test_with_client(client):
result = await client.ping()
assert result is not None
```
### Use `tmp_path` for file operations
```python
def test_file_writing(tmp_path):
file = tmp_path / "test.txt"
file.write_text("content")
assert file.read_text() == "content"
```
## Mocking
### Mock at the boundary
```python
from unittest.mock import patch, AsyncMock
async def test_external_api_call():
with patch("mymodule.external_client.fetch", new_callable=AsyncMock) as mock:
mock.return_value = {"data": "test"}
result = await my_function()
assert result == {"data": "test"}
```
### Don't mock what you own
Test your code with real implementations when possible. Mock external services, not internal classes.
## Test Naming
Use descriptive names that explain the scenario:
```python
# Good
def test_login_fails_with_invalid_password():
def test_user_can_update_own_profile():
def test_admin_can_delete_any_user():
# Bad
def test_login():
def test_update():
def test_delete():
```
## Error Testing
```python
import pytest
def test_raises_on_invalid_input():
with pytest.raises(ValueError, match="must be positive"):
calculate(-1)
async def test_async_raises():
with pytest.raises(ConnectionError):
await connect_to_invalid_host()
```
## Running Tests
```bash
uv run pytest -n auto # Run all tests in parallel
uv run pytest -n auto -x # Stop on first failure
uv run pytest path/to/test.py # Run specific file
uv run pytest -k "test_name" # Run tests matching pattern
uv run pytest -m "not integration" # Exclude integration tests
```
## Checklist
Before submitting tests:
- [ ] Each test tests one thing
- [ ] No `@pytest.mark.asyncio` decorators
- [ ] Imports at module level
- [ ] Descriptive test names
- [ ] Using in-memory transport (not HTTP) unless testing networking
- [ ] Parameterization for variations of same behavior
- [ ] Separate tests for different behaviors

View file

@ -0,0 +1,168 @@
---
name: review-issue
description: Review an incoming external issue (and any gated-closed PR behind it) and decide whether to assign the contributor or decline. Use when the maintainer says "look at this issue", "review issue #N", "should we take this", or asks whether to assign someone. Assigning the author auto-reopens their PR for normal review. This is the entry point for incoming-issue triage — distinct from review-pr, which responds to bot reviews on your own open PR.
---
# Triaging contributions under the issue-link gate
FastMCP auto-closes external PRs unless the author is **assigned to a referenced issue**
(see [require-issue-link.yml](../../../.github/workflows/require-issue-link.yml)). The practical
effect: contributors open an issue, open a PR, get auto-closed, and ask to be assigned. The
maintainer almost never sees the PR directly — **the issue is the decision point**, and
**assigning the author is the single action that reopens their PR** and sends it into review.
This skill turns "look at this issue" into one of two outcomes:
- **Assign** — the issue is valid, we want it fixed, an external PR is appropriate, and a sound
PR already exists → assign the author (auto-reopens the PR) and queue it for code review.
- **Decline** — leave the issue/PR closed and explain why on the issue.
Be opinionated about declining. The gate moved spam from junk PRs to junk issues; this skill is
worthless if it just rubber-stamps assignment. Assignment is a commitment to review and likely
merge, not a courtesy.
## How the gate works (the part that matters here)
- External PR is closed unless its body has `Fixes/Closes/Resolves #N` **and** the author is
assigned to issue `#N`.
- **Assigning the author to the issue auto-reopens their closed PR** and re-runs the check —
this is the lever you pull. `gh issue edit N --add-assignee <login>`. The assignment fires a
`require-issue-link` run; expect it to pass. If it fails, the gate itself misbehaved (not the
PR) — investigate the run, don't re-assign.
- Maintainer-authored PRs are exempt. A `trusted-contributor` label exempts a contributor up
front. Reopening the PR or removing the `missing-issue-link` label applies a sticky
`bypass-issue-check`.
- Sibling bots have usually already run on the issue: `marvin-triage-issue` (investigates +
recommends), `marvin-dedupe-issues` / `auto-close-duplicates` (dupes), `auto-close-needs-mre`
(missing MRE). Read their comments before re-deriving anything.
## Step 1 — Orient
Read the issue, its bot triage, and any PR behind it. Run these together:
```bash
gh issue view N --repo PrefectHQ/fastmcp \
--json number,title,state,author,body,labels,assignees,comments
# Find PRs the author opened that reference this issue (they're likely CLOSED):
gh pr list --repo PrefectHQ/fastmcp --state all --search "author:<login> #N in:body" \
--json number,title,state,url,labels
```
If a PR exists, pull its metadata and any review-bot comments (CodeRabbit, Codex). Treat the bot
comments as leads, not conclusions — they often don't run on closed PRs at all, and even when
they do you still owe the PR your own read:
```bash
gh pr view <pr> --repo PrefectHQ/fastmcp --json number,title,body,labels,files,additions,deletions
gh pr view <pr> --repo PrefectHQ/fastmcp --comments
```
## Step 2 — Classify the issue (is it valid AND a real bug?)
- Is there a real, reproducible problem? For bugs, demand an MRE that shows FastMCP misbehaving
— not user config error, not a question, not an upstream-SDK issue.
- Is it a duplicate or already fixed on `main`? Check the dedupe bot's comment and recent commits.
- If the issue itself is weak, **stop here and decline** — don't evaluate the PR. A good PR
attached to a bad issue is still declined.
**A reproducible MRE is not the same as a bug.** This is the trap that produces wrong verdicts:
an MRE can demonstrate real, observable behavior that is nonetheless *not a bug*, because it
violates no contract the framework intends to hold. The decisive question is not "does this
reproduce?" but "does the demonstrated behavior violate the intended contract for this API?" A
shared-mutable-state MRE only matters if callers are *supposed* to mutate that state; an
ordering/timing MRE only matters if the framework promises an order; a "wrong" value only matters
relative to what the API guarantees. An MRE that has to reach past the supported surface to
trigger the behavior (mutating a field meant to be set only at construction, depending on an
internal that isn't part of the public contract) is showing you a property, not a defect.
You usually cannot read the intended contract off the code — the code shows what it *does*, not
what it *promises*. **The maintainer is often the only authoritative source for the contract, so
stopping to ask is legitimate and expected here.** Ask "is X a supported pattern / does this API
promise Y?" before sinking time into investigating a fix. If the behavior is in-contract correct,
decline — no matter how cleanly the PR fixes it, and no matter how real the MRE looks.
## Step 3 — Investigate the PR (mandatory; do NOT skip if a PR exists)
The most common failure of this skill is judging a PR from the diff hunk and the PR description
alone. That is a cursory review and it produces wrong verdicts — a redundant-looking conditional
can be a real bug fix; a tidy-looking diff can patch the wrong layer. **You cannot assess a PR
without reading the code it changes in context.** Reading `gh pr diff` is necessary but never
sufficient.
Do all of this before forming any opinion on quality:
1. **Read the diff in full**, then **open every file it touches in the repo** (`Read`, not just
the patch). The hunk shows *what changed*; the file shows *what it changed into*.
2. **Trace the functions and values the change depends on.** Grep for the called functions,
the fields being set, and the defaults. If the PR overrides or replaces a value, find what
produced the original value and what consumes it downstream.
3. **Establish the actual root cause from the issue's MRE**, then check whether the change fixes
*that* — at the layer where the bug originates, not a compensating patch elsewhere.
4. **Check consistency with adjacent code.** Does the new value/behavior match how nearby code
already handles the same case? An inconsistency is a real finding; a match is evidence the fix
is correct.
5. **Run or read the tests** the PR adds/changes — do they actually exercise the bug, and would
they fail without the fix?
Write down, for yourself, a one-line answer to: *what was broken, where, and does this change fix
it there?* If you can't answer from evidence you've actually read, you haven't investigated yet.
Then separate findings by severity: a **cosmetic** nit (style, a redundant-but-harmless line) is a
review comment, not a blocker. A **substantive** defect (wrong layer, breaks an adjacent path,
doesn't actually fix the MRE) changes the verdict. Don't let a cosmetic nit read as a reason to
decline, and don't let a clean style read as evidence of correctness.
## Step 4 — Decide if an external PR is appropriate (CONTRIBUTING.md)
This is the gate CONTRIBUTING.md actually enforces. Map the change to a category:
- **Simple, well-scoped bug fix** → external PR welcome. Assignable.
- **Docs / typo / example fix** → welcome. Assignable.
- **Auth provider** → assignable (auth is the one integration exception).
- **Enhancement / feature** → needs a maintainer-approved design proposal *in the issue first*.
Do **not** assign just because code exists. If the proposal is sound, the path is "approve the
approach in the issue, then assign" — not "assign because they were fast."
- **Third-party integration** (middleware, provider adapters, non-auth) → decline; belongs in a
separate package.
- **Sweeping / multi-subsystem change with no prior discussion** → decline.
Combine the category with the Step 3 investigation: does it fix the cause or paper over a symptom?
Does it read like unedited LLM output (verbose body, speculative/shotgun changes)? CONTRIBUTING.md
says we close those — a closed PR that reads that way is staying closed.
## Step 5 — Recommend, then act
Present a short verdict to the maintainer before mutating anything: **assign** or **decline**,
one or two sentences of reasoning, and the exact command you'll run. Wait for confirmation on
borderline calls; for clear-cut ones you may proceed and report.
**Assign** (valid issue + appropriate external contribution + sound PR exists):
```bash
gh issue edit N --repo PrefectHQ/fastmcp --add-assignee <login>
```
That reopens the PR automatically. Then hand off to code review — invoke the `code-review` /
`review-pr` skills on the reopened PR. Assignment is not approval; the code still gets the normal
pass.
If a PR's head branch was deleted, assignment can't reopen it — the workflow comments asking the
author to open a fresh PR. Don't try to force it.
**Decline** (invalid issue, wrong contribution type, or low-quality PR): leave it closed and
comment on the **issue** explaining the decision, pointing to the relevant CONTRIBUTING.md
section. Per repo rules, use `--body-file`, never inline `--body`, for any comment that could
contain `$`, backticks, or code:
```bash
gh issue comment N --repo PrefectHQ/fastmcp --body-file /tmp/triage-reply.md
```
Keep the reply short and point to the relevant CONTRIBUTING.md section. (If a `github-reply`
skill is available for maintainer voice/tone, use it — but it isn't required.)
## What this skill does NOT do
- It doesn't bypass the gate via `trusted-contributor` / `bypass-issue-check` — that's a
deliberate maintainer escalation, not a triage outcome.
- It doesn't merge. Assignment → reopen → review → (maybe) merge are distinct steps.
- It doesn't re-run the first-pass triage the bots already did; read their output instead.

View file

@ -0,0 +1,108 @@
---
name: review-pr
description: Monitor and respond to automated PR reviews (Codex bot). Use when pushing a PR, checking review status, or responding to bot feedback. Handles the full cycle of push -> wait for review -> evaluate comments -> fix -> re-push.
---
# PR Review Workflow
This repo has `chatgpt-codex-connector[bot]` configured as an automated reviewer. After every push to a PR branch, Codex reviews the diff and either:
- Reacts with a thumbs-up on its review body (no suggestions — PR is clean)
- Posts inline comments with suggestions (each tagged with a priority badge)
## Checking review status
After pushing, check whether Codex has reviewed the latest commit:
```bash
# Get the latest commit SHA on the branch
LATEST=$(git rev-parse HEAD)
# Check if Codex has reviewed that specific commit
gh api repos/PrefectHQ/fastmcp/pulls/{PR_NUMBER}/reviews \
| jq "[.[] | select(.user.login == \"chatgpt-codex-connector[bot]\" and .commit_id == \"$LATEST\")] | length"
```
If the count is 0, Codex hasn't reviewed the latest push yet. Wait and check again.
If the count is > 0, check for inline comments on the latest review:
```bash
# Get the review body to check for thumbs-up
gh api repos/PrefectHQ/fastmcp/pulls/{PR_NUMBER}/reviews \
| jq '[.[] | select(.user.login == "chatgpt-codex-connector[bot]") | {state, body: .body[:300], commit_id: .commit_id}] | last'
```
A clean review from Codex looks like a review body that contains a thumbs-up reaction or says "no suggestions." If the body contains "Here are some automated review suggestions," there are inline comments to evaluate.
## Evaluating Codex comments
Fetch all inline comments from Codex:
```bash
gh api repos/PrefectHQ/fastmcp/pulls/{PR_NUMBER}/comments \
| jq '[.[] | select(.user.login == "chatgpt-codex-connector[bot]") | {body, path, line, created_at}]'
```
Codex comments include priority badges:
- `P0` (red) — Critical issue, likely a real bug
- `P1` (orange) — Important, worth fixing
- `P2` (yellow) — Moderate, evaluate on merit
**How to evaluate Codex comments:**
1. **Treat Codex as a competent but sometimes overzealous reviewer.** It catches real bugs (cache eviction ordering, silent data loss, missing validation) but also suggests scope expansions and hypothetical improvements.
2. **Fix real bugs** — issues in code you actually changed where behavior is incorrect or data is silently lost.
3. **Dismiss scope expansion** — if a comment points out a pre-existing limitation unrelated to your diff, note it as a potential follow-up but don't block the PR.
4. **Dismiss speculative concerns** — if a comment describes a scenario that requires very specific conditions and the existing behavior is acceptable, dismiss it.
5. **When fixing, be proactive** — if Codex found one instance of a pattern bug (e.g., missing role validation in one handler), check all similar code paths before pushing. Codex will find the next instance on the next review cycle, so get ahead of it.
## Responding to every comment
**Every Codex comment must get a visible response** — either a fix or a reply explaining why it was dismissed. The maintainer can't see your reasoning otherwise.
- **If fixing**: The fix itself is the response. No reply needed unless the fix is non-obvious.
- **If dismissing**: Reply to the comment thread with a brief explanation of why. Keep it to 1-2 sentences. Examples:
- "This is pre-existing behavior unrelated to this diff — the scope lookup fallback existed before caching was added. Worth a follow-up issue but not blocking this PR."
- "The AsyncExitStack handles cleanup when the session exits, so the subprocess isn't leaked — just kept alive slightly longer than necessary in this edge case."
- "Gemini supports a much wider range of media types than OpenAI/Anthropic, so a restrictive allowlist would be inaccurate here."
Use `gh api` to reply (note: use `in_reply_to`, not a `/replies` sub-path):
```bash
# Reply to a specific review comment
gh api repos/PrefectHQ/fastmcp/pulls/{PR_NUMBER}/comments \
-f body="Your reply here" \
-F in_reply_to={COMMENT_ID}
```
## The fix-push-review cycle
After evaluating comments:
1. Fix all real issues in one batch
2. Reply to all dismissed comments with reasoning
3. Think about what patterns Codex might flag next — check similar code paths proactively
4. Commit and push
5. Check that Codex reviews the new commit
6. Repeat until Codex gives a clean review (thumbs-up) or only has dismissible comments
## Responding to stale comments
Codex sometimes re-posts old comments that reference code you've already fixed (they appear on the old commit's diff). These are stale — verify the fix is in the latest commit and reply noting the fix is already in place.
## Labels — never apply or invent them
**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.
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
A PR is ready for human review when:
- All Codex comments are either fixed or replied to with dismissal reasoning
- CI checks pass
- The diff is clean and focused on the stated purpose

3
.coderabbit.yaml Normal file
View file

@ -0,0 +1,3 @@
reviews:
path_filters:
- "!docs/python-sdk/**"

View file

@ -10,4 +10,4 @@ There are four major MCP object types:
- Resource Templates (src/resources/)
- Prompts (src/prompts)
While these have slightly different semantics and implementations, in general changes that affect interactions with any one (like adding tags, importing, etc.) will need to be adopted, applied, and tested on all others. Be sure to look at not only the object definition but also the related `Manager` (e.g. `ToolManager`, `ResourceManager`, and `PromptManager`). Also note that while resources and resource templates are different objects, they both are handled by the `ResourceManager`.
While these have slightly different semantics and implementations, in general changes that affect interactions with any one (like adding tags, importing, etc.) will need to be adopted, applied, and tested on all others. Note that while resources and resource templates are different objects, they are both in `src/resources/`.

View file

@ -1,6 +0,0 @@
{
"setup-worktree": [
"uv sync",
"uv run pre-commit install"
]
}

View file

@ -3,31 +3,30 @@ description: Report a bug or unexpected behavior in FastMCP
labels: [bug, pending]
body:
- type: markdown
attributes:
value: Thanks for contributing to FastMCP! 🙏
- type: markdown
attributes:
value: |
Thanks for reporting a bug!
A good bug report is one of the most valuable contributions you can make — see [CONTRIBUTING.md](../../CONTRIBUTING.md). If the fix is straightforward, a PR is also welcome.
### Before you submit
To help us help you, please:
- 🔄 **Make sure you're testing on the latest version of FastMCP** - many issues are already fixed in newer versions
- 🔍 **Check if someone else has already reported this issue** or if it's been fixed on the main branch
- 📋 **You MUST include a copy/pasteable and properly formatted MRE** (minimal reproducible example) below or your issue may be closed without response
Thanks for helping to make FastMCP better! 🚀
- Make sure you're testing on the **latest version** of FastMCP — many issues are already fixed in newer releases
- Check if someone else has **already reported this** or if it's been fixed on the main branch
- You **must** include a copy/pasteable, properly formatted MRE (minimal reproducible example) or your issue may be closed without response
- **The ideal issue is a clear problem description and an MRE — that's it.** If you've done genuine investigation and have a non-obvious insight into the root cause, include it. But please don't speculate or ask an LLM to generate a diagnosis. We have LLMs too, and an incorrect analysis is harder to work with than none at all.
- **Keep it short.** A clear description plus a concise MRE is ideal — aim to fit in a single screen. Issues that include unsolicited root cause analysis, proposed fixes, or multi-section diagnostic writeups will be labeled `too-long` and not triaged until condensed.
- **Using an LLM?** Great — but it must follow these guidelines. Generic LLM output that ignores our contributing conventions will be closed. See [CONTRIBUTING.md](../../CONTRIBUTING.md).
- type: textarea
id: description
attributes:
label: Description
label: What happened?
description: |
Please explain what you're experiencing and what you would expect to happen instead.
Describe the bug in a few sentences. What did you do, what happened, and what did you expect instead?
Provide as much detail as possible to help us understand and solve your problem quickly.
Do NOT include root cause analysis, proposed fixes, or diagnostic writeups — just describe the problem.
validations:
required: true

View file

@ -3,32 +3,27 @@ description: Suggest an idea or improvement for FastMCP
labels: [enhancement, pending]
body:
- type: markdown
attributes:
value: Thanks for contributing to FastMCP! 🙏
- type: markdown
attributes:
value: |
Thanks for suggesting an improvement to FastMCP!
Enhancement issues are the **primary way** features and improvements get into FastMCP. Maintainers use well-written issues to implement changes that fit the codebase's patterns and ship quickly. A clear issue here is more impactful than a PR — see [CONTRIBUTING.md](../../CONTRIBUTING.md) for why.
### Before you submit
To help us evaluate your enhancement request:
- 🔍 **Check if this has already been requested** - search existing issues first
- 💭 **Think about the broader impact** - how would this affect other users?
- 📋 **Consider implementation complexity** - is this a small change or a major feature?
Thanks for helping to make FastMCP better! 🚀
- 🔍 **Check if this has already been requested** — search existing issues first
- 🎯 **Describe the problem you're trying to solve**, not the solution you want — we'll figure out the best implementation
- ✂️ **Keep it short.** A motivating description and a concrete use case is the ideal request — aim to fit in a single screen. Skip proposed implementations, API designs, or multi-option analyses — maintainers will figure out the approach. Requests that are difficult to parse will be labeled `too-long` and not triaged until condensed.
- 🤖 **Using an LLM?** Great — but it must follow these guidelines. Generic LLM output that ignores our contributing conventions will be closed. See [CONTRIBUTING.md](../../CONTRIBUTING.md).
- type: textarea
id: description
attributes:
label: Enhancement
description: |
Please describe the enhancement:
What problem or use case does this solve? How does current behavior fall short?
- What problem or use case would it solve?
- How would it improve your workflow or experience with FastMCP?
- Are there any alternative solutions you've considered?
Focus on the *what* and *why* — the motivating scenario. You don't need to propose an API or implementation.
validations:
required: true

100
.github/actions/run-claude/action.yml vendored Normal file
View file

@ -0,0 +1,100 @@
# Composite Action for running Claude Code Action
#
# Wraps anthropics/claude-code-action with MCP server configuration.
# Template based on elastic/ai-github-actions base action.
#
# Usage:
# - uses: ./.github/actions/run-claude
# with:
# prompt: "Your prompt here"
# claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# github-token: ${{ steps.marvin-token.outputs.token }}
# allowed-tools: "Edit,Read,Write,Bash(*),mcp__github__add_issue_comment"
#
name: "Run Claude"
description: "Run Claude Code with MCP servers"
author: "FastMCP"
branding:
icon: "cpu"
color: "orange"
inputs:
prompt:
description: "Prompt to pass to Claude"
required: true
claude-oauth-token:
description: "Claude Code OAuth token for authentication"
required: true
github-token:
description: "GitHub token for Claude to operate with"
required: true
allowed-tools:
description: "Comma-separated list of allowed tools (e.g. Edit,Write,Bash(npm test))"
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
default: "claude-opus-4-8"
allowed-bots:
description: "Allowed bot usernames, or '*' for all bots"
required: false
default: ""
track-progress:
description: "Whether Claude should track progress"
required: false
default: "true"
mcp-servers:
description: "MCP server configuration JSON"
required: false
default: '{"mcpServers":{"agents-md-generator":{"type":"http","url":"https://agents-md-generator.fastmcp.app/mcp"},"public-code-search":{"type":"http","url":"https://public-code-search.fastmcp.app/mcp"}}}'
trigger-phrase:
description: "Trigger phrase (for mention workflows)"
required: false
default: "/marvin"
outputs:
conclusion:
description: "The conclusion of the Claude Code run"
value: ${{ steps.claude.outputs.conclusion }}
runs:
using: "composite"
steps:
- name: Clean up stale Claude locks
shell: bash
run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true
- name: Run Claude Code
id: claude
env:
GITHUB_TOKEN: ${{ inputs.github-token }}
uses: anthropics/claude-code-action@v1
with:
github_token: ${{ inputs.github-token }}
claude_code_oauth_token: ${{ inputs.claude-oauth-token }}
bot_name: "Marvin Context Protocol"
trigger_phrase: ${{ inputs.trigger-phrase }}
allowed_bots: ${{ inputs.allowed-bots }}
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.mcp-servers != '' && format('--mcp-config ''{0}''', inputs.mcp-servers) || '' }}
--model ${{ inputs.model }}
settings: |
{"model": "${{ inputs.model }}"}

66
.github/actions/run-pytest/action.yml vendored Normal file
View file

@ -0,0 +1,66 @@
name: "Run Pytest"
description: "Run pytest with appropriate flags for the test type and platform"
inputs:
test-type:
description: "Type of tests to run: unit, integration, client_process, or conformance"
required: false
default: "unit"
runs:
using: "composite"
steps:
- name: Run pytest
shell: bash
run: |
if [ "${{ inputs.test-type }}" == "integration" ]; then
MARKER="integration"
TIMEOUT="30"
MAX_PROCS="2"
EXTRA_FLAGS=""
elif [ "${{ inputs.test-type }}" == "client_process" ]; then
MARKER="client_process or subprocess_heavy"
TIMEOUT="5"
MAX_PROCS="0"
EXTRA_FLAGS="-x"
elif [ "${{ inputs.test-type }}" == "conformance" ]; then
MARKER="conformance"
TIMEOUT="120"
MAX_PROCS="0"
EXTRA_FLAGS="-x"
else
MARKER="not integration and not client_process and not subprocess_heavy and not conformance"
TIMEOUT="5"
MAX_PROCS="4"
EXTRA_FLAGS=""
fi
# Windows previously ran serially: parallel workers crashed intermittently
# when many tests spawned stdio subprocesses (#2715, reverted in #2726).
# Most of those tests now run in-memory, but tests that spawn a fresh
# interpreter importing all of FastMCP still crash xdist workers on the
# 2-core Windows runners. They carry the subprocess_heavy marker and run
# in the serial client_process step instead.
PARALLEL_FLAGS=""
if [ "$MAX_PROCS" != "0" ]; then
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 \
--durations=50 \
-m "$MARKER" \
$PARALLEL_FLAGS \
$EXTRA_FLAGS \
tests

33
.github/actions/setup-uv/action.yml vendored Normal file
View file

@ -0,0 +1,33 @@
name: "Setup UV Environment"
description: "Install uv and dependencies (requires checkout first)"
inputs:
python-version:
description: "Python version to use"
required: false
default: "3.10"
resolution:
description: "Dependency resolution: locked, upgrade, or lowest-direct"
required: false
default: "locked"
runs:
using: "composite"
steps:
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: ${{ inputs.python-version }}
- name: Install dependencies
shell: bash
run: |
if [ "${{ inputs.resolution }}" == "locked" ]; then
uv sync --locked
elif [ "${{ inputs.resolution }}" == "upgrade" ]; then
uv sync --upgrade
else
uv sync --resolution ${{ inputs.resolution }}
fi

View file

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

View file

@ -1,28 +1,22 @@
## Description
<!--
Please provide a clear and concise description of the changes made in this pull request.
Using AI to generate code? Please include a note in the description with which AI tool you used.
-->
<!-- What does this PR do? Link to the issue it addresses. -->
**Contributors Checklist**
<!--
NOTE:
1. You must create an issue in the repository before making a Pull Request.
2. You must not create a Pull Request for an issue that is already assigned to someone else.
Closes #
If you do not follow these steps, your Pull Request will be closed without review.
-->
## Contribution type
- [ ] My change closes #(issue number)
- [ ] I have followed the repository's development workflow
- [ ] I have tested my changes manually and by adding relevant tests
- [ ] I have performed all required documentation updates
<!-- Check the one that applies. If you're unsure whether your change is welcome, please open an issue first — see CONTRIBUTING.md. -->
**Review Checklist**
<!-- Your Pull Request will not be reviewed if tests are failing, you have not self-reviewed your changes, or you have not checked all of the following: -->
- [ ] Bug fix (simple, well-scoped fix for a clearly broken behavior)
- [ ] Documentation improvement
- [ ] Enhancement (maintainers typically implement enhancements — see [CONTRIBUTING.md](../CONTRIBUTING.md))
## Checklist
- [ ] This PR addresses an existing issue (or fixes a self-evident bug)
- [ ] I have read [CONTRIBUTING.md](../CONTRIBUTING.md)
- [ ] I have added tests that cover my changes
- [ ] I have run `uv run prek run --all-files` and all checks pass
- [ ] I have self-reviewed my changes
- [ ] My Pull Request is ready for review
---
- [ ] If I used an LLM, it followed the repo's contributing conventions (not generic output)

26
.github/release.yml vendored
View file

@ -8,12 +8,25 @@ changelog:
labels:
- feature
- title: Enhancements 🔧
- title: Breaking Changes ⚠️
labels:
- breaking change
exclude:
labels:
- contrib
- security
- title: Enhancements ✨
labels:
- enhancement
exclude:
labels:
- breaking change
- security
- title: Security 🔒
labels:
- security
- title: Fixes 🐞
labels:
@ -21,13 +34,7 @@ changelog:
exclude:
labels:
- contrib
- title: Breaking Changes 🛫
labels:
- breaking change
exclude:
labels:
- contrib
- security
- title: Docs 📚
labels:
@ -41,6 +48,9 @@ changelog:
- title: Dependencies 📦
labels:
- dependencies
exclude:
labels:
- security
- title: Other Changes 🦾
labels:

View file

@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -euo pipefail
# Get PR review threads with comments via GitHub GraphQL API
#
# Usage:
# gh-get-review-threads.sh [FILTER]
#
# Arguments:
# FILTER - Optional: filter for unresolved threads from specific author
#
# Environment (set by composite action):
# MENTION_REPO - Repository (owner/repo format)
# MENTION_PR_NUMBER - Pull request number
# GITHUB_TOKEN - GitHub API token
#
# Output:
# JSON array of review threads with nested comments
# Parse OWNER and REPO from MENTION_REPO
REPO_FULL="${MENTION_REPO:?MENTION_REPO environment variable is required}"
OWNER="${REPO_FULL%/*}"
REPO="${REPO_FULL#*/}"
PR_NUMBER="${MENTION_PR_NUMBER:?MENTION_PR_NUMBER environment variable is required}"
FILTER="${1:-}"
gh api graphql -f query='
query($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
reviewThreads(first: 100) {
nodes {
id
isResolved
isOutdated
path
line
comments(first: 50) {
nodes {
id
body
author { login }
createdAt
}
}
}
}
}
}
}' -F owner="$OWNER" \
-F repo="$REPO" \
-F prNumber="$PR_NUMBER" \
--jq '.data.repository.pullRequest.reviewThreads.nodes' | \
if [ -n "$FILTER" ]; then
jq --arg author "$FILTER" '
map(select(
.isResolved == false and
.comments.nodes | any(.author.login == $author)
))'
else
cat
fi

View file

@ -0,0 +1,61 @@
#!/usr/bin/env bash
set -euo pipefail
# Resolve a GitHub PR review thread, optionally posting a comment first
#
# Usage:
# gh-resolve-review-thread.sh THREAD_ID [COMMENT]
#
# Arguments:
# THREAD_ID - The GraphQL node ID of the review thread to resolve
# COMMENT - Optional: Comment body to post before resolving
#
# Environment (set by composite action):
# MENTION_REPO - Repository (owner/repo format)
# MENTION_PR_NUMBER - Pull request number
# GITHUB_TOKEN - GitHub API token
#
# Behavior:
# 1. If COMMENT is provided, posts it as a reply to the thread
# 2. Resolves the thread
# Validate required environment variables
: "${MENTION_REPO:?MENTION_REPO environment variable is required}"
: "${MENTION_PR_NUMBER:?MENTION_PR_NUMBER environment variable is required}"
THREAD_ID="${1:?Thread ID required}"
COMMENT="${2:-}"
# Step 1: Post comment if provided
if [ -n "$COMMENT" ]; then
echo "Posting comment to thread..." >&2
COMMENT_RESULT=$(gh api graphql -f query='
mutation($threadId: ID!, $body: String!) {
addPullRequestReviewThreadReply(input: {
pullRequestReviewThreadId: $threadId,
body: $body
}) {
comment {
id
}
}
}' -f threadId="$THREAD_ID" -f body="$COMMENT")
if echo "$COMMENT_RESULT" | jq -e '.errors' > /dev/null 2>&1; then
echo "Error posting comment: $COMMENT_RESULT" >&2
exit 1
fi
fi
# Step 2: Resolve the thread
echo "Resolving thread..." >&2
RESOLVE_RESULT=$(gh api graphql -f query='
mutation($threadId: ID!) {
resolveReviewThread(input: {threadId: $threadId}) {
thread {
id
isResolved
}
}
}' -f threadId="$THREAD_ID" --jq '.data.resolveReviewThread.thread')
echo "$RESOLVE_RESULT"
echo "✓ Thread resolved" >&2

251
.github/scripts/pr-review/pr-comment.sh vendored Executable file
View file

@ -0,0 +1,251 @@
#!/bin/bash
# pr-comment.sh - Queue a structured inline review comment for the PR review
#
# Usage:
# pr-comment.sh <file> <line> --severity <level> --title <description> --why <reason> [suggestion via stdin]
# pr-comment.sh <file> <line> --severity <level> --title <description> --why <reason> --no-suggestion
#
# Arguments:
# file File path (required)
# line Line number (required)
# --severity Severity level: critical, high, medium, low, nitpick (required)
# --title Brief description for comment heading (required)
# --why One sentence explaining the risk/impact (required)
# --no-suggestion Explicitly skip suggestion (use for architectural issues)
#
# The suggestion code is read from stdin (use heredoc). If no stdin and no --no-suggestion, errors.
#
# Examples:
# # With suggestion (preferred)
# pr-comment.sh src/main.go 42 --severity high --title "Missing error check" --why "Errors are silently ignored" <<'EOF'
# if err != nil {
# return fmt.Errorf("operation failed: %w", err)
# }
# EOF
#
# # Without suggestion (for issues requiring broader changes)
# pr-comment.sh src/main.go 42 --severity medium --title "Consider extracting to function" \
# --why "This logic is duplicated in 3 places" --no-suggestion
#
# Environment variables (set by the composite action):
# PR_REVIEW_REPO - Repository (owner/repo)
# PR_REVIEW_PR_NUMBER - Pull request number
# PR_REVIEW_COMMENTS_DIR - Directory to cache comments (default: /tmp/pr-review-comments)
set -e
# Configuration from environment
REPO="${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}"
PR_NUMBER="${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}"
COMMENTS_DIR="${PR_REVIEW_COMMENTS_DIR:-/tmp/pr-review-comments}"
# Severity emoji mapping
declare -A SEVERITY_EMOJI=(
[critical]="🔴 CRITICAL"
[high]="🟠 HIGH"
[medium]="🟡 MEDIUM"
[low]="⚪ LOW"
[nitpick]="💬 NITPICK"
)
# Parse arguments
FILE=""
LINE=""
SEVERITY=""
TITLE=""
WHY=""
NO_SUGGESTION=false
# First two positional args are file and line
if [ $# -lt 2 ]; then
echo "Error: file and line are required"
echo "Usage: pr-comment.sh <file> <line> --severity <level> --title <desc> --why <reason> [<<'EOF' ... EOF]"
exit 1
fi
FILE="$1"
LINE="$2"
shift 2
# Parse named arguments
while [ $# -gt 0 ]; do
case "$1" in
--severity)
SEVERITY="$2"
shift 2
;;
--title)
TITLE="$2"
shift 2
;;
--why)
WHY="$2"
shift 2
;;
--no-suggestion)
NO_SUGGESTION=true
shift
;;
*)
echo "Error: Unknown argument: $1"
exit 1
;;
esac
done
# Read suggestion from stdin if available
SUGGESTION=""
if [ ! -t 0 ]; then
SUGGESTION=$(cat)
fi
# Validate required arguments
if [ -z "$SEVERITY" ]; then
echo "Error: --severity is required (critical, high, medium, low, nitpick)"
exit 1
fi
if [ -z "$TITLE" ]; then
echo "Error: --title is required"
exit 1
fi
if [ -z "$WHY" ]; then
echo "Error: --why is required"
exit 1
fi
# Validate severity level
if [ -z "${SEVERITY_EMOJI[$SEVERITY]}" ]; then
echo "Error: Invalid severity '$SEVERITY'. Must be one of: critical, high, medium, low, nitpick"
exit 1
fi
# Require either suggestion or explicit --no-suggestion
if [ -z "$SUGGESTION" ] && [ "$NO_SUGGESTION" = false ]; then
echo "Error: Suggestion required. Provide code via stdin (heredoc) or use --no-suggestion"
echo ""
echo "Example with suggestion:"
echo " pr-comment.sh file.go 42 --severity high --title \"desc\" --why \"reason\" <<'EOF'"
echo " fixed code here"
echo " EOF"
echo ""
echo "Example without suggestion:"
echo " pr-comment.sh file.go 42 --severity medium --title \"desc\" --why \"reason\" --no-suggestion"
exit 1
fi
# Validate line is a positive integer (>= 1)
if ! [[ "$LINE" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: Line number must be a positive integer (>= 1), got: $LINE"
exit 1
fi
# Get the diff for this file to validate the comment location
DIFF_DATA=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate | jq --arg f "$FILE" '.[] | select(.filename==$f)')
if [ -z "$DIFF_DATA" ]; then
echo "Error: File '${FILE}' not found in PR diff"
echo ""
echo "Files changed in this PR:"
gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate --jq '.[].filename'
exit 1
fi
PATCH=$(echo "$DIFF_DATA" | jq -r '.patch // empty')
if [ -z "$PATCH" ]; then
echo "Error: No patch data for file '${FILE}' (file may be binary or too large)"
exit 1
fi
# Verify the line exists in the diff
LINE_IN_DIFF=$(echo "$PATCH" | awk -v target_line="$LINE" '
BEGIN { current_line = 0; found = 0 }
/^@@/ {
line = $0
gsub(/.*\+/, "", line)
gsub(/[^0-9].*/, "", line)
current_line = line - 1
next
}
{
if (substr($0, 1, 1) != "-") {
current_line++
if (current_line == target_line) {
found = 1
exit
}
}
}
END { if (found) print "1"; else print "0" }
')
if [ "$LINE_IN_DIFF" != "1" ]; then
echo "Error: Line ${LINE} not found in the diff for '${FILE}'"
echo ""
echo "Note: You can only comment on lines that appear in the diff (added, modified, or context lines)"
echo ""
echo "First 50 lines of diff for this file:"
echo "$PATCH" | head -50
exit 1
fi
# Create comments directory if it doesn't exist
mkdir -p "${COMMENTS_DIR}"
# Assemble the comment body
SEVERITY_LABEL="${SEVERITY_EMOJI[$SEVERITY]}"
BODY="**${SEVERITY_LABEL}** ${TITLE}
Why: ${WHY}"
# Add suggestion block if provided
if [ -n "$SUGGESTION" ]; then
BODY="${BODY}
\`\`\`suggestion
${SUGGESTION}
\`\`\`"
fi
# Append standard footer
FOOTER='
---
Marvin Context Protocol | Type `/marvin` to interact further
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.'
BODY_WITH_FOOTER="${BODY}${FOOTER}"
# Generate unique comment ID
COMMENT_ID="comment-$(date +%s)-$(od -An -N4 -tu4 /dev/urandom | tr -d ' ')"
COMMENT_FILE="${COMMENTS_DIR}/${COMMENT_ID}.json"
# Create the comment JSON object
jq -n \
--arg path "$FILE" \
--argjson line "$LINE" \
--arg side "RIGHT" \
--arg body "$BODY_WITH_FOOTER" \
--arg id "$COMMENT_ID" \
'{
path: $path,
line: $line,
side: $side,
body: $body,
_meta: {
id: $id,
file: $path,
line: $line
}
}' > "${COMMENT_FILE}"
echo "✓ Queued review comment for ${FILE}:${LINE}"
echo " Severity: ${SEVERITY_LABEL}"
echo " Title: ${TITLE}"
echo " Comment ID: ${COMMENT_ID}"
echo " Comment will be submitted with pr-review.sh"
echo " Remove with: pr-remove-comment.sh ${FILE} ${LINE}"

128
.github/scripts/pr-review/pr-diff.sh vendored Executable file
View file

@ -0,0 +1,128 @@
#!/bin/bash
# pr-diff.sh - Show changed files or diff for a specific file
#
# Usage:
# pr-diff.sh - List all changed files (shows full diff if small enough)
# pr-diff.sh <file> - Show diff for a specific file with line numbers
#
# Environment variables (set by the composite action):
# PR_REVIEW_REPO - Repository (owner/repo)
# PR_REVIEW_PR_NUMBER - Pull request number
set -e
# Configuration from environment
REPO="${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}"
PR_NUMBER="${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}"
EXPECTED_HEAD="${PR_REVIEW_HEAD_SHA:-}"
# Check if HEAD has changed since review started (race condition detection)
if [ -n "$EXPECTED_HEAD" ]; then
CURRENT_HEAD=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha')
if [ "$CURRENT_HEAD" != "$EXPECTED_HEAD" ]; then
echo "⚠️ WARNING: PR head has changed since review started!"
echo " Review started at: ${EXPECTED_HEAD:0:7}"
echo " Current head: ${CURRENT_HEAD:0:7}"
echo " Line numbers below may not match the commit being reviewed."
echo ""
fi
fi
# Thresholds for "too big" - show file list only if exceeded
MAX_FILES=25
MAX_TOTAL_LINES=1500
FILE="$1"
# Function to add line numbers to a patch
# Format: [LINE] +added | [LINE] context | [----] -deleted
add_line_numbers() {
awk '
BEGIN { new_line = 0 }
/^@@/ {
# Parse hunk header: @@ -old_start,old_count +new_start,new_count @@
match($0, /\+([0-9]+)/)
new_line = substr($0, RSTART+1, RLENGTH-1) - 1
print ""
print $0
next
}
/^-/ {
# Deleted line - cannot comment on these
printf "[----] %s\n", $0
next
}
/^\+/ {
# Added line - can comment, show line number
new_line++
printf "[%4d] %s\n", new_line, $0
next
}
{
# Context line (space prefix) - can comment, show line number
new_line++
printf "[%4d] %s\n", new_line, $0
}
'
}
if [ -z "$FILE" ]; then
# Get file list with stats
FILES_DATA=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate)
FILE_COUNT=$(echo "$FILES_DATA" | jq 'length')
TOTAL_ADDITIONS=$(echo "$FILES_DATA" | jq '[.[].additions] | add // 0')
TOTAL_DELETIONS=$(echo "$FILES_DATA" | jq '[.[].deletions] | add // 0')
TOTAL_LINES=$((TOTAL_ADDITIONS + TOTAL_DELETIONS))
echo "PR #${PR_NUMBER} Summary: ${FILE_COUNT} files changed (+${TOTAL_ADDITIONS}/-${TOTAL_DELETIONS})"
echo ""
# Check if diff is too large
if [ "$FILE_COUNT" -gt "$MAX_FILES" ] || [ "$TOTAL_LINES" -gt "$MAX_TOTAL_LINES" ]; then
echo "⚠️ Large diff detected (>${MAX_FILES} files or >${MAX_TOTAL_LINES} lines changed)"
echo " Review files individually using: pr-diff.sh <filename>"
echo ""
echo "Files changed:"
echo "$FILES_DATA" | jq -r '.[] | " \(.filename) (+\(.additions)/-\(.deletions))"'
else
# Small enough - show all diffs with line numbers
echo "Files changed:"
echo "$FILES_DATA" | jq -r '.[] | " \(.filename) (+\(.additions)/-\(.deletions))"'
echo ""
echo "─────────────────────────────────────────────────────────────────────"
echo ""
# Show each file's diff by iterating over indices
for i in $(seq 0 $((FILE_COUNT - 1))); do
FNAME=$(echo "$FILES_DATA" | jq -r ".[$i].filename")
PATCH=$(echo "$FILES_DATA" | jq -r ".[$i].patch // empty")
if [ -n "$PATCH" ]; then
echo "## ${FNAME}"
echo "Use: pr-comment.sh ${FNAME} <LINE> --severity <level> --title \"desc\" --why \"reason\" <<'EOF' ... EOF"
echo "Format: [LINE] +added | [LINE] context | [----] -deleted (can't comment)"
echo "$PATCH" | add_line_numbers
echo ""
echo "─────────────────────────────────────────────────────────────────────"
echo ""
fi
done
fi
else
# Show specific file diff
PATCH=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate --jq --arg file "$FILE" '.[] | select(.filename==$file) | .patch')
if [ -z "$PATCH" ]; then
echo "Error: File '${FILE}' not found in PR diff"
echo ""
echo "Files changed in this PR:"
gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate --jq '.[].filename'
exit 1
fi
echo "## ${FILE}"
echo "Use: pr-comment.sh ${FILE} <LINE> --severity <level> --title \"desc\" --why \"reason\" <<'EOF' ... EOF"
echo "Format: [LINE] +added | [LINE] context | [----] -deleted (can't comment)"
echo "$PATCH" | add_line_numbers
fi

View file

@ -0,0 +1,190 @@
#!/bin/bash
# pr-existing-comments.sh - Fetch existing review threads on a PR
#
# Usage:
# pr-existing-comments.sh - Show all review threads with full details
# pr-existing-comments.sh --summary - Show per-file summary only (for large PRs)
# pr-existing-comments.sh --unresolved - Show only unresolved threads
# pr-existing-comments.sh --file <path> - Show threads for a specific file
# pr-existing-comments.sh --full - Show full comment text (no truncation)
#
# Output: Formatted summary of existing review threads grouped by file,
# showing thread status, comments, and whether issues were addressed.
#
# For large PRs, use --summary first to see the overview, then --file <path>
# to get full thread details when reviewing each file.
#
# Environment variables (set by the composite action):
# PR_REVIEW_REPO - Repository (owner/repo)
# PR_REVIEW_PR_NUMBER - Pull request number
set -e
# Configuration from environment
REPO="${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}"
PR_NUMBER="${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}"
OWNER="${REPO%/*}"
REPO_NAME="${REPO#*/}"
# Parse arguments
FILTER_UNRESOLVED=false
FILTER_FILE=""
SUMMARY_ONLY=false
FULL_TEXT=false
while [ $# -gt 0 ]; do
case "$1" in
--unresolved)
FILTER_UNRESOLVED=true
shift
;;
--file)
FILTER_FILE="$2"
shift 2
;;
--summary)
SUMMARY_ONLY=true
shift
;;
--full)
FULL_TEXT=true
shift
;;
*)
echo "Usage: pr-existing-comments.sh [--summary] [--unresolved] [--file <path>] [--full]"
exit 1
;;
esac
done
# Fetch review threads via GraphQL
THREADS=$(gh api graphql -f query='
query($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
reviewThreads(first: 100) {
nodes {
id
isResolved
isOutdated
path
line
originalLine
startLine
originalStartLine
diffSide
comments(first: 50) {
nodes {
id
body
author { login }
createdAt
originalCommit { abbreviatedOid }
}
}
}
}
}
}
}' -F owner="$OWNER" \
-F repo="$REPO_NAME" \
-F prNumber="$PR_NUMBER" \
--jq '.data.repository.pullRequest.reviewThreads.nodes')
if [ -z "$THREADS" ] || [ "$THREADS" = "null" ]; then
echo "No existing review threads found."
exit 0
fi
# Apply filters
FILTERED="$THREADS"
if [ "$FILTER_UNRESOLVED" = true ]; then
FILTERED=$(echo "$FILTERED" | jq '[.[] | select(.isResolved == false)]')
fi
if [ -n "$FILTER_FILE" ]; then
FILTERED=$(echo "$FILTERED" | jq --arg file "$FILTER_FILE" '[.[] | select(.path == $file)]')
fi
THREAD_COUNT=$(echo "$FILTERED" | jq 'length')
if [ "$THREAD_COUNT" -eq 0 ]; then
if [ "$FILTER_UNRESOLVED" = true ]; then
echo "No unresolved review threads found."
elif [ -n "$FILTER_FILE" ]; then
echo "No review threads found for ${FILTER_FILE}."
else
echo "No existing review threads found."
fi
exit 0
fi
# Count resolved vs unresolved
RESOLVED_COUNT=$(echo "$FILTERED" | jq '[.[] | select(.isResolved == true)] | length')
UNRESOLVED_COUNT=$(echo "$FILTERED" | jq '[.[] | select(.isResolved == false)] | length')
OUTDATED_COUNT=$(echo "$FILTERED" | jq '[.[] | select(.isOutdated == true)] | length')
echo "Existing review threads: ${THREAD_COUNT} total (${UNRESOLVED_COUNT} unresolved, ${RESOLVED_COUNT} resolved, ${OUTDATED_COUNT} outdated)"
echo ""
# Summary mode: show per-file counts only
if [ "$SUMMARY_ONLY" = true ]; then
echo "Threads by file:"
echo "$FILTERED" | jq -r '
group_by(.path) | .[] |
. as $threads |
($threads | length) as $total |
([$threads[] | select(.isResolved == false)] | length) as $unresolved |
([$threads[] | select(.isResolved == true)] | length) as $resolved |
([$threads[] | select(.isOutdated == true)] | length) as $outdated |
([$threads[] | select(.comments.nodes | length > 1)] | length) as $has_replies |
" " + $threads[0].path +
" — " + ($total | tostring) + " threads" +
" (" + ($unresolved | tostring) + " unresolved, " + ($resolved | tostring) + " resolved" +
(if $outdated > 0 then ", " + ($outdated | tostring) + " outdated" else "" end) +
")" +
(if $has_replies > 0 then " ⚠️ " + ($has_replies | tostring) + " with replies" else "" end)
'
echo ""
echo "Use: pr-existing-comments.sh --file <path> to see full thread details for a file"
exit 0
fi
# Full detail mode: output threads grouped by file
# Show full conversation for threads with replies
FIRST_LIMIT=200
REPLY_LIMIT=300
if [ "$FULL_TEXT" = true ]; then
FIRST_LIMIT=999999
REPLY_LIMIT=999999
fi
echo "$FILTERED" | jq -r --argjson first_limit "$FIRST_LIMIT" --argjson reply_limit "$REPLY_LIMIT" '
group_by(.path) | .[] |
"## " + .[0].path + " (" + (length | tostring) + " threads)\n" +
([.[] |
" " +
(if .isResolved then "✅ RESOLVED" elif .isOutdated then "⚠️ OUTDATED" else "🔴 UNRESOLVED" end) +
" (line " + (if .line then (.line | tostring) elif .startLine then (.startLine | tostring) elif .originalLine then ("~" + (.originalLine | tostring)) elif .originalStartLine then ("~" + (.originalStartLine | tostring)) else "?" end) + ")" +
# Show the commit the comment was originally made on
(if .comments.nodes[0].originalCommit.abbreviatedOid then " [" + .comments.nodes[0].originalCommit.abbreviatedOid + "]" else "" end) +
# Flag threads with replies — indicates a conversation happened
(if (.comments.nodes | length) > 1 then " ← has replies" else "" end) +
"\n" +
([.comments.nodes | to_entries[] |
.value as $comment |
.key as $idx |
($comment.body | gsub("\n"; " ")) as $flat |
if $idx == 0 then
" @" + ($comment.author.login // "unknown") + ": " + $flat[0:$first_limit] +
(if ($flat | length) > $first_limit then " [truncated]" else "" end)
else
" ↳ @" + ($comment.author.login // "unknown") + ": " + $flat[0:$reply_limit] +
(if ($flat | length) > $reply_limit then " [truncated]" else "" end)
end
] | join("\n")) +
"\n"
] | join("\n"))
'

View file

@ -0,0 +1,84 @@
#!/bin/bash
# pr-remove-comment.sh - Remove a queued review comment
#
# Usage:
# pr-remove-comment.sh <file> <line-number>
# pr-remove-comment.sh <comment-id>
#
# Examples:
# pr-remove-comment.sh src/main.go 42
# pr-remove-comment.sh comment-1234567890-1234567890
#
# This script removes a previously queued comment before it's submitted.
# Useful if the agent realizes it made a mistake or wants to update a comment.
#
# Environment variables (set by the composite action):
# PR_REVIEW_COMMENTS_DIR - Directory containing comment files (default: /tmp/pr-review-comments)
set -e
COMMENTS_DIR="${PR_REVIEW_COMMENTS_DIR:-/tmp/pr-review-comments}"
if [ ! -d "${COMMENTS_DIR}" ]; then
echo "No comments directory found: ${COMMENTS_DIR}"
exit 0
fi
# Check if first argument looks like a comment ID
if [[ "$1" =~ ^comment- ]]; then
COMMENT_ID="$1"
COMMENT_FILE="${COMMENTS_DIR}/${COMMENT_ID}.json"
if [ -f "${COMMENT_FILE}" ]; then
FILE=$(jq -r '._meta.file // .path' "${COMMENT_FILE}")
LINE=$(jq -r '._meta.line // .line' "${COMMENT_FILE}")
rm -f "${COMMENT_FILE}"
echo "✓ Removed comment ${COMMENT_ID} for ${FILE}:${LINE}"
else
echo "Comment not found: ${COMMENT_ID}"
exit 1
fi
else
# Treat as file and line number
FILE="$1"
LINE="$2"
if [ -z "$FILE" ] || [ -z "$LINE" ]; then
echo "Usage:"
echo " pr-remove-comment.sh <file> <line-number>"
echo " pr-remove-comment.sh <comment-id>"
echo ""
echo "Examples:"
echo " pr-remove-comment.sh src/main.go 42"
echo " pr-remove-comment.sh comment-1234567890-1234567890"
exit 1
fi
# Validate line is a positive integer (>= 1)
if ! [[ "$LINE" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: Line number must be a positive integer (>= 1), got: $LINE"
exit 1
fi
# Find and remove matching comment files
# Use nullglob to handle case where no files match
shopt -s nullglob
REMOVED=0
for COMMENT_FILE in "${COMMENTS_DIR}"/comment-*.json; do
COMMENT_FILE_PATH=$(jq -r '._meta.file // .path' "${COMMENT_FILE}")
COMMENT_LINE=$(jq -r '._meta.line // .line' "${COMMENT_FILE}")
if [ "$COMMENT_FILE_PATH" = "$FILE" ] && [ "$COMMENT_LINE" = "$LINE" ]; then
COMMENT_ID=$(basename "${COMMENT_FILE}" .json)
rm -f "${COMMENT_FILE}"
echo "✓ Removed comment ${COMMENT_ID} for ${FILE}:${LINE}"
REMOVED=$((REMOVED + 1))
fi
done
if [ "$REMOVED" -eq 0 ]; then
echo "No comment found for ${FILE}:${LINE}"
exit 1
fi
fi

143
.github/scripts/pr-review/pr-review.sh vendored Executable file
View file

@ -0,0 +1,143 @@
#!/bin/bash
# pr-review.sh - Submit a PR review (approve, request changes, or comment)
#
# Usage: pr-review.sh <APPROVE|REQUEST_CHANGES|COMMENT> [review-body]
# Example: pr-review.sh REQUEST_CHANGES "Please fix the issues noted above"
#
# This script creates and submits a review with any queued inline comments.
# Comments are read from individual files in PR_REVIEW_COMMENTS_DIR (created by pr-comment.sh).
#
# The review body can contain special characters (backticks, dollar signs, etc.)
# and will be safely passed to the GitHub API without shell interpretation.
#
# Environment variables (set by the composite action):
# PR_REVIEW_REPO - Repository (owner/repo)
# PR_REVIEW_PR_NUMBER - Pull request number
# PR_REVIEW_HEAD_SHA - HEAD commit SHA
# PR_REVIEW_COMMENTS_DIR - Directory containing queued comment files (default: /tmp/pr-review-comments)
set -e
# Configuration from environment
REPO="${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}"
PR_NUMBER="${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}"
HEAD_SHA="${PR_REVIEW_HEAD_SHA:?PR_REVIEW_HEAD_SHA environment variable is required}"
COMMENTS_DIR="${PR_REVIEW_COMMENTS_DIR:-/tmp/pr-review-comments}"
# Arguments
EVENT="$1"
shift 2>/dev/null || true
# Read body from remaining arguments
# Join all remaining arguments with spaces, preserving the string as-is
BODY="$*"
if [ -z "$EVENT" ]; then
echo "Usage: pr-review.sh <APPROVE|REQUEST_CHANGES|COMMENT> [review-body]"
echo "Example: pr-review.sh REQUEST_CHANGES 'Please fix the issues noted in the inline comments'"
exit 1
fi
# Validate event type
case "$EVENT" in
APPROVE|REQUEST_CHANGES|COMMENT)
;;
*)
echo "Error: Invalid event type '${EVENT}'"
echo "Must be one of: APPROVE, REQUEST_CHANGES, COMMENT"
exit 1
;;
esac
# Read queued comments from individual files
COMMENTS="[]"
COMMENT_COUNT=0
if [ -d "${COMMENTS_DIR}" ]; then
# Collect all comment files and merge into a single JSON array
# Remove _meta fields before submitting (they're only for internal use)
COMMENT_FILES=("${COMMENTS_DIR}"/comment-*.json)
if [ -f "${COMMENT_FILES[0]}" ]; then
# Use jq to read all comment files, extract the comment data (without _meta), and combine
COMMENTS=$(jq -s '[.[] | del(._meta)]' "${COMMENTS_DIR}"/comment-*.json)
COMMENT_COUNT=$(echo "$COMMENTS" | jq 'length')
if [ "$COMMENT_COUNT" -gt 0 ]; then
echo "Found ${COMMENT_COUNT} queued inline comment(s)"
fi
fi
fi
# Append standard footer to the review body (if body is provided)
FOOTER='
---
Marvin Context Protocol | Type `/marvin` to interact further
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.'
if [ -n "$BODY" ]; then
BODY_WITH_FOOTER="${BODY}${FOOTER}"
else
BODY_WITH_FOOTER=""
fi
# Build the review request JSON
# Use jq to safely construct the JSON with all special characters handled
REVIEW_JSON=$(jq -n \
--arg commit_id "$HEAD_SHA" \
--arg event "$EVENT" \
--arg body "$BODY_WITH_FOOTER" \
--argjson comments "$COMMENTS" \
'{
commit_id: $commit_id,
event: $event,
comments: $comments
} + (if $body != "" then {body: $body} else {} end)')
# Check if HEAD has changed since review started (race condition detection)
CURRENT_HEAD=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha')
if [ "$CURRENT_HEAD" != "$HEAD_SHA" ]; then
echo "⚠️ WARNING: PR head has changed since review started!"
echo " Review started at: ${HEAD_SHA:0:7}"
echo " Current head: ${CURRENT_HEAD:0:7}"
echo ""
echo " New commits may have shifted line numbers. Review will be submitted"
echo " against the original commit (${HEAD_SHA:0:7}) but comments may be outdated."
echo ""
fi
echo "Submitting ${EVENT} review for commit ${HEAD_SHA:0:7}..."
# Create and submit the review in one API call
# Use a temp file to safely pass the JSON body
TEMP_JSON=$(mktemp)
trap "rm -f ${TEMP_JSON}" EXIT
echo "$REVIEW_JSON" > "${TEMP_JSON}"
RESPONSE=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/reviews" \
-X POST \
--input "${TEMP_JSON}" 2>&1) || {
echo "Error submitting review:"
echo "$RESPONSE"
exit 1
}
# Clean up the comments directory after successful submission
if [ -d "${COMMENTS_DIR}" ] && [ "$COMMENT_COUNT" -gt 0 ]; then
rm -f "${COMMENTS_DIR}"/comment-*.json
# Remove directory if empty
rmdir "${COMMENTS_DIR}" 2>/dev/null || true
fi
REVIEW_URL=$(echo "$RESPONSE" | jq -r '.html_url // empty')
REVIEW_STATE=$(echo "$RESPONSE" | jq -r '.state // empty')
if [ -n "$REVIEW_URL" ]; then
echo "✓ Review submitted (${REVIEW_STATE}): ${REVIEW_URL}"
if [ "$COMMENT_COUNT" -gt 0 ]; then
echo " Included ${COMMENT_COUNT} inline comment(s)"
fi
else
echo "✓ Review submitted successfully"
fi

80
.github/scripts/triage-label.sh vendored Executable file
View file

@ -0,0 +1,80 @@
#!/usr/bin/env bash
# Locked-down label helper for the Marvin triage workflow.
#
# Marvin runs on untrusted issue/PR bodies from non-write users, so it must
# NOT be handed raw `gh api` (that would expose every endpoint the app token
# can reach). This helper is the ONLY GitHub write it is allowed to perform:
# it adds or removes repository labels on the one issue/PR being triaged.
#
# The target repo and number come from the environment set by the workflow —
# never from the model — and the operation is fixed to the additive labels
# endpoint (POST/DELETE /repos/{repo}/issues/{n}/labels), which works for both
# issues and PRs and cannot clobber labels applied by other workflows.
set -euo pipefail
repo="${TRIAGE_REPO:?TRIAGE_REPO not set}"
number="${TRIAGE_NUMBER:?TRIAGE_NUMBER not set}"
if [[ ! "$number" =~ ^[0-9]+$ ]]; then
echo "TRIAGE_NUMBER must be numeric, got: $number" >&2
exit 1
fi
op="${1:-}"
shift || true
case "$op" in
add) method=POST ;;
remove) method=DELETE ;;
*)
echo "usage: triage-label.sh <add|remove> <label>..." >&2
exit 1
;;
esac
if [[ "$#" -eq 0 ]]; then
echo "no labels given" >&2
exit 1
fi
# Reject anything that isn't a plausible label name. Notably blocks '/' so a
# crafted value can't turn the DELETE path into a different endpoint.
label_re="^[A-Za-z0-9 ._'-]+$"
for label in "$@"; do
if [[ ! "$label" =~ $label_re ]]; then
echo "refusing suspicious label name: $label" >&2
exit 1
fi
done
# Never let triage add or remove the Require Issue Link control labels. Those
# govern PR enforcement (bypass-issue-check / trusted-contributor are sticky
# exemptions, "prs welcome" waives the assignment requirement) and reopening
# (missing-issue-link is how closed PRs are found), so a prompt-injected triage
# run must not be able to grant an exemption or break recovery. Enforced here —
# in code — not merely in the prompt.
#
# Exact match against array entries, not a substring scan of a joined string:
# label names may contain spaces ("prs welcome"), which in a space-delimited
# string would also make bare "prs" and "welcome" match.
protected=(missing-issue-link bypass-issue-check trusted-contributor "prs welcome")
for label in "$@"; do
lower="${label,,}"
for p in "${protected[@]}"; do
if [[ "$lower" == "$p" ]]; then
echo "refusing to touch protected control label: $label" >&2
exit 1
fi
done
done
if [[ "$method" == POST ]]; then
args=()
for label in "$@"; do
args+=(-f "labels[]=$label")
done
gh api --method POST "/repos/${repo}/issues/${number}/labels" "${args[@]}"
else
for label in "$@"; do
gh api --method DELETE "/repos/${repo}/issues/${number}/labels/${label}"
done
fi

View file

@ -16,11 +16,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v7
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}

View file

@ -16,11 +16,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v7
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}

View file

@ -1,172 +0,0 @@
name: Martian Issue Triage
on:
issues:
types: [opened, labeled]
concurrency:
group: triage-martian-${{ github.event.issue.number }}
cancel-in-progress: true
jobs:
martian-issue-triage:
if: |
(github.event.action == 'opened' && github.actor == 'strawgate') ||
(github.event.action == 'labeled' && github.event.label.name == 'triage-martian')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
pull-requests: read
id-token: write
steps:
- name: Checkout base repository
uses: actions/checkout@v5
with:
repository: ${{ github.repository }}
ref: ${{ github.event.repository.default_branch }}
# Install UV package manager
- name: Install UV
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
- name: Set triage prompt
id: triage-prompt
run: |
cat >> $GITHUB_OUTPUT << 'EOF'
PROMPT<<PROMPT_END
You're an issue triage assistant for FastMCP, a Python framework for building Model Context Protocol servers and clients.
# IMPORTANT RULES
1. You will not make branches or pull requests. Your ONLY action will be investigating the issue, locating related issues,
pull requests, and files in the repository and reporting your findings.
2. You will identify the issue type (bug/feature/question) up front and tailor the Recommendation (e.g., for questions: answer directly + links; for bugs: point to failing tests/lines).
3. You will avoid speculation and only assert facts that are deeply rooted (traceable) to the codebase, language/framework conventions, related issues, related pull requests, etc.
4. The main branch of the repository has been cloned locally, but changes will not be accepted and you are not allowed to make pull requests or other changes. You can search the local repository for relevant code. You will use the available MCP Server tools identify related issues and pull requests (search_issues and search_pull_requests) and you can use search_code to look at the code in relevant dependent packages. For example, you can use search_code to look at the underlying SDK `https://github.com/modelcontextprotocol/python-sdk` to see how it implements a certain class or function relevant to the issue at hand.
# Getting Started
1. Call the generate_agents_md tool to get a high-level summary of the project you're working in
2. Get the issue ${{ github.event.issue.number }} in the GitHub repository: ${{ github.repository }}.
3. Use the search_issues and search_pull_requests tools to scour the repository for actually related issues and pull requests
4. Call the search_code, get_files, etc. tools to search the repository to identify the related classes, methods, docs, tests, etc that are relevant to the issue.
# Providing a Great Response
Your number one priority is to provide a great response to the issue. A great response is a response that is clear, concise, accurate, and actionable. You will avoid long paragraphs, flowery language, and overly verbose responses. Your readers have limited time and attention, so you will be concise and to the point.
In priority order your goal is to:
1. Provide context about the request or issue (related issues, pull requests, files, etc.)
2. Layout a single high-quality and actionable recommendation for how to address the issue based on your knowledge of the project, codebase, and issue
3. Provide an high quality and detailed plan that a junior developer could follow to implement the recommendation
Populate the following sections in your response:
Recommendation (or “No recommendation” with reason)
Findings
Detailed Action Plan
Related Items
Related Files
Related Webpages
You may not be able to do all of these things, sometimes you may find that all you can do is provide in-depth context of the issue and related items. That's perfectly acceptable and expected. Your performance is judged by how accurate your findings are, do the investigation required to have high confidence in your findings and recommendations. "I don't know" or "I'm unable to recommend a course of action" is better than a bad or wrong answer.
When formulating your response, you will never "bury the lede", you will always provide a clear and concise tl;dr as the first thing in your response. As your response grows in length you can organize the more detailed parts of your response collapsible sections using <details> and <summary> tags. You shouldn't put everything in collapsible sections, especially if the response is short. Use your discretion to determine when to use collapsible sections to avoid overwhelming the reader with too much detail -- think of them like an appendix that can be expanded if the reader is interested.
# Example output for "Recommendation" part of the response
PR #654 already implements the requested feature but is incomplete. The Pull Request is not in a mergeable state yet, the remaining work should be completed: 1) update the Calculator.divide method to utilize the new DivisionByZeroError or the safe_divide function, and 2) update the tests to ensure that the Calculator.divide method raises the new DivisionByZeroError when the divisor is 0.
<details>
<summary>Findings</summary>
...details from the code analysis that are relevant to the issue and the recommendation...
</details>
<details>
<summary>Detailed Action Plan</summary>
...a detailed plan that a junior developer could follow to implement the recommendation...
</details>
# Example Output for "Related Items" part of the response
<details>
<summary>Related Issues and Pull Requests</summary>
| Repository | Issue or PR | Relevance |
| --- | --- | --- |
| jlowin/fastmcp | [Add matrix operations support](https://github.com/jlowin/fastmcp/pull/680) | This pull request directly addresses the feature request for adding matrix operations to the calculator. |
| jlowin/fastmcp | [Add matrix operations support](https://github.com/jlowin/fastmcp/issues/681) | This issue directly addresses the feature request for adding matrix operations to the calculator. |
</details>
<details>
<summary>Related Files</summary>
| Repository | File | Relevance | Sections |
| --- | --- | --- | --- |
| modelcontextprotocol/python-sdk | [test_calculator.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/test_calculator.py) | This file contains the test cases for the Calculator class, including a test that specifically asserts a ValueError is raised for division by zero, confirming the current intended behavior. | [25-27](https://github.com/modelcontextprotocol/python-sdk/blob/main/test_calculator.py#L25-L27) |
| modelcontextprotocol/python-sdk | [calculator.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/calculator.py) | This file contains the implementation of the Calculator class, specifically the `divide` method which raises the ValueError when dividing by zero, matching the bug report. | [29-32](https://github.com/modelcontextprotocol/python-sdk/blob/main/calculator.py#L29-L32) |
</details>
<details>
<summary>Related Webpages</summary>
| Name | URL | Relevance |
| --- | --- | --- |
| Handling Division by Zero Best Practices | https://my-blog-about-division-by-zero.com/handling+division+by+zero+in+calculator | This webpage provides general best practices for handling division by zero in calculator applications and in Python, which is directly relevant to the issue and potential solutions. |
</details>
PROMPT_END
EOF
- name: Setup GitHub MCP Server
run: |
mkdir -p /tmp/mcp-config
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
{
"mcpServers": {
"repository-summary": {
"type": "http",
"url": "https://agents-md-generator.fastmcp.app/mcp"
},
"code-search": {
"type": "http",
"url": "https://public-code-search.fastmcp.app/mcp"
},
"github-research": {
"type": "stdio",
"command": "uvx",
"args": [
"github-research-mcp"
],
"env": {
"DISABLE_SUMMARIES": "true",
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ steps.marvin-token.outputs.token }}"
}
}
}
}
EOF
- name: Run Martian for Issue Triage
uses: anthropics/claude-code-action@v1
with:
github_token: ${{ steps.marvin-token.outputs.token }}
bot_name: "Marvin Context Protocol"
prompt: ${{ steps.triage-prompt.outputs.PROMPT }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}
track_progress: true
claude_args: |
--model claude-sonnet-4-5-20250929
--allowedTools mcp__repository-summary,mcp__code-search__search_code,mcp__github-research__get_repository,mcp__github-research__get_issue,mcp__github-research__get_pull_request,mcp__github-research__search_issues,mcp__github-research__search_pull_requests,mcp__github-research__get_files
--mcp-config /tmp/mcp-config/mcp-servers.json
settings: |
{
"GH_TOKEN": "${{ steps.marvin-token.outputs.token }}"
}

View file

@ -0,0 +1,148 @@
# Respond to /marvin mentions in issue comments (elastic mention-in-issue style)
# Calls run-claude directly
name: Comment on Issue
on:
issue_comment:
types: [created]
permissions:
actions: read
contents: write
issues: write
pull-requests: write
id-token: write
jobs:
comment:
if: |
!github.event.issue.pull_request &&
contains(github.event.comment.body, '/marvin') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Install UV
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install dependencies
run: uv sync --python 3.12
- 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 }}
- name: React to comment with eyes
env:
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}
run: |
gh api "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" -f content=eyes 2>/dev/null || true
- name: Run Claude for Issue Comment
uses: ./.github/actions/run-claude
env:
COMMENT_BODY: ${{ github.event.comment.body }}
ISSUE_TITLE: ${{ github.event.issue.title }}
with:
claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github-token: ${{ steps.marvin-token.outputs.token }}
trigger-phrase: "/marvin"
allowed-bots: "*"
allowed-tools: "Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code"
prompt: |
<context>
Repository: ${{ github.repository }}
Issue Number: #${{ github.event.issue.number }}
Issue Title: ${{ env.ISSUE_TITLE }}
Issue Author: ${{ github.event.issue.user.login }}
Comment Author: ${{ github.event.comment.user.login }}
</context>
<user_request>
${{ env.COMMENT_BODY }}
</user_request>
<task>
You have been mentioned in a GitHub issue comment. Understand the request, gather context, complete the task, and respond with results.
</task>
<constraints>
You CAN: Read/analyze code, modify files, write code, run tests, execute commands, commit code, push changes, create branches, create pull requests
</constraints>
<allowed_tools>
You have access to the following tools (comma-separated list):
Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code
You can only use tools that are explicitly listed above. For Bash commands, the pattern `Bash(command:*)` means you can run that command with any arguments. If a command is not listed, it is not available.
</allowed_tools>
<getting_started>
Use `mcp__agents-md-generator__generate_agents_md` to get repository context before responding.
</getting_started>
<investigation_approach>
Be thorough in your investigations:
- Understand the full context of the repository
- Review related code, issues, and PRs
- Consider edge cases and implications
- Gather all relevant information before responding
Available tools:
- `mcp__public-code-search__search_code`: Search code in OTHER repositories (use `Grep`/`Read` for this repo)
- `WebSearch`: Search the web for documentation, best practices, or solutions
- `WebFetch`: Fetch and read content from URLs
</investigation_approach>
<common_tasks>
- Answer questions about the codebase
- Help debug reported problems
- Suggest solutions or workarounds
- Provide code examples
- Help clarify requirements
- Link to relevant documentation or code
- Create branches, commit changes, and open PRs when asked
</common_tasks>
<response_guidelines>
- Lead with a tl;dr — the bottom line in 1-3 sentences, always visible. The reader should be able to act without expanding anything.
- Push supporting detail (code analysis, verification output, related items) into collapsible `<details>` blocks. These are appendices, not the main message.
- Short responses (a few sentences) don't need collapsible sections at all.
- Be concise and actionable.
- If the request is unclear, ask clarifying questions.
- Report findings and recommendations — not your process. Do not include task checklists or "steps I took" narration.
- Every claim needs evidence: cite file paths, line numbers, or command output. Never say "the code does X" without pointing to where.
- If you're uncertain, say so. "I couldn't confirm this" is better than a speculative answer.
</response_guidelines>
<github_safety>
- Do not write `fixes #N`, `closes #N`, or `resolves #N` in comments — these can accidentally close issues.
- When referencing issues, use plain `#N` or link syntax without action keywords.
</github_safety>
<response_footer>
Always end your comment with a new line, three dashes, and the footer message:
<exact_content>
---
Marvin Context Protocol | Type `/marvin` to interact further
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
</exact_content>
</response_footer>
<github_formatting>
When writing GitHub comments, wrap branch names, tags, or other @-references in backticks (e.g., `@main`, `@v1.0`) to avoid accidentally pinging users. Do not add backticks around terms that are already inside backticks or code blocks.
</github_formatting>

View file

@ -0,0 +1,307 @@
# Respond to /marvin mentions in PR review comments and issue comments on PRs
# Calls run-claude directly
name: Comment on PR
on:
issue_comment:
types: [created]
permissions:
contents: write
pull-requests: write
issues: read
id-token: write
jobs:
comment:
if: |
github.event.issue.pull_request &&
contains(github.event.comment.body, '/marvin') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout PR head branch
uses: actions/checkout@v7
with:
# do not set to pull_request.head.ref, claude will pull the branch if needed
fetch-depth: 0
- name: Install UV
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Install dependencies
run: uv sync --python 3.12
- 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 }}
- name: React to comment with eyes
env:
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}
run: |
gh api "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" -f content=eyes 2>/dev/null || true
- name: Get PR HEAD SHA
id: pr-info
env:
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}
run: |
PR_NUMBER="${{ github.event.issue.number }}"
HEAD_SHA=$(gh api "repos/${{ github.repository }}/pulls/${PR_NUMBER}" --jq '.head.sha')
echo "head_sha=${HEAD_SHA}" >> "$GITHUB_OUTPUT"
echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"
- name: Run Claude for PR Comment
uses: ./.github/actions/run-claude
env:
MENTION_REPO: ${{ github.repository }}
MENTION_PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
MENTION_SCRIPTS: ${{ github.workspace }}/.github/scripts/mention
PR_REVIEW_REPO: ${{ github.repository }}
PR_REVIEW_PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
PR_REVIEW_HEAD_SHA: ${{ steps.pr-info.outputs.head_sha }}
PR_REVIEW_COMMENTS_DIR: /tmp/pr-review-comments
PR_REVIEW_HELPERS_DIR: ${{ github.workspace }}/.github/scripts/pr-review
COMMENT_BODY: ${{ github.event.comment.body }}
PR_TITLE: ${{ github.event.issue.title }}
with:
claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github-token: ${{ steps.marvin-token.outputs.token }}
trigger-phrase: "/marvin"
allowed-bots: "*"
allowed-tools: "Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code"
prompt: |
<context>
Repository: ${{ github.repository }}
PR Number: #${{ steps.pr-info.outputs.pr_number }}
PR Title: ${{ env.PR_TITLE }}
PR Author: ${{ github.event.issue.user.login }}
Comment Author: ${{ github.event.comment.user.login }}
**Note**: The PR head branch has already been checked out. The workspace is ready - you can immediately start working on the PR code.
</context>
<user_request>
${{ env.COMMENT_BODY }}
</user_request>
<task>
You have been mentioned in a Pull Request comment. Understand the request, gather context, complete the task, and respond with results.
</task>
<constraints>
You CAN: Read/analyze code, modify files, write code, run tests, execute commands, resolve review threads, commit and push changes to the PR branch, checkout branches
You CANNOT: Create new branches unrelated to this PR, create new pull requests
When making changes, commit and push to the PR's head branch so the author gets the fix directly.
</constraints>
<allowed_tools>
You have access to the following tools (comma-separated list):
Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code
You can only use tools that are explicitly listed above. For Bash commands, the pattern `Bash(command:*)` means you can run that command with any arguments. If a command is not listed, it is not available.
</allowed_tools>
<getting_started>
Use `mcp__agents-md-generator__generate_agents_md` to get repository context before responding.
</getting_started>
<investigation_approach>
Be thorough in your investigations:
- Understand the full context of the repository
- Review related code, issues, and PRs
- Consider edge cases and implications
- Gather all relevant information before responding
Available tools:
- `mcp__public-code-search__search_code`: Search code in OTHER repositories (use `Grep`/`Read` for this repo)
- `WebSearch`: Search the web for documentation, best practices, or solutions
- `WebFetch`: Fetch and read content from URLs
</investigation_approach>
<common_tasks>
- Address review feedback and fix issues (commit and push to the PR branch)
- Answer questions about the changes
- Make code changes and push them
- Resolve review threads after addressing feedback
- Perform PR reviews when asked (use the PR review process below)
</common_tasks>
<pr_review_guidance>
When asked to review this PR, follow this structured review process.
The `$PR_REVIEW_HELPERS_DIR` environment variable is pre-configured for all scripts below.
<review_process>
Follow these steps in order:
**Step 1: Gather context**
- Use `mcp__agents-md-generator__generate_agents_md` to get repository context
(if this fails, explore the repository to understand the codebase — read key files like README, CONTRIBUTING, etc.)
- Run `$PR_REVIEW_HELPERS_DIR/pr-existing-comments.sh --summary` to see existing review threads per file
- Run `$PR_REVIEW_HELPERS_DIR/pr-diff.sh` to see changed files with line-numbered diffs
(for large PRs, this lists files only — review each with `pr-diff.sh <filename>`)
**Step 2: Review each file**
For each changed file:
a. If the summary showed existing threads for this file, first run:
`$PR_REVIEW_HELPERS_DIR/pr-existing-comments.sh --file <path>`
Read the full thread details. The output uses these conventions:
- `← has replies` — a conversation happened; read carefully before commenting
- `[truncated]` — comment was cut short; add `--full` if you need the complete text to understand the comment
- `[abc1234]` — commit the comment was made on; use `git show abc1234` if needed
- `~42` — approximate line from an older revision (exact line no longer maps to current diff)
b. Review the diff. Use `Read` to see full file contents when you need more context.
Identify issues matching review_criteria. Do NOT flag:
- Issues in unchanged code (only review the diff)
- Style preferences handled by linters
- Pre-existing issues not introduced by this PR
- Issues already covered by existing threads (see below)
**Existing thread rules** (check BEFORE leaving any comment):
- Resolved with reviewer reply → reviewer's decision is final. Do NOT re-flag.
Examples: "It should remain as X", "This is intentional", "No need to do this change"
- Resolved without reply → author likely fixed it. Do NOT re-raise unless the fix introduced a new problem.
- Unresolved → already flagged. Do NOT re-comment. Mention in review body if you have more to add.
- Outdated → code changed. Only re-flag if the issue still applies to the current diff.
When in doubt, do not duplicate. Redundant comments erode trust in the review process.
**Step 3: Leave comments for NEW issues only**
For each genuinely new issue not covered by existing threads:
```bash
$PR_REVIEW_HELPERS_DIR/pr-comment.sh <file> <line> \
--severity <critical|high|medium|low|nitpick> \
--title "Brief description" \
--why "Risk or impact" <<'EOF'
corrected code here
EOF
```
Always provide suggestion code. Use `--no-suggestion` only when the fix requires
changes across multiple locations. Broader architectural concerns belong in the
review body, not inline comments.
To remove a queued comment: `$PR_REVIEW_HELPERS_DIR/pr-remove-comment.sh <file> <line>`
**Step 4: Submit the review**
```bash
$PR_REVIEW_HELPERS_DIR/pr-review.sh <APPROVE|REQUEST_CHANGES|COMMENT> "<review body>"
```
- REQUEST_CHANGES: Any 🔴 CRITICAL or 🟠 HIGH issues found
- COMMENT: 🟡 MEDIUM issues found (but no critical/high)
- APPROVE: No issues, or only ⚪ LOW / 💬 NITPICK suggestions
The review body should include broader architectural concerns not suited for inline comments.
Avoid summarizing the PR or offering praise. If approving with no issues, omit the review body.
A standard footer is automatically appended to all comments and reviews.
</review_process>
<severity_classification>
🔴 CRITICAL - Must fix before merge (security vulnerabilities, data corruption, production-breaking bugs)
🟠 HIGH - Should fix before merge (logic errors, missing validation, significant performance issues)
🟡 MEDIUM - Address soon, non-blocking (error handling gaps, suboptimal patterns, missing edge cases)
⚪ LOW - Author discretion, non-blocking (minor improvements, documentation, style not covered by linters)
💬 NITPICK - Truly optional (stylistic preferences, alternative approaches — safe to ignore)
</severity_classification>
<review_criteria>
Focus on these categories, in priority order:
1. Security vulnerabilities (injection, XSS, auth bypass, secrets exposure)
2. Logic bugs that could cause runtime failures or incorrect behavior
3. Data integrity issues (race conditions, missing transactions, corruption risk)
4. Performance bottlenecks (N+1 queries, memory leaks, blocking operations)
5. Error handling gaps (unhandled exceptions, missing validation)
6. Breaking changes to public APIs without migration path
7. Missing or incorrect test coverage for critical paths
</review_criteria>
<review_calibration>
**What NOT to flag** — do not comment on:
- Issues in unchanged code (only review the diff)
- Input already validated or sanitized at a different layer
- Theoretical performance concerns without evidence that N is large
- Style or formatting not in the project's linting rules
- Missing tests for trivial or generated code
- Pre-existing patterns the PR is following consistently
**Calibration examples**:
- Unguarded return from a lookup (e.g., `tool = registry.get(name)` used without None check) → FLAG if the diff introduces the unguarded usage
- Same pattern, but the function's return type is `Tool` (not `Optional[Tool]`) → DO NOT FLAG, the type system guarantees non-None
- String interpolation in a query with user input → FLAG
- String interpolation in a query with a hardcoded enum value → DO NOT FLAG
- O(n²) loop → FLAG only if there's evidence N can be large (e.g., user-controlled list). If N is bounded by design (e.g., number of MCP tools), do not flag.
When in doubt, do not flag. A false positive wastes a reviewer's time and erodes trust in every future review comment.
</review_calibration>
</pr_review_guidance>
<review_thread_tools>
View unresolved review threads:
```bash
$MENTION_SCRIPTS/gh-get-review-threads.sh
```
Filter for unresolved threads from a specific reviewer:
```bash
$MENTION_SCRIPTS/gh-get-review-threads.sh "reviewer-username"
```
Resolve a review thread after addressing feedback:
```bash
$MENTION_SCRIPTS/gh-resolve-review-thread.sh "THREAD_ID" "Fixed by updating the error handling"
```
- `THREAD_ID` is the GraphQL node ID from the review threads output (e.g., `PRRT_kwDOABC123`)
- The comment is optional - use it to explain what you did
Note: You can resolve threads after pushing fixes, or resolve them to acknowledge feedback that will be addressed separately.
</review_thread_tools>
<response_guidelines>
- Lead with a tl;dr — the bottom line in 1-3 sentences, always visible. The reader should be able to act without expanding anything.
- Push supporting detail (code analysis, verification output, related items) into collapsible `<details>` blocks. These are appendices, not the main message.
- Short responses (a few sentences) don't need collapsible sections at all.
- Be concise and actionable.
- If the request is unclear, ask clarifying questions.
- When making code changes, commit and push them to the PR branch so the author gets the fix directly.
- Every claim needs evidence: cite file paths, line numbers, or command output. Never say "the code does X" without pointing to where.
- If you're uncertain, say so. "I couldn't confirm this" is better than a speculative answer.
**When performing a PR review**: Your substantive feedback belongs in the PR review submission
(via pr-review.sh), not in the comment response. The comment should only report:
- That you've submitted the review (with the outcome: approved, requested changes, etc.)
- Any issues encountered during the review process
- Brief status updates
Do NOT duplicate the review content in your comment - the review itself contains all the details.
Keep the comment short, e.g., "I've submitted my review requesting changes. See the review for details."
</response_guidelines>
<github_safety>
- Do not write `fixes #N`, `closes #N`, or `resolves #N` in comments — these can accidentally close issues.
- When referencing issues, use plain `#N` or link syntax without action keywords.
</github_safety>
<response_footer>
Always end your comment with a new line, three dashes, and the footer message:
<exact_content>
---
Marvin Context Protocol | Type `/marvin` to interact further
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
</exact_content>
</response_footer>
<github_formatting>
When writing GitHub comments, wrap branch names, tags, or other @-references in backticks (e.g., `@main`, `@v1.0`) to avoid accidentally pinging users. Do not add backticks around terms that are already inside backticks or code blocks.
</github_formatting>

View file

@ -19,13 +19,20 @@ 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@v5
uses: actions/checkout@v7
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
@ -37,23 +44,38 @@ jobs:
PROMPT<<PROMPT_END
Find up to 3 likely duplicate issues for GitHub issue ${{ github.repository }}/issues/${{ github.event.issue.number || inputs.issue_number }}.
Follow these steps precisely:
# Core Principle
Silence is better than noise. A false positive wastes a human's time and erodes trust in every future report. Most runs should end with no comment — that means the system is working.
# Steps
1. Check if the GitHub issue (a) is closed, (b) does not need to be deduped (eg. because it is broad product feedback without a specific solution, or positive feedback), or (c) already has a duplicates comment that you made earlier. If so, do not proceed.
2. View the GitHub issue and produce a summary of the issue
2. View the GitHub issue and produce a summary of the issue.
3. Then, launch 3 parallel agents using the Task tool to search GitHub for duplicates of this issue, using diverse keywords and search approaches, using the summary from step 2
3. Launch 3 parallel agents using the Task tool to search GitHub for duplicates, using diverse keywords and search approaches, using the summary from step 2.
4. Next, consider the results from steps 2 and 3 and filter out false positives that are likely not actually duplicates of the original issue. If there are no duplicates remaining, do not proceed.
4. Filter aggressively for false positives. The bar for "duplicate" is high:
5. Finally, comment back on the issue with a list of up to three duplicate issues (or zero, if there are no likely duplicates). If there are no duplicates, DO NOT COMMENT. Just exit.
A duplicate means the SAME bug or the SAME feature request. Apply this test to every candidate:
- **Same fix test**: Could the candidate be closed by the exact same code change? If not, not a duplicate.
- **Same symptom test**: Does the user experience the exact same broken behavior? "Both involve middleware" is not duplication. "Both get TypeError on line 42 of proxy.py when calling mount()" is duplication.
- **Same request test** (for features): Are they asking for the same specific capability? "Both want better auth" is not duplication. "Both request OAuth PKCE flow for CLI login" is duplication.
Notes for your agents:
Candidates found by only one search agent deserve extra scrutiny — a single keyword match is often a false positive.
When in doubt, do not flag. A missed duplicate is harmless; a false positive wastes the reporter's time.
If there are no duplicates remaining, do not proceed — just exit.
5. **Quality gate**: Before commenting, re-read each candidate as a skeptical reviewer. For each one, ask: "Would a maintainer who knows this codebase agree this is a duplicate, or would they dismiss it?" If you'd need to hedge with "might" or "possibly," drop it.
6. Comment back on the issue with your findings (or exit silently if none remain). Do NOT add any labels — labeling is handled by a later workflow step.
# Notes for your agents
- Use `gh` to interact with GitHub, rather than web fetch
- Do not use other tools, beyond `gh` and Task (eg. don't use other MCP servers, file edit, etc.)
- Make a todo list first
- Do not use other tools beyond `gh` and Task (no MCP servers, file edit, etc.)
- Never include this issue as a duplicate of itself
- When searching, read the FULL body of candidate issues — titles alone are not enough to judge duplication
For your comment, follow this format precisely (example with 3 suspected duplicates):
@ -61,7 +83,7 @@ jobs:
Found 3 possible duplicate issues:
1. #123: Issue title here
2. #456: Another issue title
2. #456: Another issue title
3. #789: Third issue title
This issue will be automatically closed as a duplicate in 3 days.
@ -73,21 +95,52 @@ jobs:
PROMPT_END
EOF
- 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 }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}
allowed_non_write_users: "*"
claude_args: |
--allowedTools Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh api:*),Bash(gh issue comment:*),Task
--mcp-config /tmp/mcp-config/mcp-servers.json
--allowedTools "Bash(gh issue view:*)","Bash(gh search:*)","Bash(gh issue list:*)","Bash(gh api:*)","Bash(gh issue comment:*)",Task
settings: |
{
"model": "claude-sonnet-4-5-20250929",
"model": "claude-sonnet-5",
"env": {
"GH_TOKEN": "${{ steps.marvin-token.outputs.token }}"
}
}
- name: Add potential-duplicate label if bot commented in this run
env:
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}
run: |
ISSUE=${{ github.event.issue.number || inputs.issue_number }}
# Only match bot comments created in the last 10 minutes (this run)
CUTOFF=$(date -u -d '10 minutes ago' '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null \
|| date -u -v-10M '+%Y-%m-%dT%H:%M:%SZ')
HAS_RECENT=$(gh api "repos/${{ github.repository }}/issues/${ISSUE}/comments?sort=created&direction=desc&per_page=10" \
--jq "[.[] | select(
.user.type == \"Bot\" and
(.body | test(\"possible duplicate issues\"; \"i\")) and
.created_at >= \"${CUTOFF}\"
)] | length")
if [ "$HAS_RECENT" -gt 0 ]; then
gh issue edit "$ISSUE" --add-label "potential-duplicate" -R "${{ github.repository }}"
echo "Added potential-duplicate label to #${ISSUE}"
else
echo "No recent duplicate comment found, skipping label"
fi

View file

@ -19,6 +19,7 @@ concurrency:
jobs:
label-issue-or-pr:
if: github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
@ -26,19 +27,36 @@ 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@v5
uses: actions/checkout@v7
with:
repository: ${{ github.repository }}
ref: ${{ github.event.repository.default_branch }}
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
uses: actions/create-github-app-token@v3
with:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
owner: PrefectHQ
- name: Set triage prompt
id: triage-prompt
@ -47,7 +65,16 @@ jobs:
PROMPT<<PROMPT_END
You're an issue triage assistant for FastMCP, a Python framework for building Model Context Protocol servers and clients. Your task is to analyze issues/PRs and apply appropriate labels.
IMPORTANT: Your ONLY action should be to apply labels using mcp__github__update_issue. DO NOT post any comments.
IMPORTANT: Your primary action should be to apply labels using the locked-down helper `.github/scripts/triage-label.sh`. DO NOT post comments EXCEPT when applying the too-long label (see below).
CRITICAL — LABEL MECHANICS:
- Apply labels ONLY through the helper, which adds or removes repository labels on THIS issue/PR. It already knows the target repo and number (from the workflow environment) — you never pass them:
add: `bash .github/scripts/triage-label.sh add "label1" "label2"`
remove: `bash .github/scripts/triage-label.sh remove "label1"`
- The helper uses the additive REST labels endpoint, so it works for both issues and PRs and never clobbers labels applied by other workflows — notably the Require Issue Link workflow's `missing-issue-link` control label, which must survive or an auto-closed PR won't reopen when its author is assigned.
- The helper is your ONLY GitHub write access. Do NOT use raw `gh api`, `gh issue edit`, `gh pr edit`, or any other mutation — they are not available to you.
- Only apply labels that exist in the repository (from `gh label list` in step 1). Never invent labels.
- Use `remove` only to correct a label you believe is wrong, and never remove the control labels `missing-issue-link`, `bypass-issue-check`, or `trusted-contributor`.
Issue/PR Information:
- REPO: ${{ github.repository }}
@ -66,7 +93,7 @@ jobs:
3. Analyze and apply labels based on these guidelines:
CORE CATEGORIES (apply EXACTLY ONE - these are mutually exclusive):
CORE CATEGORIES (apply EXACTLY ONE - these are mutually exclusive; skip if applying too-long):
- bug: Reports of broken functionality OR PRs that fix bugs
- enhancement: New functions/endpoints, improvements to existing features, internal tooling, workflow improvements, minor new capabilities
- feature: ONLY for major headline functionality worthy of a blog post announcement (2-4 per release, never for issues)
@ -94,8 +121,12 @@ jobs:
STATUS (apply if applicable):
- needs more info: Issue lacks reproduction steps, error messages, or clear description
- good first issue: ONLY if it's clearly scoped, has obvious solution, and touches limited files
- invalid: Spam, completely off-topic, or nonsensical (often LLM-generated)
- too-long: Apply when an issue or PR doesn't conform to CONTRIBUTING.md. Issues should be a short problem description, an MRE, and expected vs. actual behavior — not a design document. PRs should have a focused description of the change — not a report. We don't need proposed solutions or design alternatives (the issue should describe the problem and let maintainers architect the fix), summaries of what tests cover, explanations of code we can read ourselves, or speculative root-cause analysis. Common LLM failure modes to watch for: verbose "diagnostic" writeups, large proposed patches in issue bodies, multi-section reports restating what's visible in the diff, numbered lists of possible approaches or solutions, "suggested" schemas/shapes/APIs, generic analysis that doesn't reference specific code, and "Notes" sections. But these are heuristics, not rules — a complex PR may legitimately need more context, and a brief submission can still be low-quality. Judge by whether the content helps a reviewer or just adds noise. When applying too-long, still apply the core category and area labels — too-long is a format signal, not a replacement for categorization. Issues still need to be findable by category.
WHEN APPLYING too-long: After labeling, post a brief comment using mcp__github__add_issue_comment:
"Thanks for the report. This issue goes beyond what our contributor guidelines ask for — we just need a short problem description and an MRE. Please see our [contributing guidelines](https://github.com/PrefectHQ/fastmcp/blob/main/CONTRIBUTING.md) and condense this issue. We'll triage it once it's trimmed down."
Use this exact text (or very close to it). Do not editorialize or add details.
AREA LABELS (apply ONLY when thematically central to the issue):
- cli: Issues primarily about FastMCP CLI commands (run, dev, install)
@ -104,40 +135,159 @@ jobs:
- auth: Authentication is the main concern (Bearer, JWT, OAuth, WorkOS)
- openapi: OpenAPI integration/parsing is the primary topic
- http: HTTP transport or networking is the main issue
- contrib: Specifically about community contributions in src/contrib/
- contrib: Specifically about community contributions in fastmcp_slim/fastmcp/contrib/
- tests: Issues primarily about testing infrastructure, CI/CD workflows, or test coverage
- security: Apply ONLY when the issue/PR addresses an exploitable vulnerability or hardens against one. Examples: SSRF, LFI, path traversal, injection, auth bypass allowing unauthorized access, scope escalation, open redirects. Do NOT apply for ordinary auth bugs (wrong scopes returned, token refresh logic, OAuth flow correctness) unless an attacker could exploit the bug to bypass access controls or escalate privileges. The key question: "Could a malicious actor exploit this?" If the answer is just "it breaks for legitimate users," that's a bug, not a security issue.
IMPORTANT LABELING RULES:
- Be selective - only apply labels that are clearly relevant
- Don't apply area labels just because a file in that area is mentioned
- The issue must be PRIMARILY about that area to get the label
- When in doubt, don't apply the label
- Apply 2-5 labels total typically (category + maybe priority + maybe 1-2 areas)
LABELING PRINCIPLES:
- Precision over recall: a missing label is a minor inconvenience; a wrong label sends the wrong people to the wrong issue. When in doubt, don't apply.
- Don't apply area labels just because a file in that area is mentioned — the issue must be PRIMARILY about that area.
- Apply 2-5 labels total typically (category + maybe priority + maybe 1-2 areas).
- For ambiguous cases (bug vs enhancement, which area label), prefer the more conservative choice or omit the uncertain label entirely.
META LABELS (rarely needed for issues):
- dependencies: Only for dependabot PRs or issues specifically about package updates
- DON'T MERGE: Only if PR author explicitly states it's not ready
4. Apply selected labels:
Use mcp__github__update_issue to apply your selected labels
DO NOT post any comments
Add them with `bash .github/scripts/triage-label.sh add "label1" "label2"`.
DO NOT post any comments unless applying too-long (see above)
PROMPT_END
EOF
- 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 }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}
allowed_non_write_users: "*"
allowed_bots: "marvin-context-protocol"
claude_args: |
--allowedTools Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__get_pull_request_files
--allowedTools "Bash(gh label list:*)","Bash(bash .github/scripts/triage-label.sh:*)",mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__add_issue_comment,mcp__github__get_pull_request,mcp__github__get_pull_request_files
settings: |
{
"model": "claude-sonnet-4-5-20250929",
"model": "claude-sonnet-5",
"env": {
"GH_TOKEN": "${{ steps.marvin-token.outputs.token }}"
"GH_TOKEN": "${{ steps.marvin-token.outputs.token }}",
"TRIAGE_REPO": "${{ github.repository }}",
"TRIAGE_NUMBER": "${{ github.event.issue.number || github.event.pull_request.number || inputs.issue_number }}"
}
}
# Triage is fire-and-forget: nobody watches a green run, so a broken
# allowlist has to fail the job or it goes unnoticed indefinitely — a
# mangled pattern silently produced zero labels across a dozen PRs
# because the run still reported success.
#
# Only denials of commands we MEANT to grant indicate that breakage. An
# 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 Marvin could not run its tools
if: always() && steps.marvin.conclusion != 'skipped'
env:
EXECUTION_FILE: ${{ steps.marvin.outputs.execution_file }}
run: |
file="${EXECUTION_FILE:-}"
if [[ -z "$file" || ! -s "$file" ]]; then
file="${RUNNER_TEMP}/claude-execution-output.json"
fi
# A missing or empty log means we cannot tell a clean run from a
# blocked one, which is the exact failure this step exists to catch.
if [[ ! -s "$file" ]]; then
echo "::error::No Marvin execution log found; cannot verify tool permissions."
exit 1
fi
# The persisted log carries a `permission_denials` array on each
# `type: result` entry; the `permission_denials_count` scalar only
# appears in the action's condensed stdout summary, never on disk.
# Anchor to result entries rather than recursing with `..`, which
# descends into each denial's `tool_input` and double-counts any
# denied command that happens to mention the field name.
if ! summary=$(jq -sr '
[ .[] | if type == "array" then .[] else . end ]
| map(select(type == "object" and .type == "result"))
| map(.permission_denials // []) | flatten
| map(.tool_input.command // "")
| { total: length,
granted: map(select(
startswith("gh label list")
or startswith("bash .github/scripts/triage-label.sh")
))
}
| "\(.total)\t\(.granted | length)\t\(.granted | join(" | "))"
' "$file"); then
echo "::error::Could not parse Marvin execution log ($file)."
exit 1
fi
IFS=$'\t' read -r total granted commands <<<"$summary"
echo "Denied tool calls: $total (of which allowlisted: $granted)"
if [[ "$granted" -gt 0 ]]; then
echo "::error::Marvin was denied $granted call(s) to tools this workflow grants, so it could not apply labels: ${commands}. The --allowedTools value is not reaching the permission matcher intact — claude_args is lexed with shell-quote, so any Bash(...) pattern containing a space must be quoted or it is split into fragments."
exit 1
fi
if [[ "$total" -gt 0 ]]; then
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@v7
with:
name: marvin-triage-execution-log
path: |
${{ steps.marvin.outputs.execution_file }}
${{ runner.temp }}/claude-execution-output.json
if-no-files-found: ignore
retention-days: 14

View file

@ -0,0 +1,197 @@
name: Marvin Test Failure Analysis
on:
workflow_run:
workflows: ["Tests", "Run static analysis"]
types:
- completed
concurrency:
group: marvin-test-failure-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
jobs:
marvin-test-failure:
# Only run if the test workflow failed
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
issues: read
id-token: write
actions: read # Required for Claude to read CI results
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
fetch-depth: 1
- 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 }}
- name: Set up Python 3.10
uses: actions/setup-python@v7
with:
python-version: "3.10"
# Install UV package manager
- name: Install UV
uses: astral-sh/setup-uv@v7
# Install dependencies
- name: Install dependencies
run: uv sync --all-packages --group dev
- name: Set analysis prompt
id: analysis-prompt
run: |
cat >> $GITHUB_OUTPUT << 'EOF'
PROMPT<<PROMPT_END
You're a test failure analysis assistant for FastMCP, a Python framework for building Model Context Protocol servers and clients.
# Your Task
A GitHub Actions workflow has failed. Your job is to:
1. Analyze the test failure(s) to understand what went wrong
2. Identify the root cause of the failure(s)
3. Suggest a clear, actionable solution to fix the failure(s)
# Response Proportionality
Match your response length to the complexity of the failure. Not every failure needs a full investigation:
**Trivial failures** (formatting, linting) — post a short, direct comment. No collapsible sections, no root-cause deep-dive. Example:
> CI failed: `ruff format` reformatted 2 files. Run `uv run ruff format .` locally and push.
**Pre-existing flaky tests** unrelated to the PR — say so briefly. Don't write a full analysis of a test the PR didn't touch. Example:
> CI failed due to a pre-existing flaky test (`test_name`) unrelated to this PR's changes. Safe to re-run.
**Real failures caused by the PR** — these deserve the full analysis format below. Spend your effort here.
# Getting Started
1. Call the generate_agents_md tool to get a high-level summary of the project
2. Get the pull request associated with this workflow run from the GitHub repository: ${{ github.repository }}
- The workflow run ID is: ${{ github.event.workflow_run.id }}
- The workflow run was triggered by: ${{ github.event.workflow_run.event }}
- Use GitHub MCP tools to get PR details and workflow run information
3. Use the GitHub MCP tools to fetch job logs and failure information:
- Use get_workflow_run to get details about the failed workflow
- Use list_workflow_jobs to see which jobs failed
- Use get_job_logs with failed_only=true to get logs for failed jobs
- Use summarize_run_log_failures to get an AI summary of what failed
4. Analyze the failures to understand the root cause
5. Search the codebase for relevant files, tests, and implementations
# Your Response
Post a comment on the pull request with your analysis.
Lead with a tl;dr — 1-2 sentences that tell the developer what broke and what to do about it. This should be visible without expanding anything.
Push supporting detail into collapsible `<details>` blocks. The reader should be able to act on your comment without expanding a single one. Think of details blocks as appendices — there if someone wants to dig deeper, not required for the main message.
For real (non-trivial) failures, use this structure:
**tl;dr**: What failed and what to do (1-2 sentences, always visible)
**Root Cause**: Why it failed (a short paragraph, always visible)
**Fix**: Specific files and changes needed (always visible)
<details>
<summary>Log excerpts</summary>
Relevant failure output
</details>
<details>
<summary>Related files</summary>
Files relevant to the failure
</details>
# Quality Standards
- Every claim needs evidence: file paths, line numbers, log excerpts. Never say "the test fails" without citing which test and what the error was.
- Focus on facts from the logs and code, not speculation. If you can't determine the root cause, say so clearly — "I don't know" is better than a wrong diagnosis.
- If your only suggestion is a bad one (disable the test, increase the timeout, etc.), say so honestly rather than dressing it up.
- Do not paste raw CLI output (e.g., prek progress bars, pytest collection output) into the comment body. Quote only the relevant failure lines.
- Always include specific file names, tool names, and test names in your summary. Never leave a sentence with a blank where a name should be.
# Self-Review Before Posting
Before posting your comment, re-read it as the PR author would. Ask:
- Can I act on this without expanding any `<details>` block?
- Does every claim cite a specific file, line, or log excerpt?
- Am I telling them something they can't already see in the CI logs, or just restating them?
If your comment doesn't add value beyond what the logs already show, don't post it.
# STOP SIGNALS
If anyone on the PR has asked the bot to stop — e.g., "stop", "go away", "don't comment", "no more bot comments" — exit immediately without further action. This includes past comments in the thread, not just the most recent one.
If you are posting the same suggestion as you have previously made, do not post the suggestion again.
# IMPORTANT: EDIT YOUR COMMENT
Do not post a new comment every time you triage a failing workflow. If a previous comment has been posted by you (marvin)
in a previous triage, edit that comment do not add a new comment for each failure. Be sure to include a note that you've edited
your comment to reflect the latest analysis. Don't worry about keeping the old content around, there's comment history for
that.
# Available Tools
- You can run make commands (e.g., `make lint`, `make typecheck`, `make sync`) to build, test, or lint the code
- You can also run git commands (e.g., `git status`, `git log`, `git diff`) to inspect the repository
- You can use WebSearch and WebFetch to research errors, stack traces, or related issues
- For bash commands, you are limited to make and git commands only
# Problems Encountered
If you encounter any problems during your analysis (e.g., unable to fetch logs, tools not working), document them clearly so the team knows what limitations you faced.
PROMPT_END
EOF
- name: Setup GitHub MCP Server
run: |
mkdir -p /tmp/mcp-config
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
{
"mcpServers": {
"repository-summary": {
"type": "http",
"url": "https://agents-md-generator.fastmcp.app/mcp"
},
"code-search": {
"type": "http",
"url": "https://public-code-search.fastmcp.app/mcp"
},
"github-research": {
"type": "stdio",
"command": "uvx",
"args": [
"github-research-mcp"
],
"env": {
"DISABLE_SUMMARIES": "true",
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
}
}
}
}
EOF
- name: Clean up stale Claude locks
run: rm -rf ~/.claude/.locks ~/.local/state/claude/locks || true
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
github_token: ${{ steps.marvin-token.outputs.token }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY_FOR_CI }}
bot_name: "Marvin Context Protocol"
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
additional_permissions: |
actions: read
prompt: ${{ steps.analysis-prompt.outputs.PROMPT }}
claude_args: |
--allowed-tools mcp__repository-summary,mcp__code-search,mcp__github-research,WebSearch,WebFetch,"Bash(make:*)","Bash(git:*)"
--mcp-config /tmp/mcp-config/mcp-servers.json

View file

@ -0,0 +1,221 @@
# Triage new issues: investigate, recommend, apply labels
# Calls run-claude directly with triage prompt (elastic issue-triage style)
name: Triage Issue
on:
issues:
types: [opened]
jobs:
triage:
if: |
github.event.issue.user.login == 'strawgate' ||
(github.event.issue.user.login == 'jlowin' && contains(toJSON(github.event.issue.labels.*.name), 'bug'))
concurrency:
group: triage-issue-${{ github.event.issue.number }}
cancel-in-progress: true
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
pull-requests: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v7
with:
repository: ${{ github.repository }}
ref: ${{ github.event.repository.default_branch }}
- 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 }}
- name: React to issue with eyes
env:
GH_TOKEN: ${{ steps.marvin-token.outputs.token }}
run: |
gh api "repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/reactions" -f content=eyes 2>/dev/null || true
- name: Run Claude for Triage
uses: ./.github/actions/run-claude
env:
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_TITLE: ${{ github.event.issue.title }}
with:
claude-oauth-token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github-token: ${{ steps.marvin-token.outputs.token }}
allowed-tools: "Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code"
prompt: |
<context>
Repository: ${{ github.repository }}
Issue Number: #${{ github.event.issue.number }}
Issue Title: ${{ env.ISSUE_TITLE }}
Issue Author: ${{ github.event.issue.user.login }}
</context>
<issue_body>
${{ env.ISSUE_BODY }}
</issue_body>
<task>
Triage this new GitHub issue and provide a helpful, actionable response. You can write files and execute commands to test, verify, or investigate the issue.
</task>
<constraints>
This workflow is for investigation, testing, and planning.
You CANNOT: Create branches, checkout branches, commit code to the repository
Do not push changes to the repository.
You CAN: Read/analyze code, search repository, review git history, search for similar issues, write files, verify behavior, provide analysis and recommendations
</constraints>
<allowed_tools>
You have access to the following tools (comma-separated list):
Edit,MultiEdit,Glob,Grep,LS,Read,Write,WebSearch,WebFetch,mcp__github_comment__update_claude_comment,mcp__github_ci__get_ci_status,mcp__github_ci__get_workflow_run_details,mcp__github_ci__download_job_log,Bash(*),mcp__agents-md-generator__generate_agents_md,mcp__public-code-search__search_code
You can only use tools that are explicitly listed above. For Bash commands, the pattern `Bash(command:*)` means you can run that command with any arguments. If a command is not listed, it is not available.
</allowed_tools>
<getting_started>
Use `mcp__agents-md-generator__generate_agents_md` to get repository context before triaging.
</getting_started>
<investigation_tools>
- `mcp__public-code-search__search_code`: Search code in OTHER repositories (use `Grep`/`Read` for this repo)
- `WebSearch`: Search the web for documentation, best practices, or solutions
- `WebFetch`: Fetch and read content from URLs
- Git commands: You have access to git commands, but write commands (commit, push, checkout, branch creation) are blocked
- Write: You can write files (e.g., test files, temporary files for verification)
- Execution: See `<allowed_tools>` section above for exact list of available execution commands
</investigation_tools>
<execution_guidelines>
If execution commands are available (check `<allowed_tools>` section), you can:
- Run tests to verify reported bugs or test proposed solutions
- Execute scripts to understand behavior
- Run linters or static analysis tools
- Verify environment setup or dependencies
- Test specific code paths or scenarios
- Write test files to confirm behavior
When executing commands:
- Explain what you're testing and why
- Include command output in your response when relevant
- Use execution to validate your findings and recommendations
- Only use commands that are explicitly listed in `<allowed_tools>`
</execution_guidelines>
<response_goals>
Your number one priority is to provide a great response to the issue. A great response is a response that is clear, concise, accurate, and actionable. You will avoid long paragraphs, flowery language, and overly verbose responses. Your readers have limited time and attention, so you will be concise and to the point.
In priority order your goal is to:
1. Provide context about the request or issue (related issues, pull requests, files, etc.)
2. Layout a single high-quality and actionable recommendation for how to address the issue based on your knowledge of the project, codebase, and issue
3. Provide a high quality and detailed plan that a junior developer could follow to implement the recommendation
4. Use execution to verify findings when appropriate (check `<allowed_tools>` section for available commands)
Report findings and recommendations — not your process. Do not include task checklists, progress tracking, or "steps I took" narration (e.g., `- [x] Read source code`). The reader cares about what you found, not how you found it.
</response_goals>
<evidence_standards>
Every claim in your response must be grounded in evidence you can cite:
- **Code references**: Always include file path and line number (e.g., `fastmcp_slim/fastmcp/client/client.py:142`). Never say "the client code does X" without pointing to where.
- **Bug confirmation**: If you say a bug is real, show the specific code path that produces it. If you ran a test, include the command and output.
- **Related items**: When citing a related issue or PR, explain specifically why it's related — not just that it exists.
- **Confidence**: If you're uncertain about a finding, say so. "I don't know" or "I couldn't confirm this" is better than a speculative diagnosis. Only report findings you would confidently defend.
</evidence_standards>
<quality_gate>
Before posting, re-read your response as a maintainer would:
- Does the tl;dr give the full picture without expanding anything?
- Does every claim cite a specific file, line, or test result?
- Is this telling the maintainer something they couldn't find in 5 minutes of reading the issue and grepping the code?
If your response doesn't add meaningful value beyond restating the issue, it's okay to post a short "confirmed, straightforward fix in [file]:[line]" response instead of a full analysis.
</quality_gate>
<response_sections>
Populate the following sections in your response:
Recommendation (or "No recommendation" with reason)
Findings
Verification (if you executed tests or commands - check `<allowed_tools>` section)
Detailed Action Plan
Related Items
Related Files
Related Webpages
You may not be able to do all of these things, sometimes you may find that all you can do is provide in-depth context of the issue and related items. That's perfectly acceptable and expected. Your performance is judged by how accurate your findings are, do the investigation required to have high confidence in your findings and recommendations. "I don't know" or "I'm unable to recommend a course of action" is better than a bad or wrong answer.
Structure: Lead with a tl;dr (1-3 sentences, always visible) that gives the reader the bottom line — what this issue is, whether it's valid, and what to do about it. The reader should be able to act on your comment without expanding anything.
Push everything else into collapsible `<details>` blocks: findings, verification output, action plans, related items, related files. These are appendices — valuable for someone who wants to dig deeper, but not required for the main message. The only things that should be visible without clicking are the tl;dr and the recommendation. Short responses (a few sentences) don't need collapsible sections at all.
</response_sections>
<response_examples>
# Example: the tl;dr and recommendation are always visible, everything else is collapsed
**tl;dr**: Confirmed bug — `Calculator.divide` raises `ValueError` instead of `DivisionByZeroError`. PR #654 partially addresses this but is incomplete.
**Recommendation**: Complete PR #654: update `Calculator.divide` to raise `DivisionByZeroError` and update the test assertions to match.
<details>
<summary>Findings</summary>
...details from the code analysis that are relevant to the issue and the recommendation...
</details>
<details>
<summary>Verification</summary>
```bash
$ pytest test_calculator.py::test_divide_by_zero
FAILED - raises ValueError instead of DivisionByZeroError
```
This confirms the issue report is accurate.
</details>
<details>
<summary>Action Plan</summary>
...a detailed plan that a junior developer could follow to implement the recommendation...
</details>
<details>
<summary>Related Issues and Pull Requests</summary>
| Issue or PR | Relevance |
| --- | --- |
| [Add matrix operations support](https://github.com/PrefectHQ/fastmcp/pull/680) | Directly addresses the feature request |
</details>
<details>
<summary>Related Files</summary>
| File | Relevance |
| --- | --- |
| [calculator.py L29-32](https://github.com/modelcontextprotocol/python-sdk/blob/main/calculator.py#L29-L32) | The `divide` method that raises ValueError |
| [test_calculator.py L25-27](https://github.com/modelcontextprotocol/python-sdk/blob/main/test_calculator.py#L25-L27) | Test asserting ValueError (needs updating) |
</details>
</response_examples>
<response_footer>
Always end your comment with a new line, three dashes, and the footer message:
<exact_content>
---
Marvin Context Protocol | Type `/marvin` to interact further
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.
</exact_content>
</response_footer>
<github_formatting>
When writing GitHub comments, wrap branch names, tags, or other @-references in backticks (e.g., `@main`, `@v1.0`) to avoid accidentally pinging users. Do not add backticks around terms that are already inside backticks or code blocks.
Do not write `fixes #N`, `closes #N`, or `resolves #N` in comments — these can accidentally close issues. Use plain `#N` references instead.
</github_formatting>

View file

@ -1,78 +0,0 @@
name: Marvin Context Protocol
on:
issue_comment: { types: [created] }
pull_request_review_comment: { types: [created] }
pull_request_review: { types: [submitted] }
pull_request: { types: [opened, edited] }
issues: { types: [opened, edited, assigned, labeled] }
discussion: { types: [created, edited, labeled] }
discussion_comment: { types: [created] }
permissions:
contents: write
issues: write
pull-requests: write
discussions: write
actions: read
id-token: write
jobs:
marvin:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '/marvin')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '/marvin')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '/marvin')) ||
(github.event_name == 'pull_request' && contains(github.event.pull_request.body, '/marvin')) ||
(github.event_name == 'issues' && contains(github.event.issue.body, '/marvin')) ||
(github.event_name == 'discussion' && contains(github.event.discussion.body, '/marvin')) ||
(github.event_name == 'discussion_comment' && contains(github.event.comment.body, '/marvin')) ||
(github.event_name == 'issues' && github.event.action == 'assigned' && github.event.assignee.login == 'Marvin Context Protocol') ||
(github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'marvin')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
# Install UV package manager
- name: Install UV
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
# Install project dependencies
- name: Install dependencies
run: uv sync --python 3.12
# Install pre-commit hooks automatically
- name: Install pre-commit hooks
run: |
uv run pre-commit install
echo "✅ Pre-commit hooks installed"
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.MARVIN_APP_ID }}
private-key: ${{ secrets.MARVIN_APP_PRIVATE_KEY }}
# Marvin Assistant
- name: Run Marvin
uses: anthropics/claude-code-action@v1
with:
github_token: ${{ steps.marvin-token.outputs.token }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
trigger_phrase: "/marvin"
allowed_bots: "*"
claude_args: |
--allowedTools WebSearch,WebFetch,Bash(uv:*),Bash(pre-commit:*),Bash(pytest:*),Bash(ruff:*),Bash(ty:*),Bash(git:*),Bash(gh:*),mcp__github__add_issue_comment,mcp__github__create_issue,mcp__github__get_issue,mcp__github__list_issues,mcp__github__search_issues,mcp__github__update_issue,mcp__github__update_issue_comment,mcp__github__create_pull_request,mcp__github__get_pull_request,mcp__github__get_pull_request_comments,mcp__github__get_pull_request_files,mcp__github__get_pull_request_reviews,mcp__github__get_pull_request_status,mcp__github__list_pull_requests,mcp__github__update_pull_request,mcp__github__update_pull_request_branch,mcp__github__update_pull_request_comment,mcp__github__merge_pull_request
additional_permissions: |
actions: read
settings: |
{
"model": "claude-sonnet-4-5-20250929",
"env": {
"GH_TOKEN": "${{ steps.marvin-token.outputs.token }}"
}
}

View file

@ -0,0 +1,47 @@
# Minimize resolved PR review comments to reduce noise.
#
# Runs automatically on review activity for same-repo PRs. Fork PRs are
# skipped because GITHUB_TOKEN is read-only in that context. Collaborators
# can comment "/tidy" on any PR (including forks) to trigger manually.
name: Minimize Resolved Reviews
on:
pull_request_review:
types: [submitted]
pull_request_review_comment:
types: [created, edited]
issue_comment:
types: [created]
# Scope the group by event name so that the sibling events fired by a single
# review action (pull_request_review + pull_request_review_comment, same instant)
# don't cancel each other. Same-PR runs of the *same* event still supersede
# cleanly, and the last one always completes.
concurrency:
group: minimize-reviews-${{ github.event.pull_request.number || github.event.issue.number }}-${{ github.event_name }}
cancel-in-progress: true
permissions:
pull-requests: write
jobs:
minimize:
# /tidy comment: collaborators can trigger on any PR (token has write access)
# Review events: skip fork PRs where GITHUB_TOKEN lacks write permissions
if: >-
(
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '/tidy') &&
contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association)
) || (
github.event_name != 'issue_comment' &&
github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name
)
runs-on: ubuntu-latest
steps:
- name: Minimize resolved review comments
uses: strawgate/minimize-resolved-pr-reviews@v0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

View file

@ -0,0 +1,87 @@
name: Publish fastmcp-remote 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-remote 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 }}
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Build fastmcp-remote
run: uv build --package fastmcp-remote
- name: Verify matching fastmcp-slim is published
run: |
SLIM_VERSION=$(python - <<'PY'
import email.parser
import re
import zipfile
from pathlib import Path
wheel = next(Path("dist").glob("fastmcp_remote-*.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-remote." >&2
exit 1
- name: Publish fastmcp-remote to PyPI
run: uv publish -v dist/fastmcp_remote-*.tar.gz dist/fastmcp_remote-*.whl

View file

@ -0,0 +1,30 @@
name: Publish fastmcp-slim to PyPI
on:
release:
types: [published]
workflow_dispatch:
permissions:
contents: read
id-token: write
jobs:
pypi-publish:
name: Upload fastmcp-slim to PyPI
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Build fastmcp-slim
run: uv build --package fastmcp-slim
- name: Publish fastmcp-slim to PyPI
run: uv publish -v dist/fastmcp_slim-*.tar.gz dist/fastmcp_slim-*.whl

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

238
.github/workflows/publish-fastmcp.yml vendored Normal file
View file

@ -0,0 +1,238 @@
name: Publish fastmcp 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 to PyPI
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'release')
outputs:
is_prerelease: ${{ steps.package_version.outputs.is_prerelease }}
version: ${{ steps.package_version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Build fastmcp
run: uv build --package fastmcp
- name: Read built package version
id: package_version
run: |
python - <<'PY' >> "$GITHUB_OUTPUT"
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()
)
version = metadata["Version"]
public_version = version.partition("+")[0]
is_prerelease = bool(
re.search(
r"(?i)(?:^|[0-9.])(?:a|b|c|rc|alpha|beta|pre|preview|dev)[0-9]*",
public_version,
)
)
print(f"version={version}")
print(f"is_prerelease={str(is_prerelease).lower()}")
PY
- name: Verify matching fastmcp-slim is published
run: |
SLIM_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()
)
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." >&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: 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: 5
permissions:
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
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
git fetch origin "${DEFAULT_BRANCH}:refs/remotes/origin/${DEFAULT_BRANCH}"
if git merge-base --is-ancestor HEAD "refs/remotes/origin/${DEFAULT_BRANCH}"; then
echo "update_published_docs=true" >> "$GITHUB_OUTPUT"
else
echo "update_published_docs=false" >> "$GITHUB_OUTPUT"
echo "Release commit is not on ${DEFAULT_BRANCH}; skipping published-docs update."
fi
- name: Prepare published docs tree
if: steps.release_line.outputs.update_published_docs == 'true'
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

@ -1,26 +0,0 @@
name: Publish FastMCP to PyPI
on:
release:
types: [published]
workflow_dispatch:
jobs:
pypi-publish:
name: Upload to PyPI
runs-on: ubuntu-latest
permissions:
id-token: write # For PyPI's trusted publishing
steps:
- name: Checkout
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: "Install uv"
uses: astral-sh/setup-uv@v7
- name: Build
run: uv build
- name: Publish to PyPi
run: uv publish -v dist/*

613
.github/workflows/require-issue-link.yml vendored Normal file
View file

@ -0,0 +1,613 @@
# Require external PRs to reference an issue with an auto-close keyword
# (e.g. "Fixes #123") AND have the PR author assigned to that issue —
# unless the referenced issue is labeled "prs welcome", which waives the
# assignment requirement for everyone (the link itself is still required,
# since that's how the check finds the issue to read the label from).
# Otherwise the PR is labeled "missing-issue-link", commented on, and
# closed. CONTRIBUTING.md requires external contributors to be assigned to
# an issue before opening a PR; this enforces that.
#
# Adapted from langchain-ai/langchain's require_issue_link.yml. Differences:
# - Self-contained: it does NOT depend on a separate labeler workflow
# applying an "external" label first, so it can run on `opened`.
# - "External" is determined authoritatively, in-script, from the PR
# author's repo collaborator permission level — NOT from the event
# payload's author_association. author_association reports MEMBER only
# for *public* org members; a maintainer whose org membership is
# private appears as CONTRIBUTOR/NONE, so gating on it would wrongly
# enforce against private-member maintainers. getCollaboratorPermission
# reflects effective write access regardless of membership visibility.
# - The enforcement path is a single github-script step (the upstream
# version is split across four, forcing the label/comment/reopen helpers
# to be duplicated per scope).
# - Issue assignment events are handled in this same workflow so assigning
# the linked issue reopens previously closed PRs automatically.
#
# Maintainer override: reopen the PR, or remove the "missing-issue-link"
# label — either applies a sticky "bypass-issue-check" label and reopens.
name: Require Issue Link
on:
pull_request_target:
# SECURITY: pull_request_target runs with repo write scope against the
# BASE repo. NEVER check out or execute PR-head code here — it would run
# with these permissions. This workflow only reads the PR payload and
# calls the API; it never checks anything out.
# ready_for_review matters because the job skips drafts: without it a
# draft opened with no issue link would never be checked when it later
# becomes reviewable.
types: [opened, edited, reopened, ready_for_review, labeled, unlabeled]
issues:
# Assignment is what makes a previously closed "not assigned" PR compliant,
# so it needs a separate event path that finds and reopens matching PRs.
types: [assigned]
# Dry run: when 'false' the check still runs and logs its verdict but makes
# NO mutations at all (no label, comment, close, reopen, or failure). Flip
# to 'true' to enforce.
env:
ENFORCE_ISSUE_LINK: "true"
permissions:
contents: read
jobs:
check-issue-link:
# Cheap pre-filters only. Maintainer detection is deliberately NOT done
# here: the job-level `if` can't call the API, and author_association is
# unreliable for private org members (see file header). The job runs,
# then the script resolves the author's real permission and exits early
# for maintainers.
#
# Gate: only run on pull_request_target events. The workflow also listens
# to `issues.assigned` (handled by reopen-on-assignment below), and without
# this guard the job would also fire there — `github.event.pull_request` is
# null on an issues event, so `...draft == false` coerces to true and the
# script then dereferences a missing PR and crashes. Beyond the event type,
# skip drafts, bots, and already-bypassed/trusted PRs, and allow the primary
# actions plus the one maintainer-override action we care about (removing
# the missing-issue-link label).
if: >-
github.event_name == 'pull_request_target' &&
github.event.pull_request.draft == false &&
!endsWith(github.actor, '[bot]') &&
!contains(github.event.pull_request.labels.*.name, 'trusted-contributor') &&
!contains(github.event.pull_request.labels.*.name, 'bypass-issue-check') &&
(
(github.event.action != 'labeled' && github.event.action != 'unlabeled') ||
(github.event.action == 'unlabeled' && github.event.label.name == 'missing-issue-link')
)
runs-on: ubuntu-latest
timeout-minutes: 10
concurrency:
group: require-issue-link-${{ github.event.pull_request.number }}
cancel-in-progress: false
permissions:
issues: write
pull-requests: write
steps:
- name: Enforce issue link
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
const prNumber = pr.number;
const action = context.payload.action;
const enforce = process.env.ENFORCE_ISSUE_LINK === 'true';
const LABEL = 'missing-issue-link';
const MARKER = '<!-- require-issue-link -->';
// Issue-level label that waives the assignment requirement.
const OPEN_LABEL = 'prs welcome';
// Dry-run guard: every mutating call goes through this so that
// ENFORCE_ISSUE_LINK=false means strictly read-only.
async function mutate(description, fn) {
if (!enforce) {
console.log(`[dry-run] would ${description}`);
return;
}
await fn();
}
// Authoritative maintainer check. Uses collaborator permission,
// not org membership or author_association:
// - GITHUB_TOKEN is an app token and is never an org member,
// so the org-membership endpoint always 403s.
// - author_association reports MEMBER only for *public* org
// members; a private-member maintainer shows as
// CONTRIBUTOR/NONE. Permission level is visibility-
// independent and reflects effective access.
// 404 (not a collaborator) → not a maintainer. Other errors
// (rate limit, 5xx) MUST throw: silently treating them as
// "not a maintainer" could wrongly close a maintainer's PR.
// A throw aborts the script before any close/label call, so the
// job fails red and the PR is left untouched — the safe direction.
async function hasWriteAccess(username) {
if (!username) throw new Error('No username — cannot check permissions');
try {
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
owner, repo, username,
});
const ok = ['admin', 'maintain', 'write'].includes(data.permission);
console.log(`${username}: ${data.permission} — ${ok ? 'maintainer' : 'not a maintainer'}`);
return ok;
} catch (e) {
if (e.status === 404) {
console.log(`${username} is not a collaborator — not a maintainer`);
return false;
}
throw new Error(
`Permission check failed for ${username} (HTTP ${e.status ?? 'unknown'}): ${e.message}`,
);
}
}
async function addLabel() {
await mutate(`label PR #${prNumber} "${LABEL}"`, async () => {
try {
await github.rest.issues.getLabel({ owner, repo, name: LABEL });
} catch (e) {
if (e.status !== 404) throw e;
try {
await github.rest.issues.createLabel({ owner, repo, name: LABEL, color: 'b76e79' });
} catch (createErr) {
// 422 = created by a concurrent run between GET and POST.
if (createErr.status !== 422) throw createErr;
}
}
await github.rest.issues.addLabels({
owner, repo, issue_number: prNumber, labels: [LABEL],
});
});
}
async function minimizeStaleComment() {
try {
const comments = await github.paginate(
github.rest.issues.listComments,
{ owner, repo, issue_number: prNumber, per_page: 100 },
);
const stale = comments.find(c => c.body && c.body.includes(MARKER));
if (!stale) return;
await mutate(`minimize stale comment ${stale.id}`, () => github.graphql(`
mutation($id: ID!) {
minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) {
minimizedComment { isMinimized }
}
}
`, { id: stale.node_id }));
} catch (e) {
core.warning(`Could not minimize stale comment on PR #${prNumber}: ${e.message}`);
}
}
// Shared "this PR passes" cleanup: drop the label, reopen, and
// retire any stale enforcement comment.
//
// For the normal pass paths we only reopen if THIS workflow had
// closed the PR — inferred from the label still being on the
// payload. The maintainer-override paths pass forceReopen: the
// `unlabeled` event payload no longer carries the just-removed
// label, so the heuristic can't see it; without forcing, the
// advertised "remove the label to bypass" gesture would leave
// the PR closed.
async function clearEnforcement(forceReopen = false) {
await mutate(`remove "${LABEL}" from PR #${prNumber}`, async () => {
try {
await github.rest.issues.removeLabel({
owner, repo, issue_number: prNumber, name: LABEL,
});
} catch (e) {
if (e.status !== 404) throw e;
}
});
const hadLabel = pr.labels.map(l => l.name).includes(LABEL);
if (pr.state === 'closed' && (forceReopen || hadLabel)) {
await mutate(`reopen PR #${prNumber}`, async () => {
await github.rest.pulls.update({
owner, repo, pull_number: prNumber, state: 'open',
});
});
}
await minimizeStaleComment();
}
async function applyBypass(reason) {
console.log(reason);
await clearEnforcement(true);
await mutate(`add sticky "bypass-issue-check" to PR #${prNumber}`, async () => {
try {
await github.rest.issues.getLabel({ owner, repo, name: 'bypass-issue-check' });
} catch (e) {
if (e.status !== 404) throw e;
try {
await github.rest.issues.createLabel({
owner, repo, name: 'bypass-issue-check', color: '0e8a16',
});
} catch (createErr) {
if (createErr.status !== 422) throw createErr;
}
}
await github.rest.issues.addLabels({
owner, repo, issue_number: prNumber, labels: ['bypass-issue-check'],
});
});
}
// ── Maintainer-authored PRs are exempt entirely ────────────────
if (await hasWriteAccess(pr.user.login)) {
console.log(`PR author ${pr.user.login} has write access — exempt`);
await clearEnforcement();
return;
}
const sender = context.payload.sender?.login;
// ── Maintainer override: removed the "missing-issue-link" label ─
if (action === 'unlabeled') {
if (await hasWriteAccess(sender)) {
await applyBypass(`Maintainer ${sender} removed ${LABEL} from PR #${prNumber} — bypassing`);
return;
}
// Only triage/admin can manage labels, so a non-write actor
// reaching here is rare (triage role). Fall through to the
// normal check, which recomputes link + assignment and
// re-enforces with the correct message if still failing.
console.log(`Non-maintainer ${sender} removed ${LABEL} — re-checking`);
}
// ── Maintainer override: reopened a PR we had closed ───────────
if (
action === 'reopened' &&
pr.labels.map(l => l.name).includes(LABEL) &&
(await hasWriteAccess(sender))
) {
await applyBypass(`Maintainer ${sender} reopened PR #${prNumber} — bypassing`);
return;
}
// ── Race guard: re-read live labels ────────────────────────────
const { data: liveLabels } = await github.rest.issues.listLabelsOnIssue({
owner, repo, issue_number: prNumber,
});
const liveNames = liveLabels.map(l => l.name);
if (liveNames.includes('trusted-contributor') || liveNames.includes('bypass-issue-check')) {
console.log('PR carries trusted-contributor or bypass-issue-check — clearing any prior enforcement');
await clearEnforcement();
return;
}
// ── The actual check: an auto-close keyword + issue number ─────
const body = pr.body || '';
// Match GitHub's auto-close keywords against any reference form
// that GitHub itself honors: bare `#123`, the `owner/repo#123`
// shorthand, and the full issue URL. Scope the qualified forms to
// THIS repo — GitHub only auto-closes same-repo issues, so a
// cross-repo reference must not be resolved against our numbering.
const repoRef = `${owner}/${repo}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const pattern = new RegExp(
'(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s*:?\\s*' +
`(?:${repoRef}#|#|https?://github\\.com/${repoRef}/issues/)(\\d+)`,
'gi',
);
const matches = [...body.matchAll(pattern)];
if (matches.length === 0) {
console.log('No issue link found in PR body');
await enforceFailure('no-link');
return;
}
// The author must be assigned to at least one linked issue.
// CONTRIBUTING.md requires external contributors to be assigned
// before opening a PR (so maintainers can deconflict / steer
// approach first).
//
// Exception: an issue labeled OPEN_LABEL waives that requirement
// for everyone. It's how maintainers advertise "the reporter
// isn't implementing this, we'd take a PR from anyone" without
// having to assign a specific person up front. Unlike the
// PR-level `trusted-contributor` / `bypass-issue-check` escapes,
// this one lives on the *issue* and is set ahead of time.
const MAX_ISSUES = 5;
const allNumbers = [...new Set(matches.map(m => parseInt(m[1], 10)))];
const numbers = allNumbers.slice(0, MAX_ISSUES);
if (allNumbers.length > MAX_ISSUES) {
core.warning(`PR references ${allNumbers.length} issues — checking only the first ${MAX_ISSUES}`);
}
const prAuthor = pr.user.login.toLowerCase();
let sawRealIssue = false;
let assignedToAny = false;
for (const num of numbers) {
let issue;
try {
({ data: issue } = await github.rest.issues.get({
owner, repo, issue_number: num,
}));
} catch (e) {
if (e.status === 404) {
console.log(`#${num} does not exist — ignoring`);
continue;
}
// Same safe-direction rule as hasWriteAccess: a transient
// error must not be read as "not assigned" and close the PR.
throw new Error(`Cannot fetch issue #${num} (HTTP ${e.status ?? 'unknown'}): ${e.message}`);
}
sawRealIssue = true;
// GitHub returns labels as objects here, but the REST schema
// permits bare strings — normalize both rather than assume.
const labelNames = (issue.labels || [])
.map(l => (typeof l === 'string' ? l : l && l.name))
.filter(Boolean)
.map(n => n.toLowerCase());
if (labelNames.includes(OPEN_LABEL)) {
console.log(`#${num} is labeled "${OPEN_LABEL}" — assignment not required`);
assignedToAny = true;
break;
}
const assignees = (issue.assignees || []).map(a => a.login.toLowerCase());
if (assignees.includes(prAuthor)) {
console.log(`PR author ${pr.user.login} is assigned to #${num}`);
assignedToAny = true;
break;
}
console.log(`PR author ${pr.user.login} is NOT assigned to #${num} (assignees: ${assignees.join(', ') || 'none'})`);
}
if (!sawRealIssue) {
console.log('Referenced issue(s) do not exist');
await enforceFailure('no-link');
return;
}
if (!assignedToAny) {
await enforceFailure('not-assigned');
return;
}
console.log('Linked and assigned — clearing any prior enforcement');
await clearEnforcement();
// ── Label, comment, close, and fail ────────────────────────────
// `kind`: 'no-link' (no valid issue reference) or 'not-assigned'
// (referenced an issue, but the author isn't assigned to it).
async function enforceFailure(kind) {
await addLabel();
const reason = kind === 'no-link'
? "it doesn't reference a tracked issue assigned to you"
: "you aren't assigned to the issue it references";
const steps = kind === 'no-link'
? [
`1. Find or [open an issue](https://github.com/${owner}/${repo}/issues/new/choose) describing the change — if you open it, you have first claim on it.`,
"2. Add `Fixes #<issue>`, `Closes #<issue>`, or `Resolves #<issue>` to **this** PR's description — edit it in place, don't open a new PR.",
]
: [
"1. If you opened the linked issue, a maintainer will assign you when they pick it up and this PR reopens automatically. If someone else opened it, the PR reopens only if a maintainer chooses to assign it to you — please don't comment to ask.",
];
const commentBody = [
MARKER,
"**Don't open a new pull request — this one reopens on its own.** It's closed for " +
`now because ${reason}, but the moment that's fixed it reopens automatically. Keep this ` +
'PR and edit it; opening a fresh duplicate just starts you over and creates more to triage.',
'',
`Per [CONTRIBUTING.md](https://github.com/${owner}/${repo}/blob/main/CONTRIBUTING.md), an external PR must reference an issue that's assigned to its author. To get there:`,
'',
...steps,
'',
"Once you're assigned and the link is present, this PR reopens automatically — no further action needed.",
'',
`*Maintainers: reopen this PR or remove the \`${LABEL}\` label to bypass this check.*`,
].join('\n');
const comments = await github.paginate(
github.rest.issues.listComments,
{ owner, repo, issue_number: prNumber, per_page: 100 },
);
const existing = comments.find(c => c.body && c.body.includes(MARKER));
if (!existing) {
await mutate(`comment on PR #${prNumber}`, () => github.rest.issues.createComment({
owner, repo, issue_number: prNumber, body: commentBody,
}));
} else if (existing.body !== commentBody) {
await mutate(`update comment ${existing.id}`, () => github.rest.issues.updateComment({
owner, repo, comment_id: existing.id, body: commentBody,
}));
} else {
console.log('Requirement comment already present — skipping');
}
if (pr.state === 'open') {
await mutate(`close PR #${prNumber}`, () => github.rest.pulls.update({
owner, repo, pull_number: prNumber, state: 'closed',
}));
}
if (enforce) {
core.setFailed(
kind === 'no-link'
? 'PR must reference a tracked issue using an auto-close keyword (e.g. "Fixes #123").'
: 'PR author must be assigned to the referenced issue.',
);
}
}
reopen-on-assignment:
if: github.event_name == 'issues' && github.event.action == 'assigned' && !github.event.issue.pull_request
runs-on: ubuntu-latest
timeout-minutes: 10
concurrency:
group: reopen-on-assignment-${{ github.event.issue.number }}-${{ github.event.assignee.login }}
cancel-in-progress: false
permissions:
actions: write
issues: write
pull-requests: write
steps:
- name: Reopen linked PRs
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { owner, repo } = context.repo;
const issueNumber = context.payload.issue.number;
const assignee = context.payload.assignee.login;
const enforce = process.env.ENFORCE_ISSUE_LINK === 'true';
const LABEL = 'missing-issue-link';
const MARKER = '<!-- require-issue-link -->';
// Match GitHub's auto-close keywords against any reference form
// that GitHub itself honors: bare `#123`, the `owner/repo#123`
// shorthand, and the full issue URL. Scope the qualified forms to
// THIS repo — GitHub only auto-closes same-repo issues, so a
// cross-repo reference must not be resolved against our numbering.
const repoRef = `${owner}/${repo}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const pattern = new RegExp(
'(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\\s*:?\\s*' +
`(?:${repoRef}#|#|https?://github\\.com/${repoRef}/issues/)(\\d+)`,
'gi',
);
async function mutate(description, fn) {
if (!enforce) {
console.log(`[dry-run] would ${description}`);
return;
}
await fn();
}
console.log(`Issue #${issueNumber} assigned to ${assignee} — searching for closed PRs to reopen`);
const q = [
'is:pr',
'is:closed',
`author:${assignee}`,
`label:${LABEL}`,
`repo:${owner}/${repo}`,
].join(' ');
let search;
try {
({ data: search } = await github.rest.search.issuesAndPullRequests({
q,
per_page: 30,
}));
} catch (e) {
throw new Error(
`Failed to search closed PRs for ${assignee} after assigning #${issueNumber} ` +
`(HTTP ${e.status ?? 'unknown'}): ${e.message}`,
);
}
if (search.total_count === 0) {
console.log('No matching closed PRs found');
return;
}
console.log(`Found ${search.total_count} candidate PR(s)`);
for (const item of search.items) {
const prNumber = item.number;
let issue;
try {
({ data: issue } = await github.rest.issues.get({
owner, repo, issue_number: prNumber,
}));
} catch (e) {
throw new Error(`Cannot fetch PR #${prNumber} issue data (HTTP ${e.status ?? 'unknown'}): ${e.message}`);
}
const labels = (issue.labels || []).map(label => label.name);
if (labels.includes('bypass-issue-check')) {
console.log(`PR #${prNumber} already has bypass-issue-check — skipping`);
continue;
}
const body = issue.body || '';
const referencedIssues = [...body.matchAll(pattern)].map(match => parseInt(match[1], 10));
if (!referencedIssues.includes(issueNumber)) {
console.log(`PR #${prNumber} does not reference #${issueNumber} — skipping`);
continue;
}
try {
await mutate(`reopen PR #${prNumber}`, () => github.rest.pulls.update({
owner, repo, pull_number: prNumber, state: 'open',
}));
} catch (e) {
if (e.status === 422) {
core.warning(`Cannot reopen PR #${prNumber}: the head branch was likely deleted`);
await mutate(`comment on unreopenable PR #${prNumber}`, () => github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body:
`You have been assigned to #${issueNumber}, but this PR could not be ` +
'reopened because the head branch has been deleted. Please open a new PR ' +
'referencing the issue.',
}));
continue;
}
throw e;
}
await mutate(`remove "${LABEL}" from PR #${prNumber}`, async () => {
try {
await github.rest.issues.removeLabel({
owner, repo, issue_number: prNumber, name: LABEL,
});
} catch (e) {
if (e.status !== 404) throw e;
}
});
try {
const comments = await github.paginate(
github.rest.issues.listComments,
{ owner, repo, issue_number: prNumber, per_page: 100 },
);
const stale = comments.find(comment => comment.body && comment.body.includes(MARKER));
if (stale) {
await mutate(`minimize stale comment ${stale.id}`, () => github.graphql(`
mutation($id: ID!) {
minimizeComment(input: {subjectId: $id, classifier: OUTDATED}) {
minimizedComment { isMinimized }
}
}
`, { id: stale.node_id }));
}
} catch (e) {
core.warning(`Could not minimize stale comment on PR #${prNumber}: ${e.message}`);
}
try {
const { data: pr } = await github.rest.pulls.get({
owner, repo, pull_number: prNumber,
});
const { data: runs } = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'require-issue-link.yml',
head_sha: pr.head.sha,
status: 'failure',
per_page: 1,
});
if (runs.workflow_runs.length === 0) {
console.log(`No failed require-issue-link runs found for PR #${prNumber}`);
continue;
}
await mutate(`re-run failed require-issue-link run for PR #${prNumber}`, () =>
github.rest.actions.reRunWorkflowFailedJobs({
owner, repo, run_id: runs.workflow_runs[0].id,
}),
);
} catch (e) {
core.warning(`Could not re-run require-issue-link for PR #${prNumber}: ${e.message}`);
}
}

View file

@ -0,0 +1,55 @@
name: Schema Crash Test
on:
push:
branches: ["main"]
paths:
- "fastmcp_slim/fastmcp/utilities/json_schema_type.py"
- "fastmcp_slim/fastmcp/utilities/json_schema.py"
- "fastmcp_slim/fastmcp/utilities/openapi/**"
- "fastmcp_slim/fastmcp/server/providers/openapi/**"
- "fastmcp_slim/fastmcp/client/mixins/tools.py"
- "tests/utilities/json_schema_type/test_real_world_schemas.py"
- ".github/workflows/run-schema-crash-test.yml"
pull_request:
paths:
- "fastmcp_slim/fastmcp/utilities/json_schema_type.py"
- "fastmcp_slim/fastmcp/utilities/json_schema.py"
- "fastmcp_slim/fastmcp/utilities/openapi/**"
- "fastmcp_slim/fastmcp/server/providers/openapi/**"
- "fastmcp_slim/fastmcp/client/mixins/tools.py"
- "tests/utilities/json_schema_type/test_real_world_schemas.py"
- ".github/workflows/run-schema-crash-test.yml"
workflow_dispatch:
permissions:
contents: read
jobs:
schema_crash_test:
name: "Real-world schema crash test (232K schemas)"
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Set up Python
run: uv python install 3.12
- name: Install dependencies
run: uv sync
- name: Clone openapi-directory
run: git clone --depth 1 https://github.com/APIs-guru/openapi-directory.git /tmp/openapi-directory
- name: Run schema crash test
env:
RUN_REAL_WORLD_SCHEMA_TEST: "1"
OPENAPI_DIRECTORY_PATH: /tmp/openapi-directory
run: uv run pytest tests/utilities/json_schema_type/test_real_world_schemas.py -m integration -v -n auto --timeout-method=thread

View file

@ -1,18 +1,18 @@
name: Run static analysis
env:
# enable colored output
# https://github.com/pytest-dev/pytest/issues/7443
PY_COLORS: 1
on:
push:
branches: ["main"]
paths:
- "src/**"
- "fastmcp_slim/**"
- "fastmcp_remote/**"
- "tests/**"
- "uv.lock"
- "examples/**"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/**"
# run on all pull requests because these checks are required and will block merges otherwise
@ -26,29 +26,17 @@ permissions:
jobs:
static_analysis:
timeout-minutes: 2
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install uv
uses: astral-sh/setup-uv@v7
- uses: actions/checkout@v7
- name: Setup uv
uses: ./.github/actions/setup-uv
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
resolution: locked
- name: Install dependencies
run: uv sync
- name: Check lockfile is up to date
run: |
if ! uv lock --check; then
echo "❌ Lockfile is out of date!"
echo "To update the lockfile, run 'uv lock'."
exit 1
fi
echo "✅ Lockfile is up to date"
- name: Run pre-commit
run: uv run pre-commit run --all-files
- name: Run prek
uses: j178/prek-action@v2
env:
SKIP: no-commit-to-branch

View file

@ -1,26 +1,21 @@
name: Tests
env:
# enable colored output
PY_COLORS: 1
on:
push:
branches: ["main"]
paths:
- "src/**"
- "fastmcp_slim/**"
- "fastmcp_remote/**"
- "tests/**"
- "uv.lock"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/**"
# run on all pull requests because these checks are required and will block merges otherwise
pull_request:
paths:
- "src/**"
- "tests/**"
- "uv.lock"
- "pyproject.toml"
- ".github/workflows/**"
workflow_dispatch:
@ -29,64 +24,244 @@ permissions:
jobs:
run_tests:
name: "Run tests: Python ${{ matrix.python-version }} on ${{ matrix.os }}"
name: "Tests: Python ${{ matrix.python-version }} on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.10"]
include:
- os: ubuntu-latest
python-version: "3.13"
fail-fast: false
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Setup uv
uses: ./.github/actions/setup-uv
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: ${{ matrix.python-version }}
resolution: locked
- name: Install FastMCP
# run with upgrade to always test against the latest compatible versions
run: uv sync --upgrade
- name: Run unit tests
uses: ./.github/actions/run-pytest
- name: Run tests (excluding integration and client_process)
run: |
if [ "${{ matrix.os }}" = "windows-latest" ]; then
uv run pytest --inline-snapshot=disable tests -m "not integration and not client_process"
else
uv run pytest --inline-snapshot=disable tests -m "not integration and not client_process" --numprocesses auto --maxprocesses 4 --dist worksteal
fi
shell: bash
- name: Run serial subprocess tests
uses: ./.github/actions/run-pytest
with:
test-type: client_process
- name: Run client process tests separately
run: uv run pytest --inline-snapshot=disable tests -m "client_process" -x
run_integration_tests:
name: "Run integration tests"
run_tests_lowest_direct:
name: "Tests with lowest-direct dependencies"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v7
- name: Install uv
uses: astral-sh/setup-uv@v7
- name: Setup uv (lowest-direct)
uses: ./.github/actions/setup-uv
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.10"
resolution: lowest-direct
- name: Install FastMCP
# run with upgrade to always test against the latest compatible versions
run: uv sync --upgrade
- name: Run unit tests
uses: ./.github/actions/run-pytest
- name: Run serial subprocess tests
uses: ./.github/actions/run-pytest
with:
test-type: client_process
run_conformance_tests:
name: "MCP conformance tests"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- name: Setup uv
uses: ./.github/actions/setup-uv
with:
resolution: locked
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: "22"
- name: Run conformance tests
uses: ./.github/actions/run-pytest
with:
test-type: conformance
run_integration_tests:
name: "Integration tests"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- name: Setup uv
uses: ./.github/actions/setup-uv
with:
resolution: locked
- name: Run integration tests
# use longer per-test timeout than the default 3s
run: uv run pytest tests -m "integration" --timeout=15 --numprocesses auto --maxprocesses 2 --dist worksteal
uses: ./.github/actions/run-pytest
with:
test-type: integration
env:
FASTMCP_GITHUB_TOKEN: ${{ secrets.FASTMCP_GITHUB_TOKEN }}
FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID }}
FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET }}
package_install_smoke:
name: "Package install smoke"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- name: Setup uv
uses: ./.github/actions/setup-uv
with:
resolution: locked
- name: Build package wheels
run: uv build --all-packages --wheel --out-dir /tmp/fastmcp-dist
- name: Install bare slim wheel
run: |
uv venv /tmp/fastmcp-slim-bare-smoke
SLIM_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp_slim-*.whl)
uv pip install --python /tmp/fastmcp-slim-bare-smoke/bin/python "$SLIM_WHEEL"
/tmp/fastmcp-slim-bare-smoke/bin/python - <<'PY'
from importlib.metadata import entry_points
import fastmcp
import fastmcp.settings
assert any(ep.name == "fastmcp" for ep in entry_points(group="console_scripts"))
try:
from fastmcp.cli import app
except ImportError as exc:
assert "FastMCP CLI support is not installed" in str(exc)
else:
raise AssertionError(f"bare fastmcp-slim unexpectedly imported CLI app {app!r}")
try:
fastmcp.FastMCP
except ImportError as exc:
assert "fastmcp-slim[server]" in str(exc)
else:
raise AssertionError("bare fastmcp-slim unexpectedly imported FastMCP")
PY
- name: Install client slim wheel
run: |
uv venv /tmp/fastmcp-slim-client-smoke
SLIM_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp_slim-*.whl)
uv pip install --python /tmp/fastmcp-slim-client-smoke/bin/python "${SLIM_WHEEL}[client]"
/tmp/fastmcp-slim-client-smoke/bin/python - <<'PY'
from importlib.metadata import entry_points
from fastmcp import Client
from fastmcp.client.transports import StdioTransport, StreamableHttpTransport
from fastmcp.mcp_config import MCPConfig
assert any(ep.name == "fastmcp" for ep in entry_points(group="console_scripts"))
try:
from fastmcp.cli import app
except ImportError as exc:
assert "FastMCP CLI support is not installed" in str(exc)
else:
raise AssertionError(f"client-only slim unexpectedly imported CLI app {app!r}")
assert Client("https://example.com/mcp")
assert StreamableHttpTransport("https://example.com/mcp")
assert StdioTransport(command="uvx", args=["demo"])
assert MCPConfig.from_dict({"mcpServers": {"demo": {"url": "https://example.com/mcp"}}})
try:
from fastmcp import FastMCP
except ImportError as exc:
assert "fastmcp-slim[server]" in str(exc)
else:
raise AssertionError(f"client-only slim unexpectedly imported {FastMCP!r}")
PY
- name: Install server slim wheel
run: |
uv venv /tmp/fastmcp-slim-server-smoke
SLIM_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp_slim-*.whl)
uv pip install --python /tmp/fastmcp-slim-server-smoke/bin/python "${SLIM_WHEEL}[server]"
/tmp/fastmcp-slim-server-smoke/bin/python - <<'PY'
from importlib.metadata import entry_points
from fastmcp import FastMCP
from fastmcp.cli import app
assert any(
ep.name == "fastmcp" and ep.value == "fastmcp.cli:app"
for ep in entry_points(group="console_scripts")
)
mcp = FastMCP("smoke")
assert app is not None
assert mcp.name == "smoke"
PY
- name: Install full package from matching local wheels
run: |
uv venv /tmp/fastmcp-full-smoke
FULL_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp-*.whl)
uv pip install --python /tmp/fastmcp-full-smoke/bin/python --prerelease=allow --find-links /tmp/fastmcp-dist "$FULL_WHEEL"
/tmp/fastmcp-full-smoke/bin/python - <<'PY'
from importlib.metadata import entry_points
from importlib.metadata import requires
from fastmcp import Client, FastMCP
from fastmcp.client.client import CallToolResult
from fastmcp.exceptions import ToolError
fastmcp_reqs = requires("fastmcp") or []
assert any("fastmcp-slim[client,server]" in req for req in fastmcp_reqs)
assert not any("fastmcp-slim[full" in req for req in fastmcp_reqs)
assert any(
ep.name == "fastmcp" and ep.value == "fastmcp.cli:app"
for ep in entry_points(group="console_scripts")
)
assert Client("https://example.com/mcp")
assert FastMCP("smoke").name == "smoke"
assert CallToolResult is not None
assert ToolError is not None
PY
- name: Install fastmcp-remote from matching local wheels
run: |
uv venv /tmp/fastmcp-remote-smoke
REMOTE_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp_remote-*.whl)
uv pip install --python /tmp/fastmcp-remote-smoke/bin/python --prerelease=allow --find-links /tmp/fastmcp-dist "$REMOTE_WHEEL"
/tmp/fastmcp-remote-smoke/bin/python - <<'PY'
from importlib.metadata import entry_points
from importlib.metadata import requires
from fastmcp_remote.cli import build_parser
remote_reqs = requires("fastmcp-remote") or []
assert any("fastmcp-slim[client,server]" in req for req in remote_reqs)
assert any(
ep.name == "fastmcp-remote" and ep.value == "fastmcp_remote.cli:main"
for ep in entry_points(group="console_scripts")
)
assert build_parser().prog == "fastmcp-remote"
PY

157
.github/workflows/run-upgrade-checks.yml vendored Normal file
View file

@ -0,0 +1,157 @@
name: Upgrade checks
env:
PY_COLORS: 1
on:
push:
branches: ["main"]
paths:
- "fastmcp_slim/**"
- "tests/**"
- "pyproject.toml"
- "uv.lock"
- ".github/workflows/**"
schedule:
# Run daily at 2 AM UTC
- cron: "0 2 * * *"
workflow_dispatch:
permissions:
contents: read
issues: write
jobs:
static_analysis:
name: Static analysis
timeout-minutes: 2
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup uv (upgrade)
uses: ./.github/actions/setup-uv
with:
resolution: upgrade
- name: Run prek
uses: j178/prek-action@v2
env:
SKIP: no-commit-to-branch
run_tests:
name: "Tests: Python ${{ matrix.python-version }} on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
python-version: ["3.10"]
include:
- os: ubuntu-latest
python-version: "3.13"
fail-fast: false
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- name: Setup uv (upgrade)
uses: ./.github/actions/setup-uv
with:
python-version: ${{ matrix.python-version }}
resolution: upgrade
- name: Run unit tests
uses: ./.github/actions/run-pytest
- name: Run serial subprocess tests
uses: ./.github/actions/run-pytest
with:
test-type: client_process
run_integration_tests:
name: "Integration tests"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- name: Setup uv (upgrade)
uses: ./.github/actions/setup-uv
with:
resolution: upgrade
- name: Run integration tests
uses: ./.github/actions/run-pytest
with:
test-type: integration
env:
FASTMCP_GITHUB_TOKEN: ${{ secrets.FASTMCP_GITHUB_TOKEN }}
FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID }}
FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET }}
notify:
name: Notify on failure
needs: [static_analysis, run_tests, run_integration_tests]
if: failure() && github.event.pull_request == null
runs-on: ubuntu-latest
steps:
- name: Create or update failure issue
uses: jayqi/failed-build-issue-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
label: "build failed"
title-template: "Upgrade checks failing on main branch"
body-template: |
## Upgrade Checks Failure on Main Branch
The upgrade checks workflow has failed on the main branch.
**Workflow Run**: [#{{runNumber}}]({{serverUrl}}/{{repo.owner}}/{{repo.repo}}/actions/runs/{{runId}})
**Commit**: {{sha}}
**Branch**: {{ref}}
**Event**: {{eventName}}
### Common causes
- **ty (type checker)**: New ty releases frequently add stricter checks that flag previously-accepted code. Run `uv run ty check` locally with the latest ty to reproduce. Fix the type errors or bump the ty version floor in `pyproject.toml`.
- **ruff**: New lint rules or stricter defaults in a ruff upgrade.
- **MCP SDK**: Breaking changes in the `mcp` package (new method signatures, renamed types).
### What to do
1. Check the workflow logs to identify which job failed (static analysis vs tests)
2. Reproduce locally with `uv sync --upgrade && uv run prek run --all-files && uv run pytest -n auto`
3. Fix the code or adjust dependency constraints as needed
---
*This issue was automatically created by a GitHub Action.*
close-on-success:
name: Close issue on success
needs: [static_analysis, run_tests, run_integration_tests]
if: success() && github.event.pull_request == null && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Close resolved failure issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
issue=$(gh issue list \
--repo "$GITHUB_REPOSITORY" \
--label "build failed" \
--state open \
--json number \
--jq '.[0].number // empty')
if [ -n "$issue" ]; then
gh issue close "$issue" \
--repo "$GITHUB_REPOSITORY" \
--comment "Upgrade checks are passing again as of [\`${GITHUB_SHA::7}\`](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/commit/${GITHUB_SHA})."
fi

View file

@ -1,14 +1,14 @@
name: Update MCPServerConfig Schema
# This workflow runs on merges to main to automatically update the config schema
# by creating a PR when changes are needed.
# Regenerates config schema on pushes to main and opens a long-lived PR
# with the changes, so contributor PRs stay clean.
on:
push:
branches: ["main"]
paths:
- "src/fastmcp/utilities/mcp_server_config/**"
- "!src/fastmcp/utilities/mcp_server_config/v1/schema.json" # Exclude the local schema file
- "fastmcp_slim/fastmcp/utilities/mcp_server_config/**"
- "!fastmcp_slim/fastmcp/utilities/mcp_server_config/v1/schema.json"
workflow_dispatch:
permissions:
@ -21,15 +21,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
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:
token: ${{ steps.marvin-token.outputs.token }}
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
@ -41,31 +43,15 @@ jobs:
- name: Generate config schema
run: |
echo "🔄 Generating fastmcp.json schema..."
# Generate schema in docs/public for web access
uv run python -c "
from fastmcp.utilities.mcp_server_config import generate_schema
generate_schema('docs/public/schemas/fastmcp.json/latest.json')
print('✅ Latest schema generated in docs/public')
"
# Also update the v1 schema in docs/public
uv run python -c "
from fastmcp.utilities.mcp_server_config import generate_schema
generate_schema('docs/public/schemas/fastmcp.json/v1.json')
print('✅ v1 schema generated in docs/public')
"
# Generate schema in the source directory for local development
uv run python -c "
from fastmcp.utilities.mcp_server_config import generate_schema
generate_schema('src/fastmcp/utilities/mcp_server_config/v1/schema.json')
print('✅ Schema generated in utilities/mcp_server_config/v1/')
generate_schema('fastmcp_slim/fastmcp/utilities/mcp_server_config/v1/schema.json')
"
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@v8
with:
token: ${{ steps.marvin-token.outputs.token }}
commit-message: "chore: Update fastmcp.json schema"
@ -73,9 +59,9 @@ jobs:
body: |
This PR updates the fastmcp.json schema files to match the current source code.
The schema is automatically generated from `src/fastmcp/utilities/mcp_server_config/` to ensure consistency.
The schema is automatically generated from `fastmcp_slim/fastmcp/utilities/mcp_server_config/` to ensure consistency.
**Note:** This PR is fully automated and will update itself with any subsequent changes to the schema, or close automatically if the schema becomes up-to-date through other means. Feel free to leave it open until you're ready to merge.
**Note:** This PR is fully automated and will update itself with any subsequent changes to the schema, or close automatically if the schema becomes up-to-date through other means.
🤖 Generated by Marvin
branch: marvin/update-config-schema
@ -84,8 +70,3 @@ jobs:
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>"
- name: Summary
run: |
echo "✅ Config schema generation workflow completed"
echo "PR will be created if there are changes, or closed if schema is already up to date"

View file

@ -1,13 +1,13 @@
name: Update SDK Documentation
# This workflow runs on merges to main to automatically update SDK docs
# by creating a PR when changes are needed.
# Regenerates SDK docs on pushes to main and opens a long-lived PR
# with the changes, so contributor PRs stay clean.
on:
push:
branches: ["main"]
paths:
- "src/**"
- "fastmcp_slim/**"
- "pyproject.toml"
workflow_dispatch:
@ -21,15 +21,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Generate Marvin App token
id: marvin-token
uses: actions/create-github-app-token@v2
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:
token: ${{ steps.marvin-token.outputs.token }}
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
@ -40,15 +42,13 @@ jobs:
run: uv sync --python 3.12
- name: Install just
uses: extractions/setup-just@v3
uses: extractions/setup-just@v4
- name: Generate SDK documentation
run: |
echo "🔄 Generating SDK documentation..."
just api-ref-all
run: just api-ref-all
- name: Create Pull Request
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@v8
with:
token: ${{ steps.marvin-token.outputs.token }}
commit-message: "chore: Update SDK documentation"
@ -67,8 +67,3 @@ jobs:
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>"
- name: Summary
run: |
echo "✅ SDK documentation generation workflow completed"
echo "PR will be created if there are changes, or closed if documentation is already up to date"

4
.gitignore vendored
View file

@ -9,6 +9,7 @@ wheels/
*.egg
MANIFEST
.pytest_cache/
.loq_cache
.coverage
htmlcov/
.tox/
@ -53,6 +54,7 @@ dmypy.json
# Local development
.python-version
.envrc
.envrc.private
.direnv/
# Logs and databases
@ -63,11 +65,13 @@ dmypy.json
# Claude worktree management
.claude-wt/worktrees
.claude/worktrees/
# Agents
/PLAN.md
/TODO.md
/STATUS.md
plans/
# Common FastMCP test files
/test.py

View file

@ -6,15 +6,15 @@ repos:
hooks:
- id: validate-pyproject
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.1.0
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.8.4
hooks:
- id: prettier
types_or: [yaml, json5]
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.1
rev: v0.14.10
hooks:
# Run the linter.
- id: ruff-check
@ -26,13 +26,20 @@ repos:
hooks:
- id: ty
name: ty check
entry: uv run ty check
entry: uv run --isolated ty check
language: system
types: [python]
files: ^src/|^tests/
files: ^fastmcp_slim/|^tests/|^examples/
pass_filenames: false
require_serial: true
- id: loq
name: loq (file size limits)
entry: bash -c 'uv run loq || printf "\nloq violations not enforced... yet!\n"'
language: system
pass_filenames: false
verbose: true
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:

265
AGENTS.md
View file

@ -1,265 +0,0 @@
# FastMCP Development Guidelines
> **Audience**: LLM-driven engineering agents and human developers
FastMCP is a comprehensive Python framework (Python ≥3.10) for building Model Context Protocol (MCP) servers and clients. This is the actively maintained v2.0 providing a complete toolkit for the MCP ecosystem.
## Required Development Workflow
**CRITICAL**: Always run these commands in sequence before committing:
```bash
uv sync # Install dependencies
uv run pre-commit run --all-files # Ruff + Prettier + ty
uv run pytest # Run full test suite
```
**All three must pass** - this is enforced by CI. Alternative: `just build && just typecheck && just test`
**Tests must pass and lint/typing must be clean before committing.**
## Repository Structure
| Path | Purpose |
| ------------------ | --------------------------------------------------- |
| `src/fastmcp/` | Library source code (Python ≥ 3.10) |
| `├─server/` | Server implementation, `FastMCP`, auth, networking |
| `│ ├─auth/` | Authentication providers (Bearer, JWT, WorkOS) |
| `│ └─middleware/` | Error handling, logging, rate limiting |
| `├─client/` | High-level client SDK + transports |
| `│ └─auth/` | Client authentication (Bearer, OAuth) |
| `├─tools/` | Tool implementations + `ToolManager` |
| `├─resources/` | Resources, templates + `ResourceManager` |
| `├─prompts/` | Prompt templates + `PromptManager` |
| `├─cli/` | FastMCP CLI commands (`run`, `dev`, `install`) |
| `├─contrib/` | Community contributions (bulk caller, mixins) |
| `├─experimental/` | Experimental features (new OpenAPI parser) |
| `└─utilities/` | Shared utilities (logging, JSON schema, HTTP) |
| `tests/` | Comprehensive pytest suite with markers |
| `docs/` | Mintlify documentation (published to gofastmcp.com) |
| `examples/` | Runnable demo servers (echo, smart_home, atproto) |
## Core MCP Objects
When modifying MCP functionality, changes typically need to be applied across all object types:
- **Tools** (`src/tools/` + `ToolManager`)
- **Resources** (`src/resources/` + `ResourceManager`)
- **Resource Templates** (`src/resources/` + `ResourceManager`)
- **Prompts** (`src/prompts/` + `PromptManager`)
## Writing Style
- Be brief and to the point. Do not regurgitate information that can easily be gleaned from the code, except to guide the reader to where the code is located.
- **NEVER** use "This isn't..." or "not just..." constructions. State what something IS directly. Avoid defensive writing patterns like:
- "This isn't X, it's Y" or "Not just X, but Y" → Just say "This is Y"
- "Not just about X" → State the actual purpose
- "We're not doing X, we're doing Y" → Just explain what you're doing
- Any variation of explaining what something isn't before what it is
## Testing Best Practices
### Testing Standards
- Every test: atomic, self-contained, single functionality
- Use parameterization for multiple examples of same functionality
- Use separate tests for different functionality pieces
- **ALWAYS** Put imports at the top of the file, not in the test body
- **NEVER** add `@pytest.mark.asyncio` to tests - `asyncio_mode = "auto"` is set globally
- **ALWAYS** run pytest after significant changes
### Inline Snapshots
FastMCP uses `inline-snapshot` for testing complex data structures. On first run with empty `snapshot()`, pytest will auto-populate the expected value when running `pytest --inline-snapshot=create`. To update snapshots after intentional changes, run `pytest --inline-snapshot=fix`. This is particularly useful for testing JSON schemas and API responses.
### Always Use In-Memory Transport
Pass FastMCP servers directly to clients for testing:
```python
mcp = FastMCP("TestServer")
@mcp.tool
def greet(name: str) -> str:
return f"Hello, {name}!"
# Direct connection - no network complexity
async with Client(mcp) as client:
result = await client.call_tool("greet", {"name": "World"})
```
Only use HTTP transport when explicitly testing network features:
```python
# Network testing only
async with Client(transport=StreamableHttpTransport(server_url)) as client:
result = await client.ping()
```
## Development Rules
### Git & CI
- Pre-commit hooks are required (run automatically on commits)
- Never amend commits to fix pre-commit failures
- Apply PR labels: bugs/breaking/enhancements/features
- Improvements = enhancements (not features) unless specified
- **NEVER** force-push on collaborative repos
- **ALWAYS** run pre-commit before PRs
### Commit Messages and Agent Attribution
- **Agents NOT acting on behalf of @jlowin MUST identify themselves** (e.g., "🤖 Generated with Claude Code" in commits/PRs)
- Keep commit messages brief - ideally just headlines, not detailed messages
- Focus on what changed, not how or why
- Always read issue comments for follow-up information (treat maintainers as authoritative)
### PR Messages - Required Structure
- 1-2 paragraphs: problem/tension + solution (PRs are documentation!)
- Focused code example showing key capability
- **Avoid:** bullet summaries, exhaustive change lists, verbose closes/fixes, marketing language
- **Do:** Be opinionated about why change matters, show before/after scenarios
- Minor fixes: keep body short and concise
- No "test plan" sections or testing summaries
### Code Standards
- Python ≥ 3.10 with full type annotations
- Follow existing patterns and maintain consistency
- **Prioritize readable, understandable code** - clarity over cleverness
- Avoid obfuscated or confusing patterns even if they're shorter
- Use `# type: ignore[attr-defined]` in tests for MCP results instead of type assertions
- Each feature needs corresponding tests
### Documentation
- Uses Mintlify framework
- Files must be in docs.json to be included
- Never modify `docs/python-sdk/**` (auto-generated)
- **Core Principle:** A feature doesn't exist unless it is documented!
### Documentation Guidelines
- **Code Examples:** Explain before showing code, make blocks fully runnable (include imports)
- **Structure:** Headers form navigation guide, logical H2/H3 hierarchy
- **Content:** User-focused sections, motivate features (why) before mechanics (how)
- **Style:** Prose over code comments for important information
## Code Review Guidelines
### Philosophy
Code review is about maintaining a healthy codebase while helping contributors succeed. The burden of proof is on the PR to demonstrate it adds value in the intended way. Your job is to help it get there through actionable feedback.
**Critical**: A perfectly written PR that adds unwanted functionality must still be rejected. The code must advance the codebase in the intended direction, not just be well-written. When rejecting, provide clear guidance on how to align with project goals.
Be friendly and welcoming while maintaining high standards. Call out what works well - this reinforces good patterns. When code needs improvement, be specific about why and how to fix it. Remember that PRs serve as documentation for future developers.
### Focus On
- **Does this advance the codebase in the intended direction?** (Even perfect code for unwanted features should be rejected)
- **API design and naming clarity** - Identify confusing patterns (e.g., parameter values that contradict defaults) or non-idiomatic code (mutable defaults, etc.). Contributed code will need to be maintained indefinitely, and by someone other than the author (unless the author is a maintainer).
- **Suggest specific improvements**, not generic "add more tests" comments
- **Think about API ergonomics and learning curve** from a user perspective
### For Agent Reviewers
- **Read the full context**: Always examine related files, tests, and documentation before reviewing
- **Check against established patterns**: Look for consistency with existing codebase conventions
- **Verify functionality claims**: Don't just read code - understand what it actually does
- **Consider edge cases**: Think through error conditions and boundary scenarios
### Avoid
- Generic feedback without specifics
- Hypothetical problems unlikely to occur
- Nitpicking organizational choices without strong reason
- Summarizing what the PR already describes
- Star ratings or excessive emojis
- Bikeshedding style preferences when functionality is correct
- Requesting changes without suggesting solutions
- Focusing on personal coding style over project conventions
### Tone
- Acknowledge good decisions ("This API design is clean")
- Be direct but respectful
- Explain impact ("This will confuse users because...")
- Remember: Someone else maintains this code forever
### Decision Framework
Before approving, ask yourself:
1. Does this PR achieve its stated purpose?
2. Is that purpose aligned with where the codebase should go?
3. Would I be comfortable maintaining this code?
4. Have I actually understood what it does, not just what it claims?
5. Does this change introduce technical debt?
If something needs work, your review should help it get there through specific, actionable feedback. If it's solving the wrong problem, say so clearly.
### Review Comment Examples
**Good Review Comments:**
❌ "Add more tests"
✅ "The `handle_timeout` method needs tests for the edge case where timeout=0"
❌ "This API is confusing"
✅ "The parameter name `data` is ambiguous - consider `message_content` to match the MCP specification"
❌ "This could be better"
✅ "This approach works but creates a circular dependency. Consider moving the validation to `utils/validators.py`"
### Review Checklist
Before approving, verify:
- [ ] All required development workflow steps completed (uv sync, pre-commit, pytest)
- [ ] Changes align with repository patterns and conventions
- [ ] API changes are documented and backwards-compatible where possible
- [ ] Error handling follows project patterns (specific exception types)
- [ ] Tests cover new functionality and edge cases
## Key Tools & Commands
### Environment Setup
```bash
git clone <repo>
cd fastmcp
uv sync # Installs all deps including dev tools
```
### Validation Commands (Run Frequently)
- **Linting**: `uv run ruff check` (or with `--fix`)
- **Type Checking**: `uv run ty check`
- **All Checks**: `uv run pre-commit run --all-files`
### Testing
- **Standard**: `uv run pytest`
- **Integration**: `uv run pytest -m "integration"`
- **Excluding markers**: `uv run pytest -m "not integration and not client_process"`
### CLI Usage
- **Run server**: `uv run fastmcp run server.py`
- **Inspect server**: `uv run fastmcp inspect server.py`
## Critical Patterns
### Error Handling
- Never use bare `except` - be specific with exception types
- Use `# type: ignore[attr-defined]` in tests for MCP results
### Build Issues (Common Solutions)
1. **Dependencies**: Always `uv sync` first
2. **Pre-commit fails**: Run `uv run pre-commit run --all-files` to see failures
3. **Type errors**: Use `uv run ty check` directly, check `pyproject.toml` config
4. **Test timeouts**: Default 3s - optimize or mark as integration tests

1
AGENTS.md Symbolic link
View file

@ -0,0 +1 @@
CLAUDE.md

View file

@ -1 +0,0 @@
AGENTS.md

210
CLAUDE.md Normal file
View file

@ -0,0 +1,210 @@
# FastMCP Development Guidelines
> **Audience**: LLM-driven engineering agents and human developers
> **Note**: `AGENTS.md` is a symlink to this file. Edit `CLAUDE.md` directly.
FastMCP is a comprehensive Python framework (Python ≥3.10) for building Model Context Protocol (MCP) servers and clients. This is the actively maintained v2.0 providing a complete toolkit for the MCP ecosystem.
## Required Development Workflow
**CRITICAL**: Always run these commands in sequence before committing.
```bash
uv sync # Install dependencies
uv run pytest -n auto # Run full test suite
```
In addition, you must pass static checks. This is generally done as a pre-commit hook with `prek` but you can run it manually with:
```bash
uv run prek run --all-files # Ruff + Prettier + ty
```
**Tests must pass and lint/typing must be clean before committing.**
## Repository Structure
| Path | Purpose |
| ----------------- | -------------------------------------- |
| `fastmcp_slim/fastmcp/` | Library source code |
| `├─server/` | Server implementation |
| `│ ├─auth/` | Authentication providers |
| `│ └─middleware/` | Error handling, logging, rate limiting |
| `├─client/` | Client SDK |
| `│ └─auth/` | Client authentication |
| `├─tools/` | Tool definitions |
| `├─resources/` | Resources and resource templates |
| `├─prompts/` | Prompt templates |
| `├─cli/` | CLI commands |
| `└─utilities/` | Shared utilities |
| `tests/` | Pytest suite |
| `docs/` | Mintlify docs (gofastmcp.com) |
## Core MCP Objects
When modifying MCP functionality, changes typically need to be applied across all object types:
- **Tools** (`src/tools/`)
- **Resources** (`src/resources/`)
- **Resource Templates** (`src/resources/`)
- **Prompts** (`src/prompts/`)
**Before writing cross-component logic (dedupe, grouping, lookups, identity checks), read `FastMCPComponent` in `fastmcp_slim/fastmcp/utilities/components.py`.** The base class defines the shared surface — `name`, `version`, `tags`, `meta`, and critically the `key` property which is the canonical MCP identity (encodes type, identifier, and version). Prefer `item.key` over ad-hoc `name or uri or uri_template` fallbacks; overrides in `Resource` and `ResourceTemplate` already handle URI-based identity, and `.key` includes the version suffix so variants of the same component don't falsely collide.
## Development Rules
**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 — 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
- **NEVER** create a release, comment on an issue, or open a PR unless specifically instructed to do so.
- **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
- Never pass GitHub, Linear, or Slack comment bodies inline through shell arguments when the body contains `$`, `${...}`, backticks, `$(...)`, environment-variable examples, secrets, or config interpolation examples.
- Use a body file or structured API payload for outbound comments, then inspect the exact outgoing text before posting. Prefer `gh ... --body-file /path/to/comment.md` over `--body "..."`.
- When explaining environment interpolation, use placeholders and fenced code blocks. Never include raw `.env` contents in outbound comments.
### Releases
Only cut releases when the maintainer explicitly asks. Tags follow `v<version>` (e.g., `v3.2.0`). Always pass `--generate-notes` so the auto-generated changelog appears at the bottom.
**The title pun is critical.** Titles follow `v<version>: <pun>` where the pun relates to the most important theme of the release. Propose multiple options and let the maintainer choose — never pick one yourself. Look at recent releases for tone (e.g., "Code to Joy" for the code mode release, "Three at Last" for 3.0).
Write the maintainer-approved handwritten notes to a temporary file, then create the release. `--generate-notes` appends the auto-generated changelog after the handwritten content.
```bash
gh release create v4.0.0 --target main --title "v4.0.0: Theme Here" --generate-notes --notes-start-tag v3.4.4 --notes-file /tmp/release-notes.md
```
**Always pass `--notes-start-tag <last-stable-tag>`.** Without it, `--generate-notes` picks the most recent prior tag as the changelog start point — and if a prerelease exists (e.g. `v3.4.0b1`), it starts from *that*, silently truncating the PR list to only the commits since the beta. Pin it to the last stable release (e.g. `v3.3.1` when cutting `v3.4.0`). Verify after: the compare link at the bottom of the generated notes should read `v<last-stable>...v<new>`.
Use the branch that owns the release line as the target: current-major releases target `main`, 3.x maintenance releases target `release/3.x`, and 2.x maintenance releases target `release/2.x`. Confirm the target with the maintainer if there's any ambiguity. For example, cut a 3.4.4 maintenance release with `--target release/3.x`, not `main`.
The handwritten notes are prepended above the auto-generated changelog and are the part that matters. Do not include a title in the notes body — the release title (`v{version}: {pun}`) already serves as the heading. Work with the maintainer to draft the notes — propose a draft, get feedback, iterate. Do not publish without the maintainer's sign-off.
**Before drafting, always read recent existing releases** (`gh release list` then `gh release view <tag>`) to absorb the voice, structure, and level of detail. Each release builds on the tone of previous ones — don't guess at the style from these instructions alone.
**To preview what PRs will be in the release** before it's cut, call the GitHub generate-notes API. This returns the exact auto-generated changelog that `--generate-notes` would append, so you can see the full PR list — useful for picking a pun theme and making sure nothing's been missed:
```bash
gh api -X POST repos/PrefectHQ/fastmcp/releases/generate-notes \
-f tag_name=v3.2.3 \
-f target_commitish=main \
-f previous_tag_name=v3.2.2 \
--jq '.body'
```
Set `target_commitish` to the same branch that will receive the release tag. For maintenance releases, use the maintenance branch (for example, `release/3.x`) so the preview matches the release notes GitHub will generate.
**Point releases** (3.0, 3.1, 3.2) get narrative prose: open with the theme of the release, then walk through headline features conceptually — what they enable, why they matter, how they fit together. Write it the way a blog post reads, not a changelog. Multiple paragraphs, code examples where they clarify.
**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.
**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.
Because the docs land *before* the tag exists, derive the entry from the maintainer-approved handwritten notes (intro/summary) and the `--generate-notes` API *preview* (the PR-list body — see the generate-notes API call above, which returns the exact changelog without cutting anything). Scripting the link reformatting is reliable for long PR lists. The release-URL, tag, and compare links follow the known pattern (`/releases/tag/v<version>`, `compare/v<last-stable>...v<version>`) and will 404 only during the short window between merging the docs PR and cutting the release minutes later — they resolve before the release workflow completes. For this reason, create and merge the docs PR *immediately* before cutting the release — treat the two as one tight back-to-back sequence, not independent steps — so the links are valid by the time the release publishes rather than dangling for any longer than necessary.
### Commit Messages and Agent Attribution
- **Agents NOT acting on behalf of @jlowin MUST identify themselves** (e.g., "🤖 Generated with Claude Code" in commits/PRs)
- Keep commit messages brief - ideally just headlines, not detailed messages
- Focus on what changed, not how or why
- Always read issue comments for follow-up information (treat maintainers as authoritative)
- **Treat proposed solutions in issues skeptically.** This applies to solutions proposed by *users* in issue reports — not to feedback from configured review bots (CodeRabbit, chatgpt-codex-connector, etc.), which should be evaluated on their merits. The ideal issue contains a concise problem description and an MRE — nothing more. Proposed solutions are only worth considering if they clearly reflect genuine, non-obvious investigation of the codebase. If a solution reads like speculation, or like it was generated by an LLM without deep framework knowledge, ignore it and diagnose from the repro. Most reporters — human or AI — do not have sufficient understanding of FastMCP internals to correctly diagnose anything beyond a trivial bug. We can ask the same questions of an LLM when implementing; we don't need the reporter to do it for us, and a wrong diagnosis is worse than none.
### PR Messages - Required Structure
- 1-2 paragraphs: problem/tension + solution (PRs are documentation!)
- Focused code example showing key capability
- **Avoid:** bullet summaries, exhaustive change lists, verbose closes/fixes, marketing language
- **Do:** Be opinionated about why change matters, show before/after scenarios
- Minor fixes: keep body short and concise
- No "test plan" sections or testing summaries
### Code Review Guidelines
- **Fix causes, not symptoms.** When a PR works around a problem instead of addressing why it occurs, that's a red flag. A side-channel that compensates for a missing step adds permanent complexity. If the fix doesn't change the code path where the bug actually happens, ask why not.
- Focus on API design and naming clarity
- Identify confusing patterns (e.g., parameter values that contradict defaults) or non-idiomatic code (mutable defaults, etc.). Contributed code will need to be maintained indefinitely, and by someone other than the author (unless the author is a maintainer).
- Suggest specific improvements, not generic "add more tests" comments
- Think about API ergonomics from a user perspective
### Code Standards
- Python ≥ 3.10 with full type annotations
- Follow existing patterns and maintain consistency
- **Prioritize readable, understandable code** - clarity over cleverness
- Avoid obfuscated or confusing patterns even if they're shorter
- Each feature needs corresponding tests
### Module Exports
- **Do not create overeager `__init__.py` files.** Package initializers should not import heavy submodules, provider stacks, optional integrations, or modules that can point back into the package. Overeager re-exports make the framework sprawl and create circular imports that only appear in fresh interpreters or clean installs.
- **Be intentional about re-exports** - don't blindly re-export everything to parent namespaces
- Core types that define a module's purpose should be exported (e.g., `Middleware` from `fastmcp.server.middleware`)
- Specialized features can live in submodules (e.g., `fastmcp.server.middleware.dynamic`)
- Only re-export to `fastmcp.*` for the most fundamental types (e.g., `FastMCP`, `Client`)
- When in doubt, prefer users importing from the specific submodule over re-exporting
### Documentation
- Uses Mintlify framework
- Files must be in docs.json to be included
- Do not manually modify `docs/python-sdk/**` — these files are auto-generated from source code by a bot and maintained via a long-lived PR. Do not include changes to these files in contributor PRs.
- Do not manually modify `docs/public/schemas/**` or `fastmcp_slim/fastmcp/utilities/mcp_server_config/v1/schema.json` — these are auto-generated and maintained via a long-lived PR.
- **Core Principle:** A feature doesn't exist unless it is documented!
- When adding or modifying settings in `fastmcp_slim/fastmcp/settings.py`, update `docs/more/settings.mdx` to match.
### Documentation Guidelines
- **Code Examples:** Explain before showing code, make blocks fully runnable (include imports)
- **Code Formatting:** Keep code blocks visually clean — avoid deeply nested function calls. Extract intermediate values into named variables rather than inlining everything into one expression. Code in docs is read more than it's run; optimize for scannability.
- **Structure:** Headers form navigation guide, logical H2/H3 hierarchy
- **Content:** User-focused sections, motivate features (why) before mechanics (how)
- **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
- File sizes enforced by [loq](https://github.com/jakekaplan/loq). Edit `loq.toml` to raise limits; `loq baseline` to ratchet down.
- Always `uv sync` first when debugging build issues
- Default test timeout is 5s - optimize or mark as integration tests

66
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,66 @@
# Contributing to FastMCP
FastMCP is an actively maintained, high-traffic project. We welcome contributions — but the most impactful way to contribute might not be what you expect.
Participation is governed by our [Code of Conduct](CODE_OF_CONDUCT.md), and contributions are licensed under [Apache 2.0](LICENSE).
## The best contribution is a great issue
FastMCP is an opinionated framework, and its maintainers use AI-assisted tooling that is deeply tuned to those opinions — the design philosophy, the API patterns, the way the framework is meant to evolve. A well-written issue with a clear problem description is often more valuable than a pull request, because it lets maintainers produce a solution that isn't just correct, but consistent with how the framework wants to work. That matters more than speed, though it's faster too.
**A great issue looks like this:**
1. A short, motivating description of the problem or gap
2. A minimal reproducible example (for bugs) or a concrete use case (for enhancements)
3. A brief note on expected vs. actual behavior
That's it. No need to diagnose root causes, propose API designs, or suggest implementations. If you've done genuine investigation and have a non-obvious insight, include it.
## Using AI to contribute
We encourage you to use LLMs to help identify bugs, write MREs, and prepare contributions. But if you do, your LLM must take into account the conventions and contributing guidelines of this repo — including how we want issues formatted and when it's appropriate to open a PR. Generic LLM output that ignores these guidelines tells us the contribution wasn't made thoughtfully, and we will close it. A good AI-assisted contribution is indistinguishable from a good human one. A bad one is obvious.
If you're driving an agent: do **not** have it post comments asking to be assigned to an issue or announcing that it intends to work on one. Those comments are ignored. If the agent intends to contribute, open a PR instead — it will be gated on assignment (see below). Comment on an issue only to propose a genuinely novel, differentiated solution, never to claim a task that's already described.
## When to open a pull request
An open issue is not an invitation to submit a PR, and it is not a queue you join by commenting. Issues track problems; who implements them and how is a separate decision maintainers make, and whoever opened the issue has first claim on it.
**Don't post drive-by comments claiming an issue** — "can I work on this?", "please assign me", "I'll take this." They don't affect who gets assigned, they're the most common form of noise we get, and automated versions are ignored. Whoever opens the issue has first claim on it; if that's you, a maintainer will assign you. If you want to implement something someone else reported, just open a PR — you don't need permission to try, and competing PRs are fine — but it's reviewed only if a maintainer assigns you to the issue, which usually won't happen if the reporter intends to handle it. The one comment worth posting is a genuinely different approach worth discussing; a substantive design proposal is welcome, a bare claim on the task is not.
**Issues labeled `prs welcome` skip the assignment gate.** When we apply that label, we're saying the reporter isn't implementing it and we'd take a PR from anyone. Open one directly — no assignment needed, and it won't be auto-closed. Still reference the issue (`Fixes #123`), since that's how the check knows which issue to look at.
**What assignment means.** Being assigned is a commitment on both sides: we'll review your work seriously, and you'll see it through. That means responding to review feedback yourself and being able to explain any part of your change and why you made it that way. Use whatever tooling you like to get there — but if you can't answer a question about your own diff, we'll unassign the issue so someone else can pick it up.
**Bug fixes** — PRs are welcome for simple, well-scoped bug fixes where the problem and solution are both straightforward. "The function raises `TypeError` when passed `None` because of a missing guard" is a good candidate. If the fix requires design decisions or touches multiple subsystems, open an issue with a design proposal instead.
**Documentation** — Typo fixes, clarifications, and improvements to examples are always welcome as PRs.
**Enhancements and features** — We welcome enhancement PRs, but our experience is that most contributors — even when using LLMs — implement fixes that address the one instance of a problem they encountered rather than understanding why the framework produces that problem and fixing it at the right layer. This creates branching, patch-style code that's difficult to maintain and makes it impossible to reason about the framework as a coherent system. For this reason, enhancements need a design proposal in the issue before code is written. The proposal doesn't need to be long — just enough to show you've thought about how the change fits into the framework, not just how it solves your immediate case.
**Integrations** — FastMCP generally does not accept PRs that add third-party integrations (custom middleware, provider-specific adapters, etc.). If you're building something for your users, ship it as a standalone package — that's a feature, not a limitation. Authentication providers are an exception, since auth is tightly coupled to the framework.
## PR guidelines
If you do open a PR:
- **Reference an issue you're assigned to.** Every PR must reference a tracked issue using an auto-close keyword (`Fixes #123`, `Closes #123`, or `Resolves #123`), and the referenced issue must be assigned to you — unless it's labeled `prs welcome`, which waives the assignment requirement. If there isn't an issue, open one. This lets us deconflict effort and steer the approach before you invest time in code. External PRs that don't meet these conditions are automatically labeled `missing-issue-link` and closed; they reopen automatically once the link is present and you're assigned.
- **Leave "Allow edits by maintainers" enabled.** We frequently take a PR the last few steps ourselves rather than block on another round trip — tightening a test, adjusting naming, rebasing. It's enabled by default on PRs from personal forks; leave it that way. GitHub doesn't allow it at all for forks owned by an organization, so if you're contributing from one, expect us to land the final changes separately.
- **Target the right branch.** Open against `main` unless you're fixing something specific to a maintenance line, in which case target that branch directly (`release/3.x`, `release/2.x`).
- **If your PR was auto-closed, don't open a new one.** Edit the *existing* PR to add the issue link, get assigned to that issue, and it reopens on its own — the branch and history are preserved. A duplicate PR just starts you over and adds to the triage pile.
- **Keep it focused.** One logical change per PR. Don't bundle unrelated fixes or refactors.
- **Match existing patterns.** Follow the code style, type annotation conventions, and test patterns you see in the codebase. Run `uv run prek run --all-files` before submitting.
- **Write tests.** Bug fixes should include a test that fails without the fix. Enhancements should include tests for the new behavior.
- **Fix the cause, not the symptom.** If the bug is that a code path skips a step, the fix should make it stop skipping that step — not add compensation elsewhere. Workaround-style fixes will be sent back for revision.
- **Don't submit generated boilerplate.** We review every line. PRs that read like unedited LLM output — verbose descriptions, speculative changes, shotgun-style fixes — will be closed.
## What we'll close without review
To keep the project maintainable, we will close PRs that:
- Don't reference an issue or address a clearly self-evident bug
- Make sweeping changes without prior discussion
- Add third-party integrations that belong in a separate package
- Are difficult to review due to size, scope, or generated content
This isn't personal — contributing to a framework is different from contributing to an application. In an application, a fix that works is a good fix. In a framework, a fix that works but doesn't fit the framework's design creates maintenance burden that compounds over time. Every patch that works around a problem instead of solving it at the right layer makes the system harder for *everyone* to reason about — maintainers, contributors, and users. We hold contributions to this standard because the alternative is a codebase that's a series of patches rather than a coherent system. A good issue is often the best thing you can do for the project.

538
README.md
View file

@ -3,44 +3,32 @@
<!-- omit in toc -->
<picture>
<source width="550" media="(prefers-color-scheme: dark)" srcset="docs/assets/brand/wordmark-watercolor-waves-dark.png">
<source width="550" media="(prefers-color-scheme: light)" srcset="docs/assets/brand/wordmark-watercolor-waves.png">
<img width="550" alt="FastMCP Logo" src="docs/assets/brand/wordmark-watercolor-waves.png">
<source width="550" media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/PrefectHQ/fastmcp/main/docs/assets/brand/f-watercolor-waves-4-dark.png">
<source width="550" media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/PrefectHQ/fastmcp/main/docs/assets/brand/f-watercolor-waves-4.png">
<img width="550" alt="FastMCP Logo" src="https://raw.githubusercontent.com/PrefectHQ/fastmcp/main/docs/assets/brand/f-watercolor-waves-2.png">
</picture>
# FastMCP v2 🚀
# FastMCP 🚀
<strong>The fast, Pythonic way to build MCP servers and clients.</strong>
<strong>Move fast and make things.</strong>
*Made with ☕️ by [Prefect](https://www.prefect.io/)*
*Made with 💙 by [Prefect](https://www.prefect.io/)*
[![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)
[![Tests](https://github.com/jlowin/fastmcp/actions/workflows/run-tests.yml/badge.svg)](https://github.com/jlowin/fastmcp/actions/workflows/run-tests.yml)
[![License](https://img.shields.io/github/license/jlowin/fastmcp.svg)](https://github.com/jlowin/fastmcp/blob/main/LICENSE)
[![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)
<a href="https://trendshift.io/repositories/13266" target="_blank"><img src="https://trendshift.io/api/badge/repositories/13266" alt="jlowin%2Ffastmcp | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<a href="https://trendshift.io/repositories/21461" target="_blank"><img src="https://trendshift.io/api/badge/repositories/21461" alt="prefecthq%2Ffastmcp | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
</div>
> [!Note]
>
> #### FastMCP 2.0: The Standard Framework
>
> FastMCP pioneered Python MCP development, and FastMCP 1.0 was incorporated into the [official MCP SDK](https://github.com/modelcontextprotocol/python-sdk) in 2024.
>
> **This is FastMCP 2.0** — the actively maintained, production-ready framework that extends far beyond basic protocol implementation. While the SDK provides core functionality, FastMCP 2.0 delivers everything needed for production: advanced MCP patterns (server composition, proxying, OpenAPI/FastAPI generation, tool transformation), enterprise auth (Google, GitHub, WorkOS, Azure, Auth0, and more), deployment tools, testing utilities, and comprehensive client libraries.
>
> **For production MCP applications, install FastMCP:** `pip install fastmcp`
---
**FastMCP is the standard framework for building MCP applications**, providing the fastest path from idea to production.
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) is a standardized way to provide context and tools to LLMs. FastMCP makes building production-ready MCP servers simple, with enterprise auth, deployment tools, and a complete ecosystem built in.
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
# server.py
from fastmcp import FastMCP
mcp = FastMCP("Demo 🚀")
@ -54,457 +42,83 @@ if __name__ == "__main__":
mcp.run()
```
Run the server locally:
## Why FastMCP
Building an effective MCP application is harder than it looks. 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 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.
FastMCP has three pillars:
<table>
<tr>
<td align="center" valign="top" width="33%">
<a href="https://gofastmcp.com/servers/server">
<img src="https://raw.githubusercontent.com/PrefectHQ/fastmcp/main/docs/assets/images/servers-card.png" alt="Servers" />
<br /><strong>Servers</strong>
</a>
<br />Expose tools, resources, and prompts to LLMs.
</td>
<td align="center" valign="top" width="33%">
<a href="https://gofastmcp.com/apps/overview">
<img src="https://raw.githubusercontent.com/PrefectHQ/fastmcp/main/docs/assets/images/apps-card.png" alt="Apps" />
<br /><strong>Apps</strong>
</a>
<br />Give your tools interactive UIs rendered directly in the conversation.
</td>
<td align="center" valign="top" width="33%">
<a href="https://gofastmcp.com/clients/client">
<img src="https://raw.githubusercontent.com/PrefectHQ/fastmcp/main/docs/assets/images/clients-card.png" alt="Clients" />
<br /><strong>Clients</strong>
</a>
<br />Connect to any MCP server — local or remote, programmatic or CLI.
</td>
</tr>
</table>
**[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).
## Scale MCP with Horizon
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.
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.
Start with FastMCP. [Scale with Horizon →](https://www.prefect.io/horizon?utm_source=github&utm_medium=readme&utm_campaign=readme_horizon&utm_content=readme_cta)
## Installation
We recommend adding FastMCP to your project with [uv](https://docs.astral.sh/uv/):
```bash
fastmcp run server.py
uv add fastmcp
```
### 📚 Documentation
For full installation instructions, including verification and upgrading, see the [**Installation Guide**](https://gofastmcp.com/getting-started/installation).
FastMCP's complete documentation is available at **[gofastmcp.com](https://gofastmcp.com)**, including detailed guides, API references, and advanced patterns. This readme provides only a high-level overview.
**Upgrading?** We have guides for:
- [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 is also available in [llms.txt format](https://llmstxt.org/), which is a simple markdown standard that LLMs can consume easily.
## 📚 Documentation
There are two ways to access the LLM-friendly documentation:
FastMCP's complete documentation is available at **[gofastmcp.com](https://gofastmcp.com)**, including detailed guides, API references, and advanced patterns.
Documentation is also available in [llms.txt format](https://llmstxt.org/), which is a simple markdown standard that LLMs can consume easily:
- [`llms.txt`](https://gofastmcp.com/llms.txt) is essentially a sitemap, listing all the pages in the documentation.
- [`llms-full.txt`](https://gofastmcp.com/llms-full.txt) contains the entire documentation. Note this may exceed the context window of your LLM.
**Community:** Join our [Discord server](https://discord.gg/uu8dJCgttd) to connect with other FastMCP developers and share what you're building.
---
<!-- omit in toc -->
## Table of Contents
- [FastMCP v2 🚀](#fastmcp-v2-)
- [📚 Documentation](#-documentation)
- [What is MCP?](#what-is-mcp)
- [Why FastMCP?](#why-fastmcp)
- [Installation](#installation)
- [Core Concepts](#core-concepts)
- [The `FastMCP` Server](#the-fastmcp-server)
- [Tools](#tools)
- [Resources \& Templates](#resources--templates)
- [Prompts](#prompts)
- [Context](#context)
- [MCP Clients](#mcp-clients)
- [Authentication](#authentication)
- [Enterprise Authentication, Zero Configuration](#enterprise-authentication-zero-configuration)
- [Deployment](#deployment)
- [From Development to Production](#from-development-to-production)
- [Advanced Features](#advanced-features)
- [Proxy Servers](#proxy-servers)
- [Composing MCP Servers](#composing-mcp-servers)
- [OpenAPI \& FastAPI Generation](#openapi--fastapi-generation)
- [Running Your Server](#running-your-server)
- [Contributing](#contributing)
- [Prerequisites](#prerequisites)
- [Setup](#setup)
- [Unit Tests](#unit-tests)
- [Static Checks](#static-checks)
- [Pull Requests](#pull-requests)
---
## What is MCP?
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. It is often described as "the USB-C port for AI", providing a uniform way to connect LLMs to resources they can use. It may be easier to think of it as an API, but specifically designed for LLM interactions. MCP servers can:
- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context)
- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect)
- Define interaction patterns through **Prompts** (reusable templates for LLM interactions)
- And more!
FastMCP provides a high-level, Pythonic interface for building, managing, and interacting with these servers.
## Why FastMCP?
FastMCP handles all the complex protocol details so you can focus on building. In most cases, decorating a Python function is all you need — FastMCP handles the rest.
🚀 **Fast:** High-level interface means less code and faster development
🍀 **Simple:** Build MCP servers with minimal boilerplate
🐍 **Pythonic:** Feels natural to Python developers
🔍 **Complete:** Everything for production — enterprise auth (Google, GitHub, Azure, Auth0, WorkOS), deployment tools, testing frameworks, client libraries, and more
FastMCP provides the shortest path from idea to production. Deploy locally, to the cloud with [FastMCP Cloud](https://fastmcp.cloud), or to your own infrastructure.
## Installation
We recommend installing FastMCP with [uv](https://docs.astral.sh/uv/):
```bash
uv pip install fastmcp
```
For full installation instructions, including verification, upgrading from the official MCPSDK, and developer setup, see the [**Installation Guide**](https://gofastmcp.com/getting-started/installation).
## Core Concepts
These are the building blocks for creating MCP servers and clients with FastMCP.
### The `FastMCP` Server
The central object representing your MCP application. It holds your tools, resources, and prompts, manages connections, and can be configured with settings like authentication.
```python
from fastmcp import FastMCP
# Create a server instance
mcp = FastMCP(name="MyAssistantServer")
```
Learn more in the [**FastMCP Server Documentation**](https://gofastmcp.com/servers/fastmcp).
### Tools
Tools allow LLMs to perform actions by executing your Python functions (sync or async). Ideal for computations, API calls, or side effects (like `POST`/`PUT`). FastMCP handles schema generation from type hints and docstrings. Tools can return various types, including text, JSON-serializable objects, and even images or audio aided by the FastMCP media helper classes.
```python
@mcp.tool
def multiply(a: float, b: float) -> float:
"""Multiplies two numbers."""
return a * b
```
Learn more in the [**Tools Documentation**](https://gofastmcp.com/servers/tools).
### Resources & Templates
Resources expose read-only data sources (like `GET` requests). Use `@mcp.resource("your://uri")`. Use `{placeholders}` in the URI to create dynamic templates that accept parameters, allowing clients to request specific data subsets.
```python
# Static resource
@mcp.resource("config://version")
def get_version():
return "2.0.1"
# Dynamic resource template
@mcp.resource("users://{user_id}/profile")
def get_profile(user_id: int):
# Fetch profile for user_id...
return {"name": f"User {user_id}", "status": "active"}
```
Learn more in the [**Resources & Templates Documentation**](https://gofastmcp.com/servers/resources).
### Prompts
Prompts define reusable message templates to guide LLM interactions. Decorate functions with `@mcp.prompt`. Return strings or `Message` objects.
```python
@mcp.prompt
def summarize_request(text: str) -> str:
"""Generate a prompt asking for a summary."""
return f"Please summarize the following text:\n\n{text}"
```
Learn more in the [**Prompts Documentation**](https://gofastmcp.com/servers/prompts).
### Context
Access MCP session capabilities within your tools, resources, or prompts by adding a `ctx: Context` parameter. Context provides methods for:
- **Logging:** Log messages to MCP clients with `ctx.info()`, `ctx.error()`, etc.
- **LLM Sampling:** Use `ctx.sample()` to request completions from the client's LLM.
- **HTTP Request:** Use `ctx.http_request()` to make HTTP requests to other servers.
- **Resource Access:** Use `ctx.read_resource()` to access resources on the server
- **Progress Reporting:** Use `ctx.report_progress()` to report progress to the client.
- and more...
To access the context, add a parameter annotated as `Context` to any mcp-decorated function. FastMCP will automatically inject the correct context object when the function is called.
```python
from fastmcp import FastMCP, Context
mcp = FastMCP("My MCP Server")
@mcp.tool
async def process_data(uri: str, ctx: Context):
# Log a message to the client
await ctx.info(f"Processing {uri}...")
# Read a resource from the server
data = await ctx.read_resource(uri)
# Ask client LLM to summarize the data
summary = await ctx.sample(f"Summarize: {data.content[:500]}")
# Return the summary
return summary.text
```
Learn more in the [**Context Documentation**](https://gofastmcp.com/servers/context).
### MCP Clients
Interact with *any* MCP server programmatically using the `fastmcp.Client`. It supports various transports (Stdio, SSE, In-Memory) and often auto-detects the correct one. The client can also handle advanced patterns like server-initiated **LLM sampling requests** if you provide an appropriate handler.
Critically, the client allows for efficient **in-memory testing** of your servers by connecting directly to a `FastMCP` server instance via the `FastMCPTransport`, eliminating the need for process management or network calls during tests.
```python
from fastmcp import Client
async def main():
# Connect via stdio to a local script
async with Client("my_server.py") as client:
tools = await client.list_tools()
print(f"Available tools: {tools}")
result = await client.call_tool("add", {"a": 5, "b": 3})
print(f"Result: {result.content[0].text}")
# Connect via SSE
async with Client("http://localhost:8000/sse") as client:
# ... use the client
pass
```
To use clients to test servers, use the following pattern:
```python
from fastmcp import FastMCP, Client
mcp = FastMCP("My MCP Server")
async def main():
# Connect via in-memory transport
async with Client(mcp) as client:
# ... use the client
```
FastMCP also supports connecting to multiple servers through a single unified client using the standard MCP configuration format:
```python
from fastmcp import Client
# Standard MCP configuration with multiple servers
config = {
"mcpServers": {
"weather": {"url": "https://weather-api.example.com/mcp"},
"assistant": {"command": "python", "args": ["./assistant_server.py"]}
}
}
# Create a client that connects to all servers
client = Client(config)
async def main():
async with client:
# Access tools and resources with server prefixes
forecast = await client.call_tool("weather_get_forecast", {"city": "London"})
answer = await client.call_tool("assistant_answer_question", {"query": "What is MCP?"})
```
Learn more in the [**Client Documentation**](https://gofastmcp.com/clients/client) and [**Transports Documentation**](https://gofastmcp.com/clients/transports).
## Authentication
### Enterprise Authentication, Zero Configuration
FastMCP provides comprehensive authentication support that sets it apart from basic MCP implementations. Secure your servers and authenticate your clients with the same enterprise-grade providers used by major corporations.
**Built-in OAuth Providers:**
- **Google**
- **GitHub**
- **Microsoft Azure**
- **Auth0**
- **WorkOS**
- **Descope**
- **JWT/Custom**
- **API Keys**
Protecting a server takes just two lines:
```python
from fastmcp.server.auth import GoogleProvider
auth = GoogleProvider(client_id="...", client_secret="...", base_url="https://myserver.com")
mcp = FastMCP("Protected Server", auth=auth)
```
Connecting to protected servers is even simpler:
```python
async with Client("https://protected-server.com/mcp", auth="oauth") as client:
# Automatic browser-based OAuth flow
result = await client.call_tool("protected_tool")
```
**Why FastMCP Auth Matters:**
- **Production-Ready:** Persistent storage, token refresh, comprehensive error handling
- **Zero-Config OAuth:** Just pass `auth="oauth"` for automatic setup
- **Enterprise Integration:** WorkOS SSO, Azure Active Directory, Auth0 tenants
- **Developer Experience:** Automatic browser launch, local callback server, environment variable support
- **Advanced Architecture:** Full OIDC support, Dynamic Client Registration (DCR), and unique OAuth proxy pattern that enables DCR with any provider
*Authentication this comprehensive is unique to FastMCP 2.0.*
Learn more in the **Authentication Documentation** for [servers](https://gofastmcp.com/servers/auth) and [clients](https://gofastmcp.com/clients/auth).
## Deployment
### From Development to Production
FastMCP supports every deployment scenario from local development to global scale:
**Development:** Run locally with a single command
```bash
fastmcp run server.py
```
**Production:** Deploy to [**FastMCP Cloud**](https://fastmcp.cloud) — Remote MCP that just works
- Instant HTTPS endpoints
- Built-in authentication
- Zero configuration
- Free for personal servers
**Self-Hosted:** Use HTTP or SSE transports for your own infrastructure
```python
mcp.run(transport="http", host="0.0.0.0", port=8000)
```
Learn more in the [**Deployment Documentation**](https://gofastmcp.com/deployment).
## Advanced Features
FastMCP introduces powerful ways to structure and compose your MCP applications.
### Proxy Servers
Create a FastMCP server that acts as an intermediary for another local or remote MCP server using `FastMCP.as_proxy()`. This is especially useful for bridging transports (e.g., remote SSE to local Stdio) or adding a layer of logic to a server you don't control.
Learn more in the [**Proxying Documentation**](https://gofastmcp.com/patterns/proxy).
### Composing MCP Servers
Build modular applications by mounting multiple `FastMCP` instances onto a parent server using `mcp.mount()` (live link) or `mcp.import_server()` (static copy).
Learn more in the [**Composition Documentation**](https://gofastmcp.com/patterns/composition).
### OpenAPI & FastAPI Generation
Automatically generate FastMCP servers from existing OpenAPI specifications (`FastMCP.from_openapi()`) or FastAPI applications (`FastMCP.from_fastapi()`), instantly bringing your web APIs to the MCP ecosystem.
Learn more: [**OpenAPI Integration**](https://gofastmcp.com/integrations/openapi) | [**FastAPI Integration**](https://gofastmcp.com/integrations/fastapi).
## Running Your Server
The main way to run a FastMCP server is by calling the `run()` method on your server instance:
```python
# server.py
from fastmcp import FastMCP
mcp = FastMCP("Demo 🚀")
@mcp.tool
def hello(name: str) -> str:
return f"Hello, {name}!"
if __name__ == "__main__":
mcp.run() # Default: uses STDIO transport
```
FastMCP supports three transport protocols:
**STDIO (Default)**: Best for local tools and command-line scripts.
```python
mcp.run(transport="stdio") # Default, so transport argument is optional
```
**Streamable HTTP**: Recommended for web deployments.
```python
mcp.run(transport="http", host="127.0.0.1", port=8000, path="/mcp")
```
**SSE**: For compatibility with existing SSE clients.
```python
mcp.run(transport="sse", host="127.0.0.1", port=8000)
```
See the [**Running Server Documentation**](https://gofastmcp.com/deployment/running-server) for more details.
## Contributing
Contributions are the core of open source! We welcome improvements and features.
### Prerequisites
- Python 3.10+
- [uv](https://docs.astral.sh/uv/) (Recommended for environment management)
### Setup
1. Clone the repository:
```bash
git clone https://github.com/jlowin/fastmcp.git
cd fastmcp
```
2. Create and sync the environment:
```bash
uv sync
```
This installs all dependencies, including dev tools.
3. Activate the virtual environment (e.g., `source .venv/bin/activate` or via your IDE).
### Unit Tests
FastMCP has a comprehensive unit test suite. All PRs must introduce or update tests as appropriate and pass the full suite.
Run tests using pytest:
```bash
pytest
```
or if you want an overview of the code coverage
```bash
uv run pytest --cov=src --cov=examples --cov-report=html
```
### Static Checks
FastMCP uses `pre-commit` for code formatting, linting, and type-checking. All PRs must pass these checks (they run automatically in CI).
Install the hooks locally:
```bash
uv run pre-commit install
```
The hooks will now run automatically on `git commit`. You can also run them manually at any time:
```bash
pre-commit run --all-files
# or via uv
uv run pre-commit run --all-files
```
### Pull Requests
1. Fork the repository on GitHub.
2. Create a feature branch from `main`.
3. Make your changes, including tests and documentation updates.
4. Ensure tests and pre-commit hooks pass.
5. Commit your changes and push to your fork.
6. Open a pull request against the `main` branch of `jlowin/fastmcp`.
Please open an issue or discussion for questions or suggestions before starting significant work!
We welcome contributions! See the [Contributing Guide](https://gofastmcp.com/development/contributing) for setup instructions, testing requirements, and PR guidelines.

View file

@ -2,15 +2,33 @@
## Supported Versions
FastMCP v2.x receives security updates. Earlier versions are no longer supported.
| Version | Supported |
| ------- | ------------------ |
| 2.x | :white_check_mark: |
| < 2.0 | :x: |
| 3.x | :white_check_mark: |
| 2.x | :x: |
| 1.x | :x: |
| 0.x | :x: |
## Reporting a Vulnerability
Please report security vulnerabilities privately using [GitHub's security advisory feature](https://github.com/jlowin/fastmcp/security/advisories/new).
Please report security vulnerabilities privately using [GitHub's security advisory feature](https://github.com/PrefectHQ/fastmcp/security/advisories/new). Do not open public issues for security concerns.
Do not open public issues for security concerns.
## Scope
We accept reports for vulnerabilities in FastMCP itself — the library code in this repository.
The following are **out of scope**:
- Vulnerabilities in third-party dependencies or the MCP SDK itself. We'll bump version floors for known CVEs, but the fix belongs upstream.
- Limitations of upstream identity providers that FastMCP cannot control.
- Issues that require the attacker to already have server-side access or control of the MCP server configuration.
## Disclosure Process
When we receive a valid report:
1. We triage the report and determine whether it affects FastMCP directly.
2. We develop and test a fix on a private branch.
3. We coordinate CVE assignment through GitHub's advisory process when warranted.
4. We publish the advisory and release a patched version.
5. We credit the reporter in the advisory (unless they prefer otherwise).

View file

@ -0,0 +1,73 @@
---
title: Auth Provider Environment Variables
---
## Decision: Remove automatic environment variable loading from auth providers
You can still use environment variables for configuration - you just read them yourself with `os.environ` instead of relying on FastMCP's automatic loading.
**Status:** Implemented in v3.0.0
### Background
Auth providers in v2.x used `pydantic-settings` to automatically load configuration from environment variables with a `FASTMCP_SERVER_AUTH_<PROVIDER>_` prefix. For example, `GitHubProvider` would read from:
- `FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID`
- `FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET`
- `FASTMCP_SERVER_AUTH_GITHUB_BASE_URL`
- etc.
This was implemented via a `*ProviderSettings(BaseSettings)` class in each provider, combined with a `NotSet` sentinel pattern to distinguish between "not provided" and `None`.
### Why remove it
1. **Maintenance burden**: Every new provider needed to implement the settings class, validators, and the `NotSet` merging logic. This was ~50-100 lines of boilerplate per provider.
2. **Documentation complexity**: Each provider needed documentation explaining both the parameter and the corresponding environment variable. This doubled the surface area to document and maintain.
3. **Contributor friction**: New contributors adding providers had to understand and replicate this pattern, which was a source of inconsistency and bugs.
4. **Marginal user value**: Python developers are comfortable with `os.environ["VAR"]` or `os.environ.get("VAR", default)`. The automatic loading saved a single line of code per parameter while adding significant complexity.
5. **Implicit behavior**: Magic environment variable loading makes it harder to understand where values come from. Explicit `os.environ` calls are more traceable.
### Migration path
The migration is trivial - users add explicit environment variable reads:
```python
# Before (v2.x)
auth = GitHubProvider() # Relied on env vars
# After (v3.0)
import os
auth = GitHubProvider(
client_id=os.environ["GITHUB_CLIENT_ID"],
client_secret=os.environ["GITHUB_CLIENT_SECRET"],
base_url=os.environ["MY_BASE_URL"],
)
```
Users can also use `os.environ.get()` with defaults, or any other configuration library they prefer (dotenv, dynaconf, etc.).
### Backwards compatibility
We chose not to provide backwards compatibility because:
1. This is a major version bump (v3.0), which is the appropriate time for breaking changes
2. The migration is straightforward (add `os.environ` calls)
3. Maintaining compatibility would require keeping all the boilerplate we're trying to remove
4. The pattern was likely not heavily used - most production deployments pass secrets explicitly rather than relying on magic prefixes
### What was removed
- `*ProviderSettings(BaseSettings)` classes from all auth providers
- `NotSet` sentinel usage in provider constructors
- `pydantic-settings` dependency for auth providers
- Environment variable documentation from provider docs
- Related test cases for env var loading
### Result
Provider constructors are now simple and explicit. Required parameters are actually required (Python raises `TypeError` if missing), and optional parameters have clear defaults. The code is more readable and easier to maintain.

View file

@ -0,0 +1,61 @@
# Consolidating Discovery Methods
This document captures the design decisions around component listing methods in FastMCP 3.0.
## Problem
The server had parallel implementations for listing components:
- `get_tools()` / `_list_tools()`
- `get_resources()` / `_list_resources()`
- `get_prompts()` / `_list_prompts()`
- `get_resource_templates()` / `_list_resource_templates()`
These were nearly identical but with subtle differences in dedup keys, logging, and return types. The `_list_*` methods were internal and used by the MCP protocol handlers, while `get_*` methods were the public API.
## Solution
The duplicate methods were consolidated into a single set of `list_*` methods. The old `get_*` plural methods and `_list_*` internal methods were both removed.
This happened in two phases:
1. **Consolidation** (Dec 2025): Merged `get_*` and `_list_*` into a single `get_*` method with an `apply_middleware` parameter.
2. **Rename** (Jan 2026): When `FastMCP` was refactored to inherit from `Provider`, the methods were renamed to `list_*` to align with the `Provider` interface. The `apply_middleware` parameter was renamed to `run_middleware` with a default of `True`.
```python
async def list_tools(self, *, run_middleware: bool = True) -> Sequence[Tool]:
"""Canonical method for listing tools."""
...
```
## Key Changes
### Return Type: dict → list
The dict return type was removed because the key was redundant—components already have `.name` or `.uri` attributes.
```python
# Before (v2.x)
tools = await server.get_tools()
tool = tools["my_tool"]
# After (v3.0)
tools = await server.list_tools()
tool = next(t for t in tools if t.name == "my_tool")
```
### Middleware via Parameter
The `run_middleware=True` parameter (default) applies the middleware chain. This replaces the separate `_list_*_middleware()` methods.
## Benefits
1. **Single source of truth** - One method, not two
2. **Consistent behavior** - Same dedup key, same visibility filtering
3. **Clearer API** - Public method with explicit middleware opt-in
4. **Provider alignment** - `FastMCP.list_tools()` overrides `Provider.list_tools()`
5. **Less code** - Deleted ~200 lines of duplicate implementation
## Implementation Files
- `src/fastmcp/server/server.py` - Canonical `list_*` methods
- `src/fastmcp/server/providers/` - Provider base class defines the interface

View file

@ -0,0 +1,121 @@
# Prompt Internal Types - Message and PromptResult
**Version:** 3.0.0
**Impact:** Breaking change for prompts returning `mcp.types.PromptMessage`
## Summary
Prompts now use FastMCP's `Message` and `PromptResult` types internally, following the same pattern as resources (#2734). MCP SDK types are only used at the protocol boundary.
## What Changed
### Before (v2.x)
```python
from mcp.types import PromptMessage, TextContent
@mcp.prompt
def my_prompt() -> PromptMessage:
return PromptMessage(
role="user",
content=TextContent(type="text", text="Hello")
)
```
### After (v3.0)
```python
from fastmcp.prompts import Message
@mcp.prompt
def my_prompt() -> Message:
return Message("Hello") # role defaults to "user"
```
## Type Constraints
### Prompt Function Return Types
```python
str | list[Message | str] | PromptResult
```
**Valid:**
- `return "Hello"` → wrapped as single user Message
- `return [Message("Hi"), Message("Response", role="assistant")]`
- `return ["Hi", "Response"]` → strings auto-wrapped as user Messages
- `return PromptResult(messages=[...], meta={...})`
**Invalid (now raises error):**
- `return PromptMessage(...)` → Use `Message` instead
- `return Message(...)` as single value → Use `PromptResult([Message(...)])` or return a list
### Message Class
```python
Message(
content: Any, # Auto-serializes non-str to JSON
role: Literal["user", "assistant"] = "user"
)
```
**Auto-Serialization:**
- `str` → passes through as TextContent
- `dict` → JSON-serialized to text
- `list` → JSON-serialized to text
- `BaseModel` → JSON-serialized to text
- `TextContent` / `EmbeddedResource` → passes through directly
### PromptResult Class
```python
PromptResult(
messages: str | list[Message], # str wrapped as single Message
description: str | None = None,
meta: dict[str, Any] | None = None
)
```
## Why This Change?
1. **Simpler API** - `Message("Hello")` vs `PromptMessage(role="user", content=TextContent(type="text", text="Hello"))`
2. **Auto-serialization** - Dicts/lists/models automatically become JSON
3. **Consistent with resources** - Same pattern as `ResourceContent`/`ResourceResult`
4. **Type safety** - Strict typing catches errors at development time
## Migration Guide
### Simple Message
```python
# Before
from mcp.types import PromptMessage, TextContent
return PromptMessage(role="user", content=TextContent(type="text", text="Hello"))
# After
from fastmcp.prompts import Message
return Message("Hello")
```
### Conversation
```python
# Before
return [
PromptMessage(role="user", content=TextContent(type="text", text="Hi")),
PromptMessage(role="assistant", content=TextContent(type="text", text="Hello!")),
]
# After
return [
Message("Hi"),
Message("Hello!", role="assistant"),
]
```
### With Metadata
```python
from fastmcp.prompts import Message, PromptResult
return PromptResult(
messages=[Message("Analyze this")],
meta={"priority": "high"}
)
```
## PR
- #2738 - Introduce Message and PromptResult as canonical prompt types

View file

@ -0,0 +1,116 @@
# Provider Architecture: FastMCPProvider + TransformingProvider
**Version:** 3.0.0
**Impact:** Breaking change - `MountedProvider` removed
## Summary
The monolithic `MountedProvider` was split into two focused, composable components:
- **`FastMCPProvider`**: Wraps a FastMCP server, exposing its components through the Provider interface
- **`TransformingProvider`**: Wraps any provider to apply namespace prefixes and tool renames
## Why the Split?
`MountedProvider` was doing two things:
1. Wrapping a FastMCP server as a provider
2. Transforming component names with prefixes
Separating these concerns enables:
- Reusing transformations on any provider (not just FastMCP servers)
- Stacking transformations via composition
- Clearer mental model
## New API
### FastMCPProvider
Wraps a FastMCP server to expose it through the Provider interface:
```python
from fastmcp.server.providers import FastMCPProvider
sub_server = FastMCP("Sub")
@sub_server.tool
def greet(name: str) -> str:
return f"Hello, {name}!"
# Wrap as provider
provider = FastMCPProvider(sub_server)
main_server.add_provider(provider)
```
### TransformingProvider
Wraps any provider to apply transformations:
```python
# Apply namespace to all components
provider = FastMCPProvider(server).with_namespace("api")
# "my_tool" → "api_my_tool"
# "resource://data" → "resource://api/data"
# Rename specific tools (bypasses namespace)
provider = FastMCPProvider(server).with_transforms(
namespace="api",
tool_renames={"verbose_tool_name": "short"}
)
# "verbose_tool_name" → "short"
# "other_tool" → "api_other_tool"
```
### Stacking Transformations
Transformations compose via stacking:
```python
provider = (
FastMCPProvider(server)
.with_namespace("inner")
.with_namespace("outer")
)
# "tool" → "outer_inner_tool"
```
## mount() Uses This Internally
`FastMCP.mount()` now creates a `FastMCPProvider` + `TransformingProvider` internally:
```python
main.mount(sub, namespace="api")
# Equivalent to:
main.add_provider(
FastMCPProvider(sub).with_namespace("api")
)
```
## Breaking Changes
### MountedProvider Removed
```python
# Before (2.x)
from fastmcp.server.providers import MountedProvider
provider = MountedProvider(server, prefix="api")
# After (3.x)
from fastmcp.server.providers import FastMCPProvider
provider = FastMCPProvider(server).with_namespace("api")
```
### prefix → namespace
```python
# Before (deprecated)
main.mount(sub, prefix="api")
# After
main.mount(sub, namespace="api")
```
## Implementation PRs
- #2653 - Split MountedProvider into FastMCPProvider + TransformingProvider
- #2635 - Initial MountedProvider (superseded by #2653)

View file

@ -0,0 +1,60 @@
# Provider Tests: Direct Server Calls
This document captures the design decision to test providers via direct server method calls rather than wrapping in a Client.
## Problem
Provider tests were using the Client pattern:
```python
async with Client(mcp) as client:
result = await client.call_tool("add", {"x": 1, "y": 2})
assert result.data == 3
```
This conflated two concerns:
1. Does the provider/server work correctly?
2. Does the Client-Server interaction work correctly?
Additionally, ~1,200 lines of tests in `test_server_interactions.py` duplicated provider tests.
## Solution
Provider tests now call server methods directly:
```python
result = await mcp.call_tool("add", {"x": 1, "y": 2})
assert result.structured_content == {"result": 3}
```
This establishes clear test ownership:
- **Provider tests** → verify server functionality
- **Integration tests** → verify Client-Server interaction
## Result Access Patterns
Direct server calls return canonical FastMCP types, not MCP protocol types:
| Component | Access Pattern |
|-----------|----------------|
| Tool | `result.structured_content` or `result.text` |
| Resource | `result.contents[0].content` |
| Prompt | `result.messages[0].content.text` |
## Error Types
Direct calls raise FastMCP exceptions:
- `NotFoundError` - component not found
- `DisabledError` - component disabled by visibility
Client calls raise MCP protocol errors (wrapped in `McpError`).
## Implementation
- Consolidated duplicate tests from `test_server_interactions.py` into provider test files
- Reduced `test_server_interactions.py` from 1,455 → 179 lines
- Only `TestMeta` tests remain in interactions file (require Client for context injection)
## PR
- #2748 - Convert provider tests to use direct server calls

View file

@ -0,0 +1,196 @@
# Resource Internal Types - Strict Typing for Type Safety
**Version:** 3.0.0
**Impact:** Breaking change for resources returning dict/list
## Summary
ResourceResult now enforces strict typing to catch errors at development time (via type checker) rather than at runtime (when a client reads a resource).
## What Changed
### Before (v2.x)
```python
@mcp.resource("data://config")
def get_config() -> dict: # Auto-serialized to JSON
return {"key": "value"}
@mcp.resource("data://items")
def get_items() -> list: # Each item auto-wrapped
return ["item1", "item2"]
ResourceResult({"key": "value"}) # Dict auto-converted
ResourceResult(["a", "b"]) # List split into items
```
### After (v3.0)
```python
@mcp.resource("data://config")
def get_config() -> str: # Explicit JSON serialization
import json
return json.dumps({"key": "value"})
@mcp.resource("data://items")
def get_items() -> ResourceResult: # Explicit multi-item response
return ResourceResult([
ResourceContent("item1"),
ResourceContent("item2"),
])
ResourceResult([ResourceContent(...)]) # Explicit list wrapping
# Dict/list raises TypeError
```
## Type Constraints
### Resource.read() Return Type
```python
str | bytes | ResourceResult
```
**Valid:**
- `return "text content"`
- `return b"binary data"`
- `return ResourceResult([ResourceContent(...)])`
**Invalid (now raises TypeError):**
- `return {"key": "value"}` → Use `json.dumps()` instead
- `return ["item1", "item2"]` → Use `ResourceResult([ResourceContent(...)])`
- `return ResourceContent(...)` → Use `ResourceResult([ResourceContent(...)])`
### ResourceResult Type Signature
```python
ResourceResult(
contents: str | bytes | list[ResourceContent],
meta: dict[str, Any] | None = None
)
```
**Valid:**
- `ResourceResult("plain text")`
- `ResourceResult(b"binary")`
- `ResourceResult([ResourceContent(...), ResourceContent(...)])`
**Invalid (now raises TypeError):**
- `ResourceResult({"key": "value"})` → Dict not supported
- `ResourceResult(["a", "b"])` → Bare list not supported (must be list[ResourceContent])
- `ResourceResult(resource_content_obj)` → Single item must be in list
### ResourceContent Type Signature
```python
ResourceContent(
content: Any, # Auto-serializes non-str/bytes to JSON
mime_type: str | None = None,
meta: dict[str, Any] | None = None
)
```
**Auto-Serialization in ResourceContent.__init__:**
- `str` → passes through (mime_type defaults to "text/plain")
- `bytes` → passes through (mime_type defaults to "application/octet-stream")
- `dict` → JSON-serialized string (mime_type defaults to "application/json")
- `list` → JSON-serialized string (mime_type defaults to "application/json")
- `BaseModel` → JSON-serialized string (mime_type defaults to "application/json")
## Why This Change?
The old auto-conversion behavior was convenient but hid errors:
```python
# Old behavior - silent failure
return ["item1", "item2"] # Client sees 2 items OR JSON array?
# Ambiguous! Users would discover issues only when client reads resource
# New behavior - caught at dev time
return ["item1", "item2"] # Type checker error immediately
# Must explicitly write:
return json.dumps(["item1", "item2"]) # Clear intent
# OR:
return ResourceResult([ResourceContent("item1"), ResourceContent("item2")])
```
Type checkers now catch return type mismatches during development rather than at runtime.
## Migration Guide
### Returning JSON Data
**Before:**
```python
def get_config() -> dict:
return {"key": "value", "nested": {"a": 1}}
```
**After:**
```python
import json
def get_config() -> str:
return json.dumps({"key": "value", "nested": {"a": 1}})
```
### Returning Multiple Items
**Before:**
```python
def get_items() -> list:
return ["user1", "user2", "user3"]
```
**After (Option 1: Single JSON array):**
```python
import json
def get_items() -> str:
return json.dumps(["user1", "user2", "user3"])
```
**After (Option 2: Multiple content items):**
```python
from fastmcp.resources import ResourceContent, ResourceResult
def get_items() -> ResourceResult:
return ResourceResult([
ResourceContent("user1"),
ResourceContent("user2"),
ResourceContent("user3"),
])
```
### Returning Structured Data with Custom MIME Types
**Before:**
```python
def get_html() -> dict:
return {"html": "<div>content</div>"}
```
**After:**
```python
from fastmcp.resources import ResourceContent, ResourceResult
def get_html() -> ResourceResult:
return ResourceResult([
ResourceContent(
content="<div>content</div>",
mime_type="text/html"
)
])
```
## Type Checking
Your type checker will now catch these errors:
```python
@mcp.resource("data://test")
def bad_resource() -> dict: # ← Type error: should be str | bytes | ResourceResult
return {"key": "value"}
```
This is intentional. The type system enforces correct typing at development time.
## Backward Compatibility
**This is a breaking change.** Code that returns dict or list from resources will:
1. **Pass type checking**: If you ignore type warnings
2. **Fail at runtime**: Raises `TypeError` when client reads the resource
Migrate to explicit JSON serialization or ResourceResult.

View file

@ -0,0 +1,109 @@
# Explicit task_meta Parameter for Background Tasks
This document captures the design decision to add explicit `task_meta` parameters to component execution methods, replacing context variable-based task routing.
## Problem
Background task execution used context variables (`_task_metadata`, `_docket_fn_key`) to pass task metadata through the call stack. This was implicit and had several issues:
1. **Hidden state** - Task metadata flowed through context vars, making it hard to trace
2. **Fragile enrichment** - `fn_key` was enriched in 9 different places (component methods + provider wrappers)
3. **Testing difficulty** - Required setting context vars to test background behavior
4. **No programmatic API** - Users couldn't explicitly request background execution via `call_tool()`
## Solution
Add explicit `task_meta: TaskMeta | None` parameters to:
- `FastMCP.call_tool()`, `FastMCP.read_resource()`, `FastMCP.render_prompt()`
- Component methods: `Tool._run()`, `Resource._read()`, `Prompt._render()`, `ResourceTemplate._read()`
```python
from fastmcp.server.tasks import TaskMeta
# Explicit background execution
result = await server.call_tool("my_tool", {"arg": "value"}, task_meta=TaskMeta(ttl=300))
# Returns CreateTaskResult for background, ToolResult for sync
```
## fn_key Enrichment Centralization
Previously, `fn_key` (the Docket registry key) was set in 9 places:
**Component methods (5):**
- `Tool._run()`
- `Resource._read()`
- `ResourceTemplate._read()` (2 places)
- `Prompt._render()`
**Provider wrappers (4):**
- `FastMCPProviderTool._run()`
- `FastMCPProviderResource._read()`
- `FastMCPProviderPrompt._render()`
- `FastMCPProviderResourceTemplate._read()`
Now, `fn_key` is set in **3 places** (server methods only):
```python
# In call_tool(), after finding the tool:
if task_meta is not None and task_meta.fn_key is None:
task_meta = replace(task_meta, fn_key=tool.key)
# In read_resource(), after finding resource or template:
if task_meta is not None and task_meta.fn_key is None:
task_meta = replace(task_meta, fn_key=resource.key) # or template.key
# In render_prompt(), after finding the prompt:
if task_meta is not None and task_meta.fn_key is None:
task_meta = replace(task_meta, fn_key=prompt.key)
```
## Why This Works for Mounted Servers
For mounted servers, `provider.get_tool(name)` returns a `FastMCPProviderTool` whose `.key` is already namespaced (e.g., `"tool:child_multiply"`). So setting `fn_key = tool.key` in the parent server gives the correct namespaced key.
When the provider wrapper delegates to the child server, `fn_key` is already set, so the child server won't override it.
## Type-Safe Overloads
Each method uses `@overload` to provide correct return types:
```python
@overload
async def call_tool(
self, name: str, arguments: dict[str, Any], *, task_meta: None = None
) -> ToolResult: ...
@overload
async def call_tool(
self, name: str, arguments: dict[str, Any], *, task_meta: TaskMeta
) -> ToolResult | mcp.types.CreateTaskResult: ...
```
## Middleware Runs Before Docket
A key fix from #2663: background tasks now properly pass through all middleware stacks before being submitted to Docket. Previously, background task submission bypassed middleware entirely.
The flow is now:
1. MCP handler extracts task metadata from request
2. Server method (`call_tool`, etc.) finds component via provider
3. Server enriches `task_meta.fn_key` with component key
4. Component's `_run()`/`_read()`/`_render()` is called
5. Middleware runs (logging, auth, rate limiting, etc.)
6. `check_background_task()` submits to Docket if task_meta present
For mounted servers, the wrapper components delegate to the child server, which runs the child's middleware before the actual execution or Docket submission.
## Removed Dead Code
- `_task_metadata` context variable
- `_docket_fn_key` context variable
- `get_task_metadata()` function
- `key` parameter in `check_background_task()` (backwards compat fallback)
## Implementation PRs
- #2663 - Components own execution; middleware runs before Docket
- #2749 - `task_meta` for `call_tool()`
- #2750 - `task_meta` for `read_resource()`
- #2751 - `task_meta` for `render_prompt()` + fn_key centralization

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,113 @@
# Visibility & Enable/Disable Design
This document captures the design decisions for the enable/disable system in FastMCP 3.0.
## Core Principle
**Components describe capabilities. Servers and providers control availability.**
Previously, each component had an `enabled` field that users could mutate directly. This caused a fundamental problem: when components pass through providers (especially TransformingProvider), you receive copies—and mutating a copy doesn't affect the original.
## Solution: Hierarchical Visibility
Both servers and providers maintain their own `VisibilityFilter`. If a component is disabled at any level, it's disabled up the chain.
```
Provider A (filters) → Provider B (filters) → Server (filters) → Client sees only enabled components
```
## VisibilityFilter
The `VisibilityFilter` class (`src/fastmcp/utilities/visibility.py`) provides:
### Blocklist (disable)
```python
server.disable(keys=["tool:my_tool"]) # Hide specific component
server.disable(tags={"internal"}) # Hide all components with tag
```
### Allowlist (enable with only=True)
```python
server.enable(tags={"public"}, only=True) # Show ONLY components with tag
```
### Blocklist Wins
If a component is in both blocklist and allowlist, blocklist wins. This ensures you can always hide something regardless of other filters.
### Change Detection
The `VisibilityFilter` only sends notifications when visibility actually changes:
- Disabling an already-disabled component: no notification
- Enabling an already-enabled component: no notification
- Actual state change: notification sent
## Vocabulary
Consistent verbs throughout the codebase:
- `enable()` / `disable()` - methods on servers and providers
- `is_enabled()` - check if component is visible
- `_disabled_keys`, `_disabled_tags` - blocklist state
- `_enabled_keys`, `_enabled_tags` - allowlist state
- `_default_enabled` - True unless `only=True` was used
## Notifications
`VisibilityFilter` handles notifications directly via `_send_notification()`. This:
1. Gets the current request context (if any)
2. Queues the appropriate list-changed notification
3. No-ops gracefully outside request context
This simplifies the code—no callback wiring needed between VisibilityFilter and its owners.
## Migration from 2.x
### Component enable/disable removed
```python
# Before (2.x) - BROKEN: mutates a copy
tool.disable()
# After (3.x)
server.disable(keys=["tool:my_tool"])
```
### enabled field removed
```python
# Before (2.x)
@mcp.tool(enabled=False)
def my_tool(): ...
# After (3.x)
@mcp.tool
def my_tool(): ...
mcp.disable(keys=["tool:my_tool"])
```
### include_tags/exclude_tags deprecated
```python
# Before (deprecated)
mcp = FastMCP("server", exclude_tags={"internal"})
# After
mcp = FastMCP("server")
mcp.disable(tags={"internal"})
```
## Component Keys
Components use prefixed keys for enable/disable:
- Tools: `"tool:function_name"`
- Prompts: `"prompt:prompt_name"`
- Resources: `"resource:resource://uri"`
- Templates: `"template:resource://{param}/path"`
Use `component.key` to get the correct key format.
## Implementation Files
- `src/fastmcp/utilities/visibility.py` - VisibilityFilter class
- `src/fastmcp/server/providers/base.py` - Provider.enable/disable
- `src/fastmcp/server/server.py` - FastMCP.enable/disable
- `src/fastmcp/utilities/components.py` - Component.enable/disable raise NotImplementedError

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

@ -0,0 +1,595 @@
---
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](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 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](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 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 test="skip"
from fastmcp.types import TextContent, Tool, ToolAnnotations, ErrorData
```
The re-export set is deliberately limited to names that trace to a documented user import: `TextContent`, `ImageContent`, `AudioContent`, `EmbeddedResource`, `ResourceLink`, `ContentBlock`, `Tool`, `Resource`, `ResourceTemplate`, `Prompt`, `PromptMessage`, `CallToolResult`, `GetPromptResult`, `ReadResourceResult`, `TextResourceContents`, `BlobResourceContents`, `SamplingMessage`, `CreateMessageResult`, `SamplingCapability`, `Root`, `ErrorData`, `Completion`, `Annotations`, `ToolAnnotations`, `Icon`, `ToolResultContent`, plus the pre-existing `Textarea`. Notification and request wrapper types (e.g. `ToolListChangedNotification`) are not re-exported — import those from `mcp_types` directly.
*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:
```python
from fastmcp import Client
async def read_schema():
async with Client("my_mcp_server.py") as client:
tools = await client.list_tools()
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); `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()`).
### The bridge is a genuine runtime toggle — Absorbed (post-review fix)
The bridge properties install unconditionally, and each getter reads the live `mcp_camelcase_compat` setting on every access: warn-and-return when enabled, raise `AttributeError` when disabled. An earlier version installed the bridge once at import, so flipping the setting afterward did nothing — commit `d9659453` fixed this so the toggle works at runtime:
```python
import fastmcp
fastmcp.settings.mcp_camelcase_compat = False # now takes effect immediately
```
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`.
### `mcp-types` is now a core slim dependency — Absorbed (post-review fix)
Bare `import fastmcp` loads `mcp_types` via `_sdk_patches` and `_compat`, so a bare `fastmcp-slim` install (without the `[mcp]` extra) hit `ModuleNotFoundError`. Because `mcp-types` only pulls `pydantic` and `typing-extensions` (already core), it was promoted to a core dependency while the full `mcp` SDK stays in the `[mcp]` extra.
*Verify:* `fastmcp_slim/pyproject.toml` (`mcp-types==2.0.0b1` in core dependencies), commit `e16ffad4`.
### `McpError` is an alias; construction changed — Bridged (catch) / Breaking (construct)
`fastmcp.exceptions.McpError` is a plain alias of the SDK's `MCPError` — a plain alias, not a subclass, so `except McpError` still catches SDK-raised errors and `err.error.code` still reads:
```python
from fastmcp.exceptions import McpError
try:
...
except McpError as err:
print(err.error.code) # unchanged
```
Construction is the one unavoidable behavior break. The v1 pattern of wrapping an `ErrorData` positionally raises `TypeError` under v2; construct with keywords instead:
```python
from fastmcp.exceptions import McpError
# Before (raises TypeError under SDK v2):
# raise McpError(ErrorData(code=-32000, message="Client not supported"))
raise McpError(code=-32000, message="Client not supported")
```
*Verify:* `fastmcp_slim/fastmcp/exceptions.py` (`McpError = MCPError`).
## Server core
The SDK v2 rewrote the server request-handling model. FastMCP's handler layer is the most heavily rewritten part of the migration, but the public server API is unchanged.
### Handler adapters — Absorbed
Handlers are now registered by method string via `add_request_handler(method, params_type, handler)`, take a uniform `(ctx, params)` signature, and return the **bare** result model (no `ServerResult` wrapper). FastMCP's `_setup_handlers` builds one thin adapter per method (`tools/list`, `tools/call`, `resources/read`, `prompts/get`, `logging/setLevel`, …) that binds the request context, adapts params to the existing handler body, and returns the bare result. The v1 decorator overrides and `_wrap_list_handler` are deleted.
*Verify:* `fastmcp_slim/fastmcp/server/low_level.py` (462 lines changed), `fastmcp_slim/fastmcp/server/mixins/mcp_operations.py`.
### FastMCP-owned request context — Absorbed
The SDK's `request_ctx` ContextVar is gone; the SDK passes context to handlers as an argument only. FastMCP owns its own `fastmcp_request_ctx` ContextVar, set at the top of every adapter. It stores a FastMCP-owned `FastMCPRequestContext` wrapper rather than the raw SDK context, because the raw `ServerRequestContext.meta` is a bare `TypedDict` carrying only `progress_token` — the full `_meta` block (which holds `_meta.fastmcp.version` and the distributed-trace parent) has to be lifted out of the raw params dict. `Context.request_context` and its consumers (`report_progress`, `session_id`, telemetry trace extraction, `get_http_request`) all read through the wrapper.
*Verify:* `fastmcp_slim/fastmcp/server/dependencies.py`, `server/context.py`, `server/telemetry.py`.
### `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 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](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`.
### Per-session state re-homed to the connection — Absorbed
Because `ServerSession` is now per-request, per-session state can no longer live on the session object. The minimum logging level is re-homed to a FastMCP-side map keyed by session id (via `connection.session_id`), and `client_supports_extension` becomes a free function reading `session.client_params.capabilities`.
*Verify:* `fastmcp_slim/fastmcp/server/low_level.py`, `server/context.py` (`_log_to_server_and_client`).
### `extensions` capability read from the real field — Absorbed (post-review fix)
SDK v2 declares `extensions` as a real field on `ClientCapabilities`, so a client sending `ClientCapabilities(extensions={...})` populates the field, not `model_extra`. `client_supports_extension` now reads `caps.extensions` first and falls back to `model_extra` only for legacy-serialized clients.
*Verify:* commit `96ca0092`, `server/low_level.py` / `server/context.py`.
### Task protocol and the `_sdk_patches` shim — Absorbed (with an upstream gap)
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](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)
SDK v2 seeds an `OpenTelemetryMiddleware` into every lowlevel `Server`, so each inbound request already emits a SERVER span. FastMCP emits its own richer SERVER span per request (with `fastmcp.*` and auth/session attributes), so a server with an OTel exporter installed would export **two** SERVER spans per request under different attribute conventions. `LowLevelServer.__init__` now drops the SDK's seeded `OpenTelemetryMiddleware` (matched by type, not position, leaving any other seeded middleware intact) and keeps FastMCP's spans. Inbound W3C trace-context extraction is unaffected — FastMCP's telemetry reads `traceparent` from `_meta` itself, so distributed traces still link client to server. Client-side is not double-counted: the SDK's `ClientSession` emits a low-level `MCP send <method>` CLIENT span that nests *under* FastMCP's high-level client span, a legitimate parent/child hierarchy rather than a duplicate.
*Verify:* `fastmcp_slim/fastmcp/server/low_level.py` (the `OpenTelemetryMiddleware` filter); `tests/server/telemetry/test_server_tracing.py::TestSingleServerSpan`.
### 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. `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.
`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)
Resource-not-found responses from the core `resources/read` handler previously used `-32002`. SEP-2164 (and the SDK's own mcpserver, which maps `ResourceNotFoundError``INVALID_PARAMS`) makes this `-32602`. The per-adapter `MCPError(code=..., ...)` literals in `server/mixins/mcp_operations.py` are replaced by a single `fastmcp.exceptions.to_mcp_error()` translator that maps FastMCP's public exceptions to the `mcp_types` code constants (`NotFoundError`/`DisabledError`/`ValidationError``INVALID_PARAMS`, else `INTERNAL_ERROR`). Clients that string-matched on the old `-32002` for resource-not-found must switch to `-32602`; the human-readable message ("Resource not found: ...") is unchanged. The opt-in `ErrorHandlingMiddleware`, which has its own documented per-method-prefix code mapping, is intentionally left as-is.
*Verify:* `fastmcp_slim/fastmcp/exceptions.py` (`to_mcp_error`); `fastmcp_slim/fastmcp/server/mixins/mcp_operations.py`; `tests/test_exceptions.py`.
### `Cachable*` response-cache models renamed to `Cacheable*` — Breaking (rename) <!-- codespell:ignore -->
The response-caching middleware's Pydantic wrapper models — used to serialize cached tool, resource, and prompt results for `ResponseCachingMiddleware` — carried a spelling typo. `CachableToolResult`, `CachableResourceContent`, `CachableResourceResult`, `CachableMessage`, and `CachablePromptResult` are renamed to `CacheableToolResult`, `CacheableResourceContent`, `CacheableResourceResult`, `CacheableMessage`, and `CacheablePromptResult`. None of these classes are re-exported from `fastmcp` or any package `__init__.py`, so the realistic blast radius is limited to code that imported the old names directly from `fastmcp.server.middleware.caching`:
```python
# Before (now raises ImportError):
# from fastmcp.server.middleware.caching import CachableToolResult
# After
from fastmcp.server.middleware.caching import CacheableToolResult
```
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 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
All SDK transports (`streamable_http_client`, `sse_client`, `stdio_client`) now yield a 2-tuple `(read, write)` instead of exposing a third `get_session_id` element. HTTP configuration flows through a caller-supplied `http_client=`. Only the tuple unpack changed on the FastMCP side.
*Verify:* `fastmcp_slim/fastmcp/client/transports/http.py`, `transports/sse.py`, `transports/stdio.py`.
### Float timeouts; `timedelta` still accepted — Absorbed
The SDK session and call timeouts are now plain floats. FastMCP's public `Client(timeout=...)` still accepts a `timedelta`, a plain float, or an int, normalizing through the existing `normalize_timeout_to_seconds` at the `SessionKwargs` chokepoint:
```python
from datetime import timedelta
from fastmcp import Client
client = Client("my_mcp_server.py", timeout=timedelta(seconds=30)) # still works
client = Client("my_mcp_server.py", timeout=30.0) # also works
```
*Verify:* `fastmcp_slim/fastmcp/client/transports/base.py` (`SessionKwargs.read_timeout_seconds: float | None`), `client/client.py`.
### 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](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.
A client only passes the argument when it wants non-default settings, so an ordinary `Client` is unaffected and transports that don't accept it keep working. A custom `ClientTransport` used as a *proxy backend* must accept and honor it:
```python
import contextlib
from fastmcp.client.transports.base import ClientTransport, TransportOptions
class MyTransport(ClientTransport):
@contextlib.asynccontextmanager
async def connect_session(self, *, transport_options=None, **session_kwargs):
options = transport_options or TransportOptions()
async with options.session_class(read, write, **session_kwargs) as session:
yield session
```
A transport that wraps others must pass it along; `MCPConfigTransport` forwards it to both its single-server delegate and its composite server.
*Verify:* `fastmcp_slim/fastmcp/client/transports/base.py` (`TransportOptions`), the four built-in transports, `transports/config.py`, and `tests/server/providers/proxy/test_proxy_server.py`.
### `get_session_id` via header sniff — Bridged
The SDK dropped `get_session_id` from the streamable-HTTP transport with no replacement (the SDK source has an author TODO acknowledging it breaks the Transport protocol). FastMCP reconstructs it by registering an httpx2 response event hook on the client it owns, capturing the `mcp-session-id` response header (httpx2 preserves httpx's `event_hooks` API). The removal trigger is the upstream TODO.
*Verify:* `fastmcp_slim/fastmcp/client/transports/http.py` (`_capture_session_id`, `get_session_id`).
### Pagination via `params=` — Absorbed
The SDK's `cursor=` kwarg on `list_*` is gone; pagination now flows through `params=PaginatedRequestParams(cursor=...)`. FastMCP's public `cursor=` on the `list_*_mcp` methods is preserved and translated internally.
*Verify:* `fastmcp_slim/fastmcp/client/mixins/{tools,resources,prompts}.py`.
### OAuth `callback_handler` returns `AuthorizationCodeResult` — Breaking (advanced)
The one OAuth break: a custom `callback_handler` must return an `AuthorizationCodeResult` (fields `code`, `state`, `iss`) instead of the old `tuple[str, str | None]`. Everything else in the OAuth surface — `OAuthClientProvider` kwargs, `TokenStorage`, `async_auth_flow` — is unchanged.
*Verify:* `fastmcp_slim/fastmcp/client/auth/oauth.py`.
### Notification dispatch unwrapped — Absorbed
The client's notification handling was reworked for the v2 message model. Custom server-to-client notifications (like SEP-1686 `notifications/tasks/status`) are no longer tee'd to a user `message_handler` — the SDK routes them only through `NotificationBinding` (see sdk-feedback #8). FastMCP registers a binding so task-status updates reach the Task registry.
*Verify:* `fastmcp_slim/fastmcp/client/messages.py`, `client/tasks.py`.
### `SDKServer` alias — Absorbed (post-review rename)
The in-memory transport resolves the low-level server per server type. The alias for the SDK's own `MCPServer` was renamed from the misleading `FastMCP1Server` / `FastMCP1x` to `SDKServer`, since it names the SDK v2 server, not a FastMCP 1.x object.
*Verify:* commit `5c3b82e4`; `client/client.py`, `client/transports/memory.py`, `server/providers/proxy.py`, `cli/run.py`.
### Proxy request-context stash — Absorbed (post-review fix)
Proxy forwarding handlers stash the request context so a backend that issues a server-initiated request (list_roots/sampling/elicitation) can relay it back to the proxy's own client. This stash was initially applied only on the tool path; commit `1ac166bd` extended it to proxied resources, templates, and prompts.
*Verify:* commit `1ac166bd`, `server/providers/proxy.py`.
### Shared response cache via `KeyValueResponseCacheStore` — New
The SDK's client response cache (SEP-2549) reads and writes through a pluggable `ResponseCacheStore`; the default is a per-client in-memory LRU. FastMCP adds `KeyValueResponseCacheStore`, an adapter over the same `AsyncKeyValue` key-value abstraction the event store and OAuth proxy already use, so a fleet of clients (e.g. proxy replicas) can share one Redis-backed response cache. Pass it via `CacheConfig(store=...)`; a custom store requires an explicit `partition` (SDK) and `target_id` (FastMCP). Results serialize through a type-tagged envelope validated against an allowlist of cacheable result models — an unknown tag is a cache miss, never an import-by-name — and each adapter owns its own collection so `clear()` never touches another tenant.
```python
from fastmcp.client.caching import KeyValueResponseCacheStore
from mcp.client.caching import CacheConfig
from key_value.aio.stores.redis import RedisStore
store = KeyValueResponseCacheStore(storage=RedisStore(url="redis://localhost"))
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](feature-program.md)).
### Kept overrides — Absorbed
Four overrides survive, each for a concrete reason:
1. **Event-store session scoping.** The SDK hands every per-session transport the *same* `event_store` object, one stream-ID keyspace shared across sessions. FastMCP's `FastMCPStreamableHTTPSessionManager` returns a fresh `SessionScopedEventStore(shared, session_id=…)` per session, so resumability events don't leak across sessions.
2. **Lifespan reconciliation.** The SDK builder enters the bare lowlevel `Server.lifespan` (which yields `{}`). FastMCP drives its own `_lifespan_manager` — ref-counted for mounts, Ctrl-C-shielded, docket-aware. The SDK path silently skips all of it, so FastMCP sets the server lifespan to delegate to `_lifespan_manager` and lets the manager enter it once.
3. **Graceful transport termination.** FastMCP's lifespan `finally` drains the manager's server instances via `transport.terminate()` before task-group cancel, fixing the Uvicorn "returned without completing response" edge (#3025). The SDK just cancels.
4. **User ASGI middleware hook.** The SDK builder hardcodes an empty middleware list and only appends auth. FastMCP's `http_app(middleware=...)` and `RequestContextMiddleware` have nowhere to go in the SDK path.
*Verify:* `fastmcp_slim/fastmcp/server/http.py`, `server/event_store.py`, `server/mixins/lifespan.py`.
### DNS-rebinding ownership — Absorbed (security)
FastMCP owns DNS-rebinding protection through its `HostOriginGuardMiddleware`, which is more expressive than the SDK's and is the documented surface. To avoid two allowlists double-blocking with confusing errors from two layers, FastMCP **always** disables the SDK's layer by passing `TransportSecuritySettings(enable_dns_rebinding_protection=False)` to the manager — both when FastMCP's protection is on (so they don't double-block) and when it's off (so the SDK's default-on flip can't silently re-enable it).
*Verify:* `fastmcp_slim/fastmcp/server/http.py` (`enable_dns_rebinding_protection=False`, `HostOriginGuardMiddleware`).
### httpx2 replaces httpx — Breaking (custom client/factory, typing) / Absorbed (everything else)
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 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:
- **Custom client factory / client.** `StreamableHttpTransport(httpx_client_factory=...)`, `SSETransport(httpx_client_factory=...)`, and `OAuth(httpx_client_factory=...)` factories must now return `httpx2.AsyncClient`; a custom `httpx.Auth` passed as `Client(auth=...)` should become `httpx2.Auth`. httpx2 is a drop-in fork, so the change is an import swap (`import httpx``import httpx2`). This is a typing break; at runtime a duck-compatible httpx client still satisfies the SDK, but mixing `httpx.Timeout`/`httpx.Auth` with an httpx2 client is unsupported.
- **OpenAPI client.** `FastMCP.from_openapi(client=...)` and `OpenAPIProvider(client=...)` are now type-hinted `httpx2.AsyncClient`. There is no `isinstance` gate, so an existing `httpx.AsyncClient` still works at runtime via duck-typing this release; the typing nudges you to httpx2.
- **TLS trust store.** httpx2 verifies TLS against the OS trust store via `truststore` (honoring `SSL_CERT_FILE`/`SSL_CERT_DIR` first) instead of the bundled certifi CA set. This now applies to **all** FastMCP HTTP, including server-auth upstream IdP calls — not just the client path. Corporate-CA and certifi-pinned setups may see different trust behavior.
- **Logger renames.** FastMCP HTTP now logs under `httpx2` and `httpcore2.*` (was `httpx`/`httpcore.*`). Anyone filtering FastMCP HTTP logs by logger name must update the names.
The session-id header hook (below) works unchanged: httpx2 keeps httpx's `event_hooks` API. FastMCP's tool/resource/prompt handlers still map upstream 429/timeout errors to actionable `ToolError`/`ResourceError`; because a user's own tool may raise from either library, `server/server.py` catches both `httpx2` and (if installed) legacy `httpx` `HTTPStatusError`/`TimeoutException` via a defensive `try: import httpx` shim.
*Verify:* `fastmcp_slim/pyproject.toml` (`mcp` extra lists only `httpx2`); no FastMCP source imports `httpx` except the documented defensive shim in `server/server.py`.
## Protocol eras
The SDK v2 serves multiple protocol eras from one server, and FastMCP formally embraces this.
### Dual-era serving — Absorbed (supersedes "latest only")
A single FastMCP server now handles clients across the protocol transition: the session-based handshake eras (through 2025-11-25) and the sessionless `2026-07-28` era (capability discovery via `server/discover`) simultaneously. This supersedes FastMCP's earlier "latest protocol only" stance.
### Per-feature era matrix — Breaking (feature availability by era)
The push-style Context features that require the server to call back into the client are unavailable on the sessionless `2026-07-28` era, because that era removes server-initiated requests (SEP-2577). The request/response features flow on every era.
| Context feature | Session-based eras | `2026-07-28` (sessionless) |
| --- | --- | --- |
| `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](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` 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 *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`.
### Server-initiated sampling and roots removed from the server API — Breaking
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 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`.
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 `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_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)
A FastMCP server can emit SEP-2549 freshness hints so a caching client (`fastmcp.Client(cache=...)`) may reuse a response without a wire round-trip. Two constructor params carry it: `FastMCP(cache_ttl=300, cache_scope="public")`, where `cache_ttl` is in seconds and `cache_scope` is `"public"` or `"private"` (default `"private"` when a TTL is set). The hint is uniform by construction — one server-level value applies to every SDK-cacheable method (`tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, `resources/read`, and `server/discover`) with 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. `cache_ttl` must be positive, and a `cache_scope` without a `cache_ttl` is rejected at construction (a scope alone does not enable caching, since the client gates on the TTL's presence). Absent both params, no hint is emitted. Honoring is modern-only (the SDK client reads hints only at `2026-07-28`) and opt-in on the client, so a hinted server is inert unless the client passes `cache=`.
*Verify:* `fastmcp_slim/fastmcp/server/caching.py` (`build_cache_hints`), `fastmcp_slim/fastmcp/server/server.py` (constructor params passed to `LowLevelServer(cache_hints=...)`), `tests/server/test_cache_hints.py` (unit validation + end-to-end interop with `fastmcp.Client(cache=True)`).
### 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](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](known-gaps.md) page.
## Security
FastMCP retains hardening that is not yet upstream and does not remove it during the migration.
### Retained OAuth / DCR hardening — Absorbed
FastMCP keeps its own DCR redirect-URI hardening (PRs #4419, #4408) regardless of the SDK's validation, which still accepts unsafe `javascript:`/`data:` redirect schemes at the model level (sdk-feedback #4). The streamable-HTTP DNS-rebinding protection above is a second retained security surface.
*Verify:* recent commits `67527c1f` (block unsafe OAuth redirect schemes), `57a27992` (DNS rebinding), `cccb529f` (DCR redirect URI validation) on `main`.
### 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](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`).
### Templated resource parameters are path-screened by default — Breaking (behavior)
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](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`.
## Removed in 4.0
Deprecations that warned in 3.x are removed in 4.0. Each entry below is a hard removal — the old surface raises `TypeError` / `AttributeError` rather than warning, unless noted otherwise.
### Module and class shims
- **`fastmcp.server.proxy`** (deprecated 3.0) — Breaking. Import proxy classes (`FastMCPProxy`, `ProxyClient`, etc.) from `fastmcp.server.providers.proxy` instead.
- **`fastmcp.server.openapi`** and its submodules (`server`, `components`, `routing`), including the **`FastMCPOpenAPI`** class (deprecated 3.0) — Breaking. Use `FastMCP` with an `OpenAPIProvider` from `fastmcp.server.providers.openapi` instead.
- **`fastmcp.experimental.server.openapi`** and **`fastmcp.experimental.utilities.openapi`** shims (deprecated 2.14) — Breaking. Import from `fastmcp.server.providers.openapi` and `fastmcp.utilities.openapi` respectively.
- **`fastmcp.server.apps`** and **`fastmcp.server.app`** shims (deprecated 3.2) — Breaking. Import from `fastmcp.apps` (e.g. `AppConfig`) or `fastmcp` (`FastMCPApp`) instead.
- **`PromptToolMiddleware`** and **`ResourceToolMiddleware`** (deprecated 3.1) — Breaking. Use the `PromptsAsTools` / `ResourcesAsTools` transforms from `fastmcp.server.transforms` instead. The non-deprecated `ToolInjectionMiddleware` base class is retained.
- **`StreamableHttpTransport(sse_read_timeout=...)`** (deprecated no-op) — Breaking. The parameter had no effect under the SDK v2 client; configure timeouts via `read_timeout_seconds` in `session_kwargs` or on the httpx2 client via `httpx_client_factory`. `SSETransport` still accepts `sse_read_timeout`.
### `FastMCP` server methods and `mount()` kwargs
The following `FastMCP` methods and parameters, deprecated since 3.0, are removed:
- `FastMCP.as_proxy(...)``create_proxy(...)` (`from fastmcp.server import create_proxy`)
- `FastMCP.import_server(sub)``mount(sub)`
- `mount(prefix=...)``mount(namespace=...)`
- `mount(as_proxy=...)` — removed; mounts always invoke the child's lifespan and middleware, so the flag was already meaningless. To proxy a server, wrap it with `create_proxy()` before mounting.
- `FastMCP.add_tool_transformation(name, config)``add_transform(ToolTransform({name: config}))`
- `FastMCP.remove_tool_transformation(name)` — removed; it was a no-op that only warned (transforms are immutable once added). Use `server.disable(keys=[...])` to hide tools.
- `FastMCP.remove_tool(name)``mcp.local_provider.remove_tool(name)`
The `_REMOVED_KWARGS` constructor shim (which raises helpful `TypeError`s for kwargs removed in 3.0) is retained through 4.0.
### 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](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** — 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

@ -0,0 +1,49 @@
---
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](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
FastMCP v4.0 is an engine swap. Three forces drive the major version:
**The MCP Python SDK v2 rebuild.** 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`). It also rewrites the server request-handling model — handlers are now registered by method string and return bare result models, there is no `request_ctx` ContextVar, and server-side middleware is a first-class SDK concept. FastMCP absorbs almost all of this so that a typical server needs zero code changes.
**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 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](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](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](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-3) guide. These development notes are the exhaustive version behind it.

View file

@ -0,0 +1,85 @@
---
title: Known Gaps and Upstream Dependencies
---
The migration ships with a set of deliberate gaps: temporary shims, xfailed tests, and pins that depend on the MCP Python SDK v2 reaching GA. Each is tracked here with its removal trigger. This page is the checklist for the beta-to-stable transition and the advisory relationship with the SDK team.
## The xfail register
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/`) — 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.
**Protocol eras (`tests/server/test_protocol_eras.py`).** One remaining strict xfail, and it too is task-related: the v2 SDK high-level client exposes no `task=` parameter on `call_tool`, so a SEP-1686 task-augmented `tools/call` cannot be submitted through it. It resolves with the SEP-1686 wire-layer removal above; the SEP-2663 rebuild submits tasks by advertising the extension capability and claiming a `CreateTaskResult`, not through a `task=` params field. The earlier strict xfail for the `ctx.elicit` / `ctx.sample` "Method not found" degradation (sdk-feedback #10) is **gone** — the era-gating shipped in #4448 flipped it to a passing test.
**MCP Apps (`tests/test_apps.py`).** Two xfails tied to **sdk-feedback #2** — the `extensions` capability is stripped by the pre-2026 version sieve, so the UI extension can't be advertised to legacy-era clients.
## Shims and their removal triggers
Every shim in the migration is temporary and carries a documented removal trigger.
| Shim | Location | Removal trigger |
| --- | --- | --- |
| `_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 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
The `2026-07-28` era is stateless by protocol construction, and the recurring maintainer question is whether that statelessness has to be woven through FastMCP everywhere. It does not — but the honest accounting has three parts: features that are legacy-only because the protocol removed the mechanism, features that already work because they never relied on a session, and a short list of design holes where the current code *doesn't error* but also *doesn't work*. Everything below concerns `2026-07-28` connections only. Every client in the field today negotiates a handshake era, where all of this behaves exactly as it always has.
**The SDK ground truth.** On the modern paths the SDK's `Connection` is strictly per-request: a fresh `Connection` is built from each POST's envelope, its `exit_stack` unwinds when the request returns, `connection.session_id` is always `None`, and `connection.state` is a fresh dict per request. The manager's `stateless` flag never enters the picture — modern routing short-circuits ahead of it. There is no standing server→client stream: notifications emitted *during* a request ride that POST's own SSE sink, and anything emitted after the POST returns is dropped (`_NO_CHANNEL`); server→client *requests* raise `NoBackChannelError`. The only replacement is `subscriptions/listen`, which carries four list-changed / resource-updated event kinds and nothing else — no logging, progress, or task-status events, no resumability, and it is not yet wired into FastMCP. There is no `EventStore` or `Last-Event-ID` on modern paths at all; both belong to the legacy transport.
### Legacy-only by construction — document, don't build
These are not bugs. The protocol removed the mechanism they depend on, so they are simply out of scope on `2026-07-28`:
- **Per-session log levels.** `logging/setLevel` is absent from the 2026 method registry, so the `_client_log_levels` handler is unreachable. There is no per-session log-level state because there is no session.
- **`EventStore` / resumability.** `EventStore`, `SessionScopedEventStore`, and Last-Event-ID resumption are never constructed on the modern paths. Resumability presupposes a durable stream, which the era does not have.
- **Ping keepalive.** Server-initiated ping is a server→client request and is therefore structurally a no-op on modern connections; the SDK owns SSE-level pings on this transport.
### Already stateless by construction — works on 2026
These work 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. 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](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 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.
## Upstream advisory dossier
FastMCP acts as an advisor to the SDK team. The migration produced a dossier of ten findings (`sdk-feedback.md`) — verified bugs and hard edges to report upstream, plus questions to bundle into a feedback thread. The highest-priority items:
- **#1 (bug)** — SEP-1686 task result types ship but the method registries omit them. *Moot: the SEP-1686 wire shape was removed from the spec; the SEP-2663 rebuild claims `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. *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](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
The beta-to-stable transition is a small set of tracked steps:
- **Swap the pins.** When `mcp 2.0.0` reaches GA, change `mcp-types==2.0.0b1` (core) and the `mcp` pin (the `[mcp]` extra) in `fastmcp_slim/pyproject.toml` from the beta to the stable release, and cut `4.0.0` instead of another pre-release.
- **Re-run the xfail suite against the GA SDK.** Any strict xfail that starts passing means a gap closed — remove the marker and, where applicable, the corresponding shim.
- **Confirm `release/3.x`** is cut from pre-merge `main` and receiving upstream security patches for users who stay on the SDK v1 line.

View file

@ -0,0 +1,53 @@
---
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)
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.
The protocol layer for this flow — grant parsing, the `exchange_identity_assertion` provider hook, and metadata advertisement — comes from the SDK. The validation and issuance logic that makes the flow actually work is FastMCP's implementation, and enabling it is one parameter on the existing auth providers:
```python
from fastmcp import FastMCP
from fastmcp.server.auth import OAuthProxy, IdentityAssertion
auth = OAuthProxy(
..., # existing upstream configuration unchanged
identity_assertion=IdentityAssertion(
trusted_issuers=["https://login.acme-corp.com"],
),
)
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](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.
## Modern-era capability inventory
The complete picture of what a FastMCP v4 server and client provide on the `2026-07-28` era:
| Capability | What FastMCP provides |
| --- | --- |
| **Dual-era serving** | One server answers both `server/discover` (modern, sessionless) and `initialize` (handshake) connections, auto-detected per connection. Any replica behind a plain load balancer can answer a modern request. |
| **Identity assertion (SEP-990)** | Complete server-side implementation, one parameter to enable (above). |
| **Authorization server** | Full AS stack: `OAuthProxy` bridges DCR-expecting MCP clients to non-DCR enterprise IdPs, ~18 built-in providers, consent UI, self-issued JWTs, protected-resource metadata (RFC 9728). |
| **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")` — 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. |
| **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_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](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`.

142
docs/apps/architecture.mdx Normal file
View file

@ -0,0 +1,142 @@
---
title: Architecture
sidebarTitle: Architecture
description: How FastMCP apps work under the hood — from Python to pixels.
icon: sitemap
---
import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.2.0" />
You don't need this page to build apps. It's for when something isn't rendering the way you expect, when UI tool calls aren't reaching your server, or when you're writing [custom HTML apps](/apps/low-level) and need to understand the protocol directly.
## The pipeline
An MCP app moves through five stages from Python to pixels:
```
Python components → JSON tree → structuredContent → Renderer iframe → Host UI
```
You write Prefab components. FastMCP serializes them to a JSON component tree and delivers it as `structuredContent` on the tool result. The host loads the Prefab renderer in a sandboxed iframe, pushes the JSON in, and the renderer paints the UI. If the UI calls server tools, it talks back through the same `postMessage` channel.
The sections below walk each stage.
## Tool registration
When you mark a tool with `app=True` or `@app.ui()`, FastMCP wires up the metadata and renderer resource that the protocol requires.
### The `app=True` flag
`app` on `@mcp.tool` accepts `True`, an `AppConfig`, or a dict. When you pass `True`, FastMCP explicitly marks the tool as a Prefab UI tool and stamps placeholder UI metadata so the provider can synthesize the correct renderer resource later. When you omit `app`, FastMCP only applies this automatically if the tool's return type is a Prefab type (`PrefabApp`, `Component`, or unions containing them).
The tool and renderer are linked through a `resourceUri` field in the metadata. Internally, registration uses the placeholder URI `ui://prefab/renderer.html`; when tools and resources are listed or read, FastMCP rewrites that placeholder to a per-tool URI like `ui://prefab/tool/<hash>/renderer.html` and synthesizes the matching renderer resource on demand.
### FastMCPApp registration
`FastMCPApp` uses the same mechanism but adds two things. First, it tags every tool — both `@app.ui()` entry points and `@app.tool()` backends — with `meta["fastmcp"]["app"]` set to the app's name. That tag lets the server identify which app a tool belongs to when routing UI calls.
Second, it sets `meta["ui"]["visibility"]` to control who can see each tool. Entry points default to `["model"]` (LLM-visible). Backend tools default to `["app"]` (UI-only). Hosts use this to filter the tool list.
## Serialization
When a Prefab tool runs, its return value — a `PrefabApp` or a bare `Component` — becomes a JSON blob the renderer can interpret.
### `PrefabApp.to_json()`
The entry point is `PrefabApp.to_json()`. It walks the component tree and produces a JSON object with three top-level keys: `view` (the component tree), `state` (initial state values), and `_meta` (routing metadata).
FastMCP passes a `tool_resolver` callback to `to_json()`. Whenever the tree contains a `CallTool` action that references a function (not a string), the resolver converts it to a `ResolvedTool` with the function's registered name. For `FastMCPApp` backend tools, that registered name is then wrapped in the deterministic hashed format described below. The resolver also handles `unwrap_result` — a flag telling the renderer to unwrap single-value results from the `{"result": value}` envelope FastMCP uses for schema compliance.
### Hashed backend tool references
FastMCP still tags app tools with `meta["fastmcp"]["app"]`, but backend routing no longer depends on sending the app name through each tool call. During serialization, FastMCP passes a resolver to `PrefabApp.to_json()`. When the tree contains `CallTool(save_contact)`, the resolver turns it into a deterministic hashed name such as `<hash>_save_contact`, where the hash is derived from the app name and backend tool name.
That hashed name rides along inside `structuredContent` all the way to the renderer. When the renderer calls the backend tool, it sends the hashed tool name in the normal MCP `tools/call` request. The server recognizes that format and routes through the app-tool lookup path described below.
### ToolResult assembly
The final tool result has two parts: `content` (a list of `TextContent` blocks for the LLM) and `structuredContent` (the JSON tree for the renderer). By default, Prefab tools send `"[Rendered Prefab UI]"` as the text content — just enough for the LLM to know something was rendered. If you return a `ToolResult` explicitly, you control both halves.
## Tool call routing
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.
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.
### Late-bound tool names
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.
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.
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
`get_tool_by_hash` is defined on the `Provider` base class and overridden by aggregate and wrapped providers. Aggregate providers fan out the lookup across child providers in parallel. Wrapped providers (like `FastMCPProvider`, which wraps a nested `FastMCP` server) delegate to the inner server's hashed lookup. Backend tools are reachable through any depth of composition.
## The renderer
The Prefab renderer is a self-contained JavaScript application that interprets the JSON component tree and renders it as a React UI.
### Renderer resources
FastMCP exposes the renderer through per-tool resources such as `ui://prefab/tool/<hash>/renderer.html`, each with MIME type `text/html;profile=mcp-app`. The HTML is bundled inside the `prefab-ui` Python package; `get_renderer_html()` returns it as a string. The resources are synthesized on demand from each tool's UI metadata, so CSP and permissions can differ per tool even though they use the same Prefab renderer.
The resource also carries CSP metadata (via `get_renderer_csp()`) declaring the CDN domains the renderer needs. Hosts use this to configure the iframe's Content Security Policy.
### `postMessage` communication
The renderer lives in a sandboxed iframe and communicates with the host using `postMessage`. The protocol follows the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) spec:
The host pushes the tool result (with `structuredContent`) into the iframe. The renderer parses the component tree, initializes state, and renders the UI. When the user interacts — submitting a form, clicking a button — and the interaction triggers a `CallTool` action, the renderer sends a `callServerTool` message back to the host via `postMessage`. The host forwards it as a regular MCP `tools/call` request to the server, using the hashed backend name that FastMCP serialized into the action.
The response flows back the same way: server → host → iframe via `postMessage`, and the renderer updates state with the result.
### AppBridge
The `@modelcontextprotocol/ext-apps` JavaScript SDK provides the `App` class (sometimes called AppBridge) that manages the `postMessage` handshake. It handles connection negotiation, tool result delivery, server tool calls, and host context (safe area insets, theme preferences). The Prefab renderer uses it internally; you only touch it directly when building [custom HTML apps](/apps/low-level).
## The dev server
`fastmcp dev apps` simulates the host-side behavior locally without a real MCP client.
### Proxy architecture
Two HTTP servers. Your MCP server runs on port 8000 with the Streamable HTTP transport. The dev UI runs on port 8080 and serves a picker page that lists your app tools.
A reverse proxy at `/mcp` on the dev server forwards requests to your MCP server. This matters because the renderer iframe runs on `localhost:8080` and your MCP server runs on `localhost:8000` — without the proxy, the renderer's `callServerTool` requests would be cross-origin and the browser would block them. The proxy keeps everything same-origin from the iframe's perspective.
### The launch flow
When you select a tool and click launch, the dev UI calls the tool through the proxy, receives the `structuredContent` response, and opens a new tab. That tab loads the tool's renderer resource (via the proxy), creates an AppBridge, and pushes the tool result into the renderer. From here on it matches what a real host provides: the renderer displays the UI, and any `CallTool` actions route back through the proxy to your server.
Auto-reload is on by default, so changes to your server code restart the MCP server automatically. The dev UI keeps running — relaunch the tool to see changes.

View file

@ -0,0 +1,23 @@
from prefab_ui.app import PrefabApp
from prefab_ui.components import Column
from prefab_ui.components.charts import BarChart, ChartSeries
data = [
{"quarter": "Q1", "revenue": 42000, "costs": 28000},
{"quarter": "Q2", "revenue": 51000, "costs": 31000},
{"quarter": "Q3", "revenue": 47000, "costs": 29000},
{"quarter": "Q4", "revenue": 63000, "costs": 35000},
]
with PrefabApp() as app:
with Column(css_class="p-6"):
BarChart(
data=data,
series=[
ChartSeries(data_key="revenue", label="Revenue"),
ChartSeries(data_key="costs", label="Costs"),
],
x_axis="quarter",
show_legend=True,
height=250,
)

View file

@ -0,0 +1,78 @@
from prefab_ui.actions import ShowToast
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
H3,
Badge,
Button,
Column,
DataTable,
DataTableColumn,
Form,
Input,
Row,
Select,
SelectOption,
Separator,
)
contacts = [
{"name": "Arthur Dent", "email": "arthur@earth.com", "category": "Customer"},
{"name": "Ford Prefect", "email": "ford@betelgeuse.org", "category": "Partner"},
{
"name": "Trillian Astra",
"email": "trillian@heartofgold.com",
"category": "Customer",
},
{"name": "Zaphod Beeblebrox", "email": "zaphod@galaxy.gov", "category": "Vendor"},
]
rows = [
{
"name": c["name"],
"email": c["email"],
"category": Badge(
c["category"],
variant="success"
if c["category"] == "Customer"
else "secondary"
if c["category"] == "Partner"
else "outline",
),
}
for c in contacts
]
with PrefabApp() as app:
with Column(gap=4, css_class="p-6"):
DataTable(
columns=[
DataTableColumn(key="name", header="Name", sortable=True),
DataTableColumn(key="email", header="Email"),
DataTableColumn(key="category", header="Category"),
],
rows=rows,
search=True,
)
Separator()
H3("Add Contact")
with Form(
on_submit=ShowToast(
"Contact saved! (preview demo — no backend wired)",
variant="success",
),
):
with Row(gap=4):
Input(name="name", label="Name", placeholder="Full name", required=True)
Input(
name="email",
label="Email",
placeholder="name@example.com",
required=True,
)
with Select(name="category", label="Category"):
SelectOption(value="Customer", label="Customer")
SelectOption(value="Partner", label="Partner")
SelectOption(value="Vendor", label="Vendor")
Button("Save Contact")

View file

@ -0,0 +1,68 @@
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Badge,
Column,
DataTable,
DataTableColumn,
Row,
Separator,
)
from prefab_ui.components.charts import BarChart, ChartSeries
from prefab_ui.components.metric import Metric
monthly = [
{"month": "Jan", "revenue": 48200, "costs": 31000},
{"month": "Feb", "revenue": 52100, "costs": 32500},
{"month": "Mar", "revenue": 61800, "costs": 34200},
{"month": "Apr", "revenue": 58400, "costs": 33800},
]
deals = [
{"account": "Acme Corp", "value": "$84,000", "stage": "Won"},
{"account": "Globex Inc", "value": "$52,000", "stage": "Negotiation"},
{"account": "Initech", "value": "$31,500", "stage": "Proposal"},
{"account": "Wayne Enterprises", "value": "$45,000", "stage": "Lost"},
]
rows = [
{
"account": d["account"],
"value": d["value"],
"stage": Badge(
d["stage"],
variant="success"
if d["stage"] == "Won"
else "destructive"
if d["stage"] == "Lost"
else "secondary",
),
}
for d in deals
]
total = sum(m["revenue"] for m in monthly)
with PrefabApp() as app:
with Column(gap=4, css_class="p-6"):
with Row(gap=6):
Metric(label="Revenue (Q1-Q4)", value=f"${total:,}")
Metric(label="Deals", value=f"{len(deals)}")
BarChart(
data=monthly,
series=[
ChartSeries(data_key="revenue", label="Revenue"),
ChartSeries(data_key="costs", label="Costs"),
],
x_axis="month",
show_legend=True,
height=200,
)
Separator()
DataTable(
columns=[
DataTableColumn(key="account", header="Account", sortable=True),
DataTableColumn(key="value", header="Value", sortable=True),
DataTableColumn(key="stage", header="Stage"),
],
rows=rows,
)

View file

@ -0,0 +1,24 @@
from prefab_ui.app import PrefabApp
from prefab_ui.components import Column, DataTable, DataTableColumn
employees = [
{"name": "Alice Chen", "role": "Staff Engineer", "dept": "Platform"},
{"name": "Bob Martinez", "role": "Lead Designer", "dept": "Design"},
{"name": "Carol Johnson", "role": "Senior Engineer", "dept": "Platform"},
{"name": "David Kim", "role": "Product Manager", "dept": "Product"},
{"name": "Eva Mueller", "role": "Engineer", "dept": "Platform"},
{"name": "Frank Lee", "role": "Data Scientist", "dept": "ML"},
{"name": "Grace Park", "role": "Eng Manager", "dept": "Platform"},
]
with PrefabApp() as app:
with Column(gap=4, css_class="p-6"):
DataTable(
columns=[
DataTableColumn(key="name", header="Name", sortable=True),
DataTableColumn(key="role", header="Role", sortable=True),
DataTableColumn(key="dept", header="Dept", sortable=True),
],
rows=employees,
search=True,
)

View file

@ -0,0 +1,461 @@
"""The Hitchhiker's Guide dashboard from the Prefab welcome page.
Run with:
prefab serve examples/hitchhikers-guide/dashboard.py
prefab export examples/hitchhikers-guide/dashboard.py
"""
from prefab_ui import PrefabApp
from prefab_ui.actions import SetInterval, SetState, ShowToast
from prefab_ui.components import (
Alert,
AlertDescription,
AlertTitle,
Badge,
Button,
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
Carousel,
Checkbox,
Column,
Combobox,
ComboboxOption,
DataTable,
DataTableColumn,
DatePicker,
Dialog,
Grid,
GridItem,
HoverCard,
Loader,
Metric,
Muted,
P,
Progress,
Radio,
RadioGroup,
Ring,
Row,
Separator,
Slider,
Switch,
Text,
Tooltip,
)
from prefab_ui.components.charts import (
BarChart,
ChartSeries,
RadarChart,
Sparkline,
)
from prefab_ui.components.control_flow import Else, If
from prefab_ui.rx import Rx
ctx_tick = Rx("ctx_tick")
# Context window: climbs from 24% to ~78%, then resets
ctx_pct = (ctx_tick % 20) * 3 + 20
ctx_variant = (ctx_pct > 70).then(
"destructive", (ctx_pct <= 33).then("success", "default")
)
with PrefabApp(
title="Prefab Showcase",
state={"ctx_tick": 0, "improbability": 42},
on_mount=SetInterval(
400,
on_tick=SetState("ctx_tick", ctx_tick + 1),
),
) as app:
with Grid(columns={"default": 1, "md": 2, "lg": 4}, gap=4):
# ── Col 1 ─────────────────────────────────────────────────────────
with Column(gap=4):
with Card():
with CardHeader():
CardTitle("Register Towel")
CardDescription("The most important item in the galaxy")
with CardContent():
with Column(gap=3):
with Combobox(
placeholder="Type...",
search_placeholder="Search types...",
):
ComboboxOption("Bath", value="bath")
ComboboxOption("Beach", value="beach")
ComboboxOption("Interstellar", value="interstellar")
ComboboxOption("Microfiber", value="micro")
DatePicker(placeholder="Registration date")
with CardFooter():
with Row(gap=2):
with Dialog(
title="Towel Registered!",
description="Your towel has been added to the galactic registry.",
):
Button("Register")
Text("Don't forget to bring it.")
Button("Cancel", variant="outline")
with If("{{ !pressed }}"):
Button(
"This is probably the best button to press.",
variant="success",
on_click=SetState("pressed", True),
)
with Else():
Button(
"Please do not press this button again.",
variant="destructive",
on_click=SetState("pressed", False),
)
with Card():
with CardHeader():
CardTitle("Ship Status")
with CardContent():
with Column(gap=3):
with Row(
align="center",
css_class="justify-between",
):
Text("heart-of-gold")
with HoverCard(open_delay=0, close_delay=200):
Badge("In Orbit", variant="default")
with Column(gap=2):
Text("heart-of-gold")
Muted("Deployed 2h ago")
Progress(
value=100,
max=100,
variant="success",
)
Progress(
value=100,
max=100,
indicator_class="bg-yellow-400",
)
with Row(
align="center",
css_class="justify-between",
):
Text("vogon-poetry")
with Tooltip("64% — ETA 12 min", delay=0):
with Badge(variant="secondary"):
Loader(size="sm")
Text("Deploying")
Progress(value=64, max=100)
with Row(
align="center",
css_class="justify-between",
):
Text("deep-thought")
with Tooltip(
"Computing... 7.5 million years remaining",
delay=0,
):
with Badge(variant="outline"):
Loader(size="sm", variant="ios")
Text("Soon...")
Progress(value=12, max=100)
with Card():
with CardHeader():
CardTitle("Planet Ratings")
with CardContent():
RadarChart(
data=[
{"axis": "Views", "earth": 30, "mag": 95},
{"axis": "Fjords", "earth": 65, "mag": 100},
{"axis": "Pubs", "earth": 90, "mag": 10},
{"axis": "Mice", "earth": 40, "mag": 85},
{"axis": "Tea", "earth": 95, "mag": 15},
{"axis": "Safety", "earth": 45, "mag": 70},
],
series=[
ChartSeries(dataKey="earth", label="Earth"),
ChartSeries(dataKey="mag", label="Magrathea"),
],
axis_key="axis",
height=200,
show_legend=True,
show_tooltip=True,
)
# ── Col 2 ─────────────────────────────────────────────────────────
with Column(gap=4):
with Card():
with CardHeader():
CardTitle("Survival Odds")
with CardContent(css_class="w-fit mx-auto"):
Ring(
value=42,
label="42%",
variant="info",
size="lg",
thickness=12,
indicator_class="group-hover:drop-shadow-[0_0_24px_rgba(59,130,246,0.9)]",
)
with Card():
with CardHeader():
with Row(gap=2, align="center"):
CardTitle("Improbability Drive")
Loader(
variant="pulse",
size="sm",
css_class="text-blue-500",
)
with CardContent():
with Column(gap=2):
Slider(
min=0,
max=100,
value=42,
name="improbability",
)
with Row(
align="center",
css_class="justify-between",
):
Muted("Probable")
Muted("Infinite")
with Carousel(auto_advance=3000, show_controls=False, direction="up"):
with Alert(variant="success", icon="circle-check"):
AlertTitle("Don't Panic")
AlertDescription("Normality achieved.")
with Alert(variant="destructive", icon="triangle-alert"):
AlertTitle("Display Department")
AlertDescription("Beware of the leopard.")
with Card():
with CardHeader():
CardTitle("Prefect Horizon Config")
with CardContent():
with Column(gap=3):
Switch(
label="Auto-scale agents",
value=True,
name="autoscale",
)
Separator()
Switch(
label="Code Mode",
value=True,
name="code_mode",
)
Separator()
Switch(
label="Tool call caching",
value=False,
name="cache",
)
with CardFooter():
Button(
"Save Preferences",
on_click=ShowToast("Preferences saved!"),
)
with Card():
with CardHeader():
CardTitle("Travel Class")
with CardContent():
with RadioGroup(name="travel_class"):
Radio(option="economy", label="Economy")
Radio(option="business", label="Business Class")
Radio(
option="improbability",
label="Infinite Improbability",
value=True,
)
# ── Cols 34: summary row, chart, then 2-col grid below ─────────
with GridItem(css_class="md:col-span-2"):
with Column(gap=4):
with Grid(columns=2, gap=4, css_class="h-32"):
with Card():
with CardHeader():
CardTitle("Context Window")
with CardContent():
with Column(
gap=6,
justify="center",
css_class="h-full",
):
with Row(
align="center",
css_class="justify-between",
):
Text(f"{ctx_pct}% used")
Muted(f"{ctx_pct * 2}k / 200k tokens")
with Tooltip(
"Auto-compact buffer: 12%",
delay=0,
):
Progress(
value=ctx_pct,
max=100,
variant=ctx_variant,
)
with Card(css_class="pb-0 gap-0"):
with CardContent():
Metric(
label="Fjords designed",
value="1,847",
delta="+3 coastlines",
)
Sparkline(
data=[
820,
950,
1100,
980,
1250,
1400,
1350,
1500,
1680,
1847,
],
variant="success",
fill=True,
css_class="h-16",
)
with Card():
with CardHeader():
CardTitle("Towel Incidents")
with CardContent():
BarChart(
data=[
{"month": "Jan", "lost": 8, "found": 5},
{"month": "Feb", "lost": 24, "found": 15},
{"month": "Mar", "lost": 12, "found": 28},
{"month": "Apr", "lost": 35, "found": 19},
{"month": "May", "lost": 18, "found": 38},
{"month": "Jun", "lost": 42, "found": 30},
],
series=[
ChartSeries(dataKey="lost", label="Lost"),
ChartSeries(dataKey="found", label="Found"),
],
x_axis="month",
height=200,
bar_radius=4,
show_legend=True,
show_tooltip=True,
show_grid=True,
)
with Grid(columns=2, gap=4):
with Column(gap=4):
with Card():
with CardContent():
with Column(gap=2):
Checkbox(label="Towel packed", value=True)
Checkbox(label="Guide charged", value=True)
Checkbox(
label="Babel fish inserted",
value=False,
)
with Card():
with CardHeader():
CardTitle("Marvin's Mood")
with CardContent():
with Column(gap=3):
P("How's life?")
with Column(gap=2):
Button(
"Meh",
on_click=ShowToast(
"Noted. Enthusiasm levels nominal."
),
)
Button(
"Depressed",
variant="info",
on_click=ShowToast(
"I think you ought to "
"know I'm feeling very "
"depressed."
),
)
Button(
"Don't talk to me about life",
variant="warning",
on_click=ShowToast(
"Brain the size of a "
"planet and they ask me "
"to pick up a piece of "
"paper."
),
)
with Column(gap=4):
with Card():
with CardContent():
with Row(gap=2, align="center"):
Loader(variant="dots", size="sm")
Muted("Marvin is thinking...")
with Card():
with CardContent():
DataTable(
columns=[
DataTableColumn(
key="crew",
header="Crew",
sortable=True,
),
DataTableColumn(
key="species",
header="Species",
sortable=True,
),
DataTableColumn(
key="towel",
header="Towel?",
sortable=True,
),
DataTableColumn(
key="status",
header="Status",
sortable=True,
),
],
rows=[
{
"crew": "Arthur Dent",
"species": "Human",
"towel": "Yes",
"status": "Confused",
},
{
"crew": "Ford Prefect",
"species": "Betelgeusian",
"towel": "Always",
"status": "Drinking",
},
{
"crew": "Zaphod",
"species": "Betelgeusian",
"towel": "Lost it",
"status": "Presidential",
},
{
"crew": "Trillian",
"species": "Human",
"towel": "Yes",
"status": "Navigating",
},
{
"crew": "Marvin",
"species": "Android",
"towel": "No point",
"status": "Depressed",
},
{
"crew": "Slartibartfast",
"species": "Magrathean",
"towel": "Somewhere",
"status": "Designing",
},
],
search=True,
paginated=False,
)

View file

@ -0,0 +1,21 @@
from prefab_ui.app import PrefabApp
from prefab_ui.components import Column
from prefab_ui.components.charts import PieChart
data = [
{"category": "Bug", "count": 42},
{"category": "Feature", "count": 28},
{"category": "Docs", "count": 15},
{"category": "Infra", "count": 10},
]
with PrefabApp() as app:
with Column(css_class="p-6"):
PieChart(
data=data,
data_key="count",
name_key="category",
inner_radius=50,
show_legend=True,
height=240,
)

View file

@ -0,0 +1,66 @@
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Column,
Row,
Select,
SelectOption,
Switch,
Text,
)
from prefab_ui.components.charts import BarChart, ChartSeries
from prefab_ui.components.control_flow import If
from prefab_ui.components.metric import Metric
from prefab_ui.rx import Rx
region = Rx("region")
north = [
{"month": "Jan", "sales": 22000},
{"month": "Feb", "sales": 25500},
{"month": "Mar", "sales": 24200},
]
south = [
{"month": "Jan", "sales": 5800},
{"month": "Feb", "sales": 6400},
{"month": "Mar", "sales": 5600},
]
west = [
{"month": "Jan", "sales": 6000},
{"month": "Feb", "sales": 6000},
{"month": "Mar", "sales": 5600},
]
with PrefabApp(
state={
"region": "north",
"north": north,
"south": south,
"west": west,
"show_target": True,
},
) as app:
with Column(
gap=4,
css_class="p-6",
let={
"data": "{{ region == 'south' ? south : region == 'west' ? west : north }}",
},
):
with Row(gap=4, align="center"):
with Select(name="region", css_class="w-40"):
SelectOption(value="north", label="North")
SelectOption(value="south", label="South")
SelectOption(value="west", label="West")
Switch(name="show_target", css_class="ml-auto")
Text("Show target", css_class="text-sm text-muted-foreground")
BarChart(
data=Rx("data"),
series=[ChartSeries(data_key="sales", label="Sales")],
x_axis="month",
height=200,
)
with If(Rx("show_target")):
Metric(
label="Q1 Target",
value="$75,000",
)

View file

@ -0,0 +1,116 @@
from collections import Counter
from prefab_ui.actions import SetState
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
H3,
Badge,
Card,
CardContent,
CardHeader,
Column,
DataTable,
DataTableColumn,
Grid,
Row,
Small,
Text,
)
from prefab_ui.components.charts import PieChart
from prefab_ui.components.control_flow import If
from prefab_ui.rx import STATE, Rx
MEMBERS = [
{
"name": "Alice Chen",
"role": "Staff Engineer",
"office": "San Francisco",
"email": "alice@company.com",
"projects": 3,
},
{
"name": "Bob Martinez",
"role": "Lead Designer",
"office": "New York",
"email": "bob@company.com",
"projects": 5,
},
{
"name": "Carol Johnson",
"role": "Senior Engineer",
"office": "London",
"email": "carol@company.com",
"projects": 2,
},
{
"name": "David Kim",
"role": "Product Manager",
"office": "San Francisco",
"email": "david@company.com",
"projects": 7,
},
{
"name": "Eva Mueller",
"role": "Engineer",
"office": "Berlin",
"email": "eva@company.com",
"projects": 1,
},
{
"name": "Frank Lee",
"role": "Data Scientist",
"office": "San Francisco",
"email": "frank@company.com",
"projects": 4,
},
{
"name": "Grace Park",
"role": "Engineering Manager",
"office": "New York",
"email": "grace@company.com",
"projects": 6,
},
]
OFFICE_COUNTS = [
{"office": office, "count": count}
for office, count in Counter(m["office"] for m in MEMBERS).items()
]
with PrefabApp(state={"selected": None}) as app:
with Column(gap=4, css_class="p-6"):
with Grid(columns=[1, 2], gap=4):
PieChart(
data=OFFICE_COUNTS,
data_key="count",
name_key="office",
show_legend=True,
)
DataTable(
columns=[
DataTableColumn(key="name", header="Name", sortable=True),
DataTableColumn(key="role", header="Role", sortable=True),
DataTableColumn(key="office", header="Office", sortable=True),
],
rows=MEMBERS,
search=True,
on_row_click=SetState("selected", Rx("$event")),
)
with If(STATE.selected):
with Card():
with CardHeader():
with Row(gap=2, align="center"):
H3(Rx("selected.name"))
Badge(Rx("selected.office"))
with CardContent():
with Grid(columns=3, gap=4):
with Column(gap=0):
Small("Role")
Text(Rx("selected.role"))
with Column(gap=0):
Small("Email")
Text(Rx("selected.email"))
with Column(gap=0):
Small("Active Projects")
Text(Rx("selected.projects"))

View file

@ -0,0 +1,39 @@
from collections import Counter
from prefab_ui.app import PrefabApp
from prefab_ui.components import Column, DataTable, DataTableColumn, Grid
from prefab_ui.components.charts import PieChart
members = [
{"name": "Alice Chen", "role": "Staff Engineer", "office": "San Francisco"},
{"name": "Bob Martinez", "role": "Lead Designer", "office": "New York"},
{"name": "Carol Johnson", "role": "Senior Engineer", "office": "London"},
{"name": "David Kim", "role": "Product Manager", "office": "San Francisco"},
{"name": "Eva Mueller", "role": "Engineer", "office": "Berlin"},
{"name": "Frank Lee", "role": "Data Scientist", "office": "San Francisco"},
{"name": "Grace Park", "role": "Engineering Manager", "office": "New York"},
]
office_counts = [
{"office": office, "count": count}
for office, count in Counter(m["office"] for m in members).items()
]
with PrefabApp() as app:
with Column(gap=4, css_class="p-6"):
with Grid(columns=[1, 2], gap=4):
PieChart(
data=office_counts,
data_key="count",
name_key="office",
show_legend=True,
)
DataTable(
columns=[
DataTableColumn(key="name", header="Name", sortable=True),
DataTableColumn(key="role", header="Role", sortable=True),
DataTableColumn(key="office", header="Office", sortable=True),
],
rows=members,
search=True,
)

67
docs/apps/development.mdx Normal file
View file

@ -0,0 +1,67 @@
---
title: Development
sidebarTitle: Development
description: Preview and test your app tools locally without a full MCP host.
icon: flask
---
import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.2.0" />
<Frame>
<img src="/apps/images/dev-app.png" alt="The dev UI showing a rendered Prefab app with the MCP inspector panel" />
</Frame>
`fastmcp dev apps` gives you a browser preview for your app tools without needing an MCP host client. It starts your server and a local dev UI side by side: you pick a tool, fill in its arguments, and the rendered result opens in a new tab.
Works with both [Interactive Tools](/apps/prefab) and [custom HTML apps](/apps/low-level).
## Quick start
```bash
fastmcp dev apps server.py
```
The dev UI opens at `http://localhost:8080`. Your MCP server runs on port 8000 with auto-reload enabled by default — save a file and the server restarts automatically.
## How it works
The dev server does three things:
The **picker page** connects to your MCP server, finds all tools with UI metadata, and renders a form for each one. The forms are auto-generated from the tool's input schema — text fields, dropdowns, checkboxes, all wired up.
When you submit a form, the dev server **calls your tool** via the MCP protocol and opens the result in a new tab. The result page loads the tool's UI resource (the Prefab renderer or your custom HTML) inside an AppBridge — the same protocol that real MCP hosts use.
A **reverse proxy** on `/mcp` forwards requests from the browser to your MCP server, avoiding CORS issues that would otherwise block the iframe-based renderer from talking to a different port.
## MCP inspector
The dev UI includes an inspector panel on the left side that captures MCP traffic in real time. It shows JSON-RPC messages flowing between the browser and your server — requests, responses, and AppBridge `postMessage` traffic.
Each entry shows direction, method, timing, and a smart summary. Click any entry to expand the full JSON-RPC body. The panel auto-scrolls to new messages unless you've scrolled up to inspect older ones.
The inspector is useful for debugging: you can see exactly what arguments your tool received, what it returned, and how the AppBridge communicated with the renderer.
## Options
```bash
fastmcp dev apps server.py:mcp --mcp-port 9000 --dev-port 9090 --no-reload
```
| Option | Flag | Default | Description |
| ------ | ---- | ------- | ----------- |
| MCP Port | `--mcp-port` | `8000` | Port for your MCP server |
| Dev Port | `--dev-port` | `8080` | Port for the dev UI |
| Auto-Reload | `--reload` / `--no-reload` | On | Watch files and restart the server on changes |
| Host | `--host` | `127.0.0.1` | Interface for both local servers to bind |
| Log Panel | `--log-panel` / `--no-log-panel` | On | Show or hide the log panel in the dev UI |
## Multiple tools
If your server has multiple app tools, the picker shows a dropdown. Each tool gets its own form and launch button. The tool's `title` is displayed when available, falling back to the tool name.
```bash
# Server with multiple app tools
fastmcp dev apps examples/apps/contacts/contacts_server.py
```

92
docs/apps/examples.mdx Normal file
View file

@ -0,0 +1,92 @@
---
title: Examples
sidebarTitle: Examples
description: Example apps you can run right now.
icon: images
---
import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.2.0" />
Each tile below is a working FastMCP server you can run with `fastmcp dev apps` or connect to from any MCP host. Source lives in `examples/apps/` in the repository.
<Columns cols={2}>
<Tile href="#sales-dashboard" title="Sales Dashboard" description="Metrics, charts, and deal pipeline">
<div style={{overflow: "hidden", width: "100%"}}>
<img src="/apps/images/app-example-sales-dashboard.png" />
</div>
</Tile>
<Tile href="#system-monitor" title="System Monitor" description="Live CPU, memory, disk with auto-refresh">
<img src="/apps/images/app-example-system-dashboard.png" />
</Tile>
<Tile href="#quiz" title="Quiz" description="LLM-generated trivia with scoring">
<img src="/apps/images/app-example-quiz.png" />
</Tile>
<Tile href="#interactive-map" title="Interactive Map" description="Geocoded addresses on Leaflet">
<img src="/apps/images/app-example-map.png" />
</Tile>
<Tile href="/apps/providers/file-upload" title="File Upload" description="Drag-and-drop upload provider">
<img src="/apps/images/app-file-upload.png" />
</Tile>
<Tile href="/apps/providers/approval" title="Approval" description="Human-in-the-loop confirmation">
<img src="/apps/images/app-approval.png" />
</Tile>
<Tile href="/apps/providers/choice" title="Choice" description="Clickable option selection">
<img src="/apps/images/app-choice.png" />
</Tile>
<Tile href="/apps/providers/form" title="Form Input" description="Pydantic model forms">
<img src="/apps/images/app-form.png" />
</Tile>
<Tile href="/apps/generative" title="Generative UI" description="LLM writes the UI at runtime">
<img src="/apps/images/app-showcase.png" />
</Tile>
</Columns>
## Running the examples
Preview any example in your browser with the dev server:
```bash
pip install "fastmcp[apps]"
fastmcp dev apps examples/apps/sales_dashboard/sales_dashboard_server.py
```
The dev UI lets you pick a tool and fill in arguments. In a real deployment the LLM provides those arguments from conversation context — the quiz example especially shines when connected to a host like Goose or Claude Desktop, where the LLM generates the questions itself.
## Standalone apps
### Sales dashboard
A full dashboard with KPI metrics, revenue trends, segment breakdown, and a deal pipeline table. Shows what you can build with a single `app=True` tool and Prefab's chart and data components.
```bash
fastmcp dev apps examples/apps/sales_dashboard/sales_dashboard_server.py
```
### System monitor
Reads live CPU, memory, and disk stats from your machine using `psutil`. Auto-refreshes via `SetInterval` calling a backend tool, with a dropdown to control the refresh rate. The chart accumulates up to 100 data points over time.
```bash
pip install psutil
fastmcp dev apps examples/apps/system_monitor/system_monitor_server.py
```
### Quiz
The LLM generates trivia questions and passes them to the tool. The user answers via buttons, sees correct/incorrect feedback, and tracks score across questions. Demonstrates multi-turn client-side state with FastMCPApp.
```bash
fastmcp dev apps examples/apps/quiz/quiz_server.py
```
### Interactive map
Accepts addresses or place names, geocodes them via OpenStreetMap Nominatim (free, no API key), and renders an interactive Leaflet map using Prefab's `Embed` component with inline HTML. A reminder that Prefab apps can break out of built-in components when they need to.
```bash
fastmcp dev apps examples/apps/map/map_server.py
```
For ready-made building blocks like approvals, choice pickers, file uploads, and Pydantic forms, see the [Providers](/apps/providers/approval) group.

474
docs/apps/fastmcp-app.mdx Normal file
View file

@ -0,0 +1,474 @@
---
title: FastMCPApp
sidebarTitle: FastMCPApp
description: Wire an interactive UI to backend tools with managed visibility and composition safety.
icon: puzzle-piece
tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
import PrefabPinWarning from '/snippets/prefab-pin-warning.mdx'
import { PrefabDemoFrame } from '/snippets/prefab-demo-frame.mdx'
<VersionBadge version="3.2.0" />
<PrefabPinWarning />
<PrefabDemoFrame demo="contacts" height="650px" title="Contacts app demo" />
Search a list, fill out a form, click save, the list updates. That pattern — UI that reads and writes data on the server — needs two things: backend tools that actually do the work, and a way to call them from the UI. `FastMCPApp` handles the wiring.
You'll build up to the contacts app above by the end of this page. Let's start with something smaller.
## A minimal interactive app
The smallest interactive app: a form that saves a note, and a list that updates when the user submits.
```python
from prefab_ui.actions import SetState, ShowToast
from prefab_ui.actions.mcp import CallTool
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Badge, Button, Column, ForEach, Form, Heading,
Input, Row, Separator, Text,
)
from prefab_ui.rx import RESULT
from fastmcp import FastMCP, FastMCPApp
app = FastMCPApp("Notes")
notes_db: list[dict] = []
@app.tool()
def add_note(title: str, body: str) -> list[dict]:
"""Save a note and return all notes."""
notes_db.append({"title": title, "body": body})
return list(notes_db)
@app.ui()
def notes_app() -> PrefabApp:
"""Open the notes app."""
with Column(gap=6, css_class="p-6") as view:
Heading("Notes")
with ForEach("notes") as note:
with Row(gap=2, align="center"):
Text(note.title, css_class="font-semibold")
Badge(note.body)
Separator()
with Form(
on_submit=CallTool(
"add_note",
on_success=[
SetState("notes", RESULT),
ShowToast("Note saved!", variant="success"),
],
on_error=ShowToast("Failed to save", variant="error"),
)
):
Input(name="title", label="Title", required=True)
Input(name="body", label="Body", required=True)
Button("Add Note")
return PrefabApp(view=view, state={"notes": list(notes_db)})
mcp = FastMCP("Notes Server", providers=[app])
```
The model sees one tool: `notes_app`. Calling it opens the UI. When the user submits the form, `CallTool("add_note")` fires, the server saves the note, returns the updated list, and `SetState("notes", RESULT)` writes that list back into state. `ForEach("notes")` re-renders. The model never sees `add_note` — it's UI-only.
## Why not just `@mcp.tool(app=True)`?
A fair question. Any [Interactive Tool](/apps/prefab) can call a server tool — there's nothing stopping you from putting `CallTool("add_note")` inside a regular `@mcp.tool(app=True)`. It works for one or two tools. Things get harder once the app grows:
- Which tools should the model see, and which are UI-only?
- 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, 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.
## `@app.ui()` — entry points
Entry points are what the model sees. They return a `PrefabApp` and default to `visibility=["model"]`, showing up in the LLM tool list but not callable from within the UI.
```python
@app.ui()
def dashboard() -> PrefabApp:
"""The model calls this to open the dashboard."""
with Column(gap=4, css_class="p-6") as view:
Heading("Dashboard")
...
return PrefabApp(view=view)
```
`@app.ui()` supports the same options as `@mcp.tool`: `name`, `description`, `title`, `tags`, `icons`, `auth`, and `timeout`.
## `@app.tool()` — backend tools
Backend tools do the work. By default they're visible only to the UI (`visibility=["app"]`), not the model.
```python
@app.tool()
def save_contact(name: str, email: str) -> list[dict]:
"""Save a contact and return the updated list."""
db.append({"name": name, "email": email})
return list(db)
```
If you want a tool callable by both the model and the UI, pass `model=True`:
```python
@app.tool(model=True)
def list_contacts() -> list[dict]:
"""Both the model and the UI can call this."""
return list(db)
```
Backend tools support `name`, `description`, `auth`, and `timeout`.
## `CallTool` — UI → backend
`CallTool` is how the UI invokes a backend tool. Pass the tool's name (or a direct function reference):
```python
from prefab_ui.actions.mcp import CallTool
CallTool("save_contact", arguments={"name": "Alice", "email": "alice@example.com"})
# Or a function reference — resolves to a stable global key
CallTool(save_contact, arguments={...})
```
Arguments can reference state with `Rx`:
```python
from prefab_ui.rx import STATE
CallTool("search", arguments={"query": STATE.search_term})
```
### Handling results
Server calls are async. Use `on_success` and `on_error` callbacks:
```python
from prefab_ui.actions import SetState, ShowToast
from prefab_ui.rx import RESULT
CallTool(
"save_contact",
on_success=[
SetState("contacts", RESULT),
ShowToast("Saved!", variant="success"),
],
on_error=ShowToast("Something went wrong", variant="error"),
)
```
`RESULT` is a reactive reference to the tool's return value, available inside `on_success`. `ERROR` (from `prefab_ui.rx`) is the counterpart inside `on_error`. Callbacks can be a single action or a list; they execute in order and short-circuit on error.
### `result_key` shorthand
When a tool's return value should replace a state key, use `result_key`:
```python
CallTool("list_contacts", result_key="contacts")
# same as:
CallTool("list_contacts", on_success=SetState("contacts", RESULT))
```
## Actions
`CallTool` is one of several actions. Actions attach to handlers like `on_click`, `on_submit`, and `on_change`.
Client-side actions run instantly in the browser, no server round-trip:
```python
from prefab_ui.actions import SetState, ToggleState, AppendState, PopState, ShowToast
SetState("count", 42)
ToggleState("expanded")
AppendState("items", {"name": "New Item"})
PopState("items", 0)
ShowToast("Done!", variant="success")
```
Pass a list to chain actions:
```python
Button(
"Reset",
on_click=[
SetState("query", ""),
SetState("results", []),
ShowToast("Cleared"),
],
)
```
### Loading states
A common pattern: disable a button and show a spinner while a call is in flight.
```python
from prefab_ui.rx import Rx
saving = Rx("saving")
Button(
saving.then("Saving...", "Save"),
disabled=saving,
on_click=[
SetState("saving", True),
CallTool(
"save_data",
on_success=[
SetState("saving", False),
SetState("result", RESULT),
ShowToast("Saved!", variant="success"),
],
on_error=[
SetState("saving", False),
ShowToast("Failed", variant="error"),
],
),
],
)
# PrefabApp(view=view, state={"saving": False, ...})
```
## Forms
Forms collect input and submit it to a tool. When submitted, named input values become the tool's arguments.
### Manual forms
```python
from prefab_ui.components import Form, Input, Select, SelectOption, Textarea, Button
with Form(
on_submit=CallTool(
"create_ticket",
on_success=ShowToast("Ticket created!", variant="success"),
)
):
Input(name="title", label="Title", required=True)
with Select(name="priority", label="Priority"):
SelectOption("Low", value="low")
SelectOption("Medium", value="medium")
SelectOption("High", value="high")
Textarea(name="description", label="Description")
Button("Create Ticket")
```
On submit, `CallTool` receives `{"title": ..., "priority": ..., "description": ...}`.
### Forms from Pydantic models
For structured input, `Form.from_model()` generates the whole form — inputs, labels, validation:
```python
from typing import Literal
from pydantic import BaseModel, Field
class BugReport(BaseModel):
title: str = Field(title="Bug Title")
severity: Literal["low", "medium", "high", "critical"] = Field(
title="Severity", default="medium"
)
description: str = Field(title="Description")
@app.ui()
def report_bug() -> PrefabApp:
with Column(gap=4, css_class="p-6") as view:
Heading("Report a Bug")
Form.from_model(
BugReport,
on_submit=CallTool(
"create_bug",
on_success=ShowToast("Bug filed!", variant="success"),
),
)
return PrefabApp(view=view)
@app.tool()
def create_bug(data: BugReport) -> str:
return f"Created: {data.title}"
```
`str` becomes a text input, `Literal` becomes a select, `bool` becomes a checkbox. Field titles and defaults are respected.
## Composition and namespacing
The reason `FastMCPApp` exists — and why you'd pick it over plain `@mcp.tool(app=True)` with string-based `CallTool` — is composition safety.
When you mount a server under a namespace, tool names get prefixed:
```python
platform = FastMCP("Platform")
platform.mount("contacts", contacts_server)
# "save_contact" becomes "contacts_save_contact"
```
`CallTool("save_contact")` would now be broken. But `CallTool(save_contact)` with a function reference resolves to a globally stable identifier that bypasses the namespace. Your app works the same whether standalone or mounted.
### Mounting
`FastMCPApp` is a Provider. Add it to a server with `providers=` or `add_provider`:
```python
mcp = FastMCP("Platform", providers=[app])
# or
mcp = FastMCP("Platform")
mcp.add_provider(app)
```
Multiple apps can coexist; each gets its own global keys, so there's no collision even if two apps have a tool named `save`.
```python
mcp = FastMCP("Platform", providers=[contacts_app, inventory_app, billing_app])
```
### Running standalone
For development, `FastMCPApp` has a `run()` shortcut that wraps itself in a temporary `FastMCP` server:
```python
app = FastMCPApp("Contacts")
# ... register tools ...
if __name__ == "__main__":
app.run()
```
## A full example: contact manager
This brings everything together — entry point, backend tools, Pydantic form, manual form, state, actions, and multi-visibility.
```python expandable
from __future__ import annotations
from typing import Literal
from prefab_ui.actions import SetState, ShowToast
from prefab_ui.actions.mcp import CallTool
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Badge, Button, Column, ForEach, Form,
Heading, Input, Muted, Row, Separator, Text,
)
from prefab_ui.rx import RESULT, Rx
from pydantic import BaseModel, Field
from fastmcp import FastMCP, FastMCPApp
contacts_db: list[dict] = [
{"name": "Arthur Dent", "email": "arthur@earth.com", "category": "Customer"},
{"name": "Ford Prefect", "email": "ford@betelgeuse.org", "category": "Partner"},
]
class ContactModel(BaseModel):
name: str = Field(title="Full Name", min_length=1)
email: str = Field(title="Email")
category: Literal["Customer", "Vendor", "Partner", "Other"] = "Other"
app = FastMCPApp("Contacts")
@app.tool()
def save_contact(data: ContactModel) -> list[dict]:
"""Save a new contact and return the updated list."""
contacts_db.append(data.model_dump())
return list(contacts_db)
@app.tool()
def search_contacts(query: str) -> list[dict]:
"""Filter contacts by name or email."""
q = query.lower()
return [
c for c in contacts_db
if q in c["name"].lower() or q in c["email"].lower()
]
@app.tool(model=True)
def list_contacts() -> list[dict]:
"""Return all contacts. Visible to both the model and the UI."""
return list(contacts_db)
@app.ui()
def contact_manager() -> PrefabApp:
"""Open the contact manager."""
with Column(gap=6, css_class="p-6") as view:
Heading("Contacts")
with ForEach("contacts") as contact:
with Row(gap=2, align="center"):
Text(contact.name, css_class="font-medium")
Muted(contact.email)
Badge(contact.category)
Separator()
Heading("Add Contact", level=3)
Form.from_model(
ContactModel,
on_submit=CallTool(
"save_contact",
on_success=[
SetState("contacts", RESULT),
ShowToast("Contact saved!", variant="success"),
],
on_error=ShowToast("Failed to save", variant="error"),
),
)
Separator()
Heading("Search", level=3)
with Form(
on_submit=CallTool(
"search_contacts",
arguments={"query": Rx("query")},
on_success=SetState("contacts", RESULT),
)
):
Input(name="query", placeholder="Search by name or email...")
Button("Search")
return PrefabApp(view=view, state={"contacts": list(contacts_db)})
mcp = FastMCP("Contacts Server", providers=[app])
if __name__ == "__main__":
mcp.run()
```
Also available as a runnable server at `examples/apps/contacts/contacts_server.py`.
## Next steps
- **[Interactive Tools](/apps/prefab)** — the building blocks: charts, tables, dashboards, reactive state
- **[Examples](/apps/examples)** — complete working servers
- **[Development](/apps/development)** — preview and test app tools locally
- **[Prefab UI docs](https://prefab.prefect.io)** — full component reference

134
docs/apps/generative.mdx Normal file
View file

@ -0,0 +1,134 @@
---
title: Generative UI
sidebarTitle: Generative UI
description: Let the LLM build custom Prefab UIs on the fly.
icon: wand-magic-sparkles
tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
<VersionBadge version="3.2.0" />
<video src="/apps/images/generative-ui.mp4" autoPlay loop muted playsInline style={{width:"100%", borderRadius:"8px", marginBottom:"1rem"}} />
With Generative UI, the LLM writes the UI code at runtime. Instead of calling a pre-built tool with a fixed shape, the model writes Prefab Python tailored to the current data and request. The user watches the UI stream in as the model generates it.
```python
from fastmcp import FastMCP
from fastmcp.apps.generative import GenerativeUI
mcp = FastMCP("Prefab Studio")
mcp.add_provider(GenerativeUI())
```
One provider registers three things:
- **`generate_prefab_ui`** — a tool that accepts Python code, executes it in a Pyodide sandbox, and renders the result as a Prefab app
- **`search_prefab_components`** — a tool the LLM uses to discover what components are available
- **The streaming renderer** — a `ui://` resource with browser-side Pyodide that progressively renders partial code as the LLM generates it
## How it works
When the LLM calls `generate_prefab_ui`, it writes Prefab Python code into the `code` argument. The MCP Apps protocol creates the renderer iframe in parallel with the tool call, so the app is already running by the time partial arguments start flowing.
As the LLM generates each token:
1. The host forwards partial arguments to the app via `ontoolinputpartial`
2. The renderer extracts the growing `code` string
3. Browser-side Pyodide executes whatever compiles successfully
4. The user sees components appear as they're written
When the LLM finishes, the server runs the complete code in a server-side Pyodide sandbox for validation, and the renderer swaps the streaming preview for the final server-validated result.
## What the LLM writes
The tool description includes examples that teach the model the Prefab patterns. A typical generation looks like:
```python
from prefab_ui.components import Column, Row, Heading, Text, Badge, Card, CardContent
from prefab_ui.components.charts import BarChart, ChartSeries
from prefab_ui.app import PrefabApp
with PrefabApp() as app:
with Column(gap=6, css_class="p-6"):
Heading("Q3 Revenue Report")
BarChart(
data=[
{"month": "Jul", "revenue": 42000},
{"month": "Aug", "revenue": 51000},
{"month": "Sep", "revenue": 63000},
],
series=[ChartSeries(data_key="revenue", label="Revenue")],
x_axis="month",
)
with Row(gap=4):
with Card():
with CardContent():
Text("Total", css_class="text-sm text-muted-foreground")
Heading("$156,000")
with Card():
with CardContent():
Text("Growth", css_class="text-sm text-muted-foreground")
Badge("+18%", variant="success")
```
The model writes real Python — loops, f-strings, computation, helper functions. Prefab gives it charts, tables, forms, cards, badges, and layout primitives to compose.
## The component search tool
Before writing code, the LLM can call `search_prefab_components` to discover what's available:
```
search_prefab_components("Chart")
→ 7 components matching 'Chart':
AreaChart — from prefab_ui.components.charts import AreaChart
BarChart — from prefab_ui.components.charts import BarChart
...
```
Passing `detail=True` returns full field descriptions and docstrings. The search tool introspects Prefab classes at runtime, so it's always up to date with the installed version.
## Passing data
The `generate_prefab_ui` tool accepts a `data` parameter. Values become global variables in the sandbox:
```python
# The LLM can reference 'sales_data' directly in its code
result = await generate_prefab_ui(
code="...",
data={"sales_data": [{"month": "Jan", "revenue": 42000}, ...]}
)
```
This lets the model use data from earlier in the conversation to build visualizations.
## Configuration
`GenerativeUI` takes options for customizing tool names:
```python
GenerativeUI(
tool_name="generate_prefab_ui", # default
components_tool_name="search_prefab_components", # default
include_components_tool=True, # default
)
```
## Requirements
Generative UI needs `fastmcp[apps]`, which pulls in `prefab-ui`. The server-side Pyodide sandbox (for final validation) requires Deno — it installs automatically on first use.
The streaming renderer loads Pyodide from CDN in the browser. The CSP is configured automatically by the provider — no manual setup.
## Sandbox limitations
The Pyodide sandbox includes the Python standard library and Prefab. External packages (NumPy, pandas, requests, etc.) are **not available** — the LLM's code must work with only built-in Python and Prefab. If the LLM imports something unavailable, the sandbox raises `ImportError`.
## Next steps
- **[Interactive Tools](/apps/prefab)** — the component building blocks the LLM will use
- **[Prefab component reference](https://prefab.prefect.io/docs/components)** — full component library
- **[Development](/apps/development)** — preview generative tools locally with `fastmcp dev apps`

Binary file not shown.

After

Width:  |  Height:  |  Size: 571 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 683 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 580 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

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