diff --git a/.claude/skills/code-review/SKILL.md b/.claude/skills/code-review/SKILL.md
index 2542ee68f..bcc2698dd 100644
--- a/.claude/skills/code-review/SKILL.md
+++ b/.claude/skills/code-review/SKILL.md
@@ -19,6 +19,13 @@ Be friendly and welcoming while maintaining high standards. Call out what works
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:
diff --git a/.claude/skills/review-issue/SKILL.md b/.claude/skills/review-issue/SKILL.md
new file mode 100644
index 000000000..384aa137c
--- /dev/null
+++ b/.claude/skills/review-issue/SKILL.md
@@ -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 `. 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: #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 --repo PrefectHQ/fastmcp --json number,title,body,labels,files,additions,deletions
+gh pr view --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
+```
+
+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.
diff --git a/.claude/skills/review-pr/SKILL.md b/.claude/skills/review-pr/SKILL.md
new file mode 100644
index 000000000..ae37ee33a
--- /dev/null
+++ b/.claude/skills/review-pr/SKILL.md
@@ -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
diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
index 267df6812..1e6139cfb 100644
--- a/.github/ISSUE_TEMPLATE/bug.yml
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -3,33 +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
- - 💡 **The ideal issue is a clear problem description and an MRE — that's it.** If you've done a 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 or proposed fix. We have LLMs too, and an incorrect analysis is harder to work with than none at all.
- - ✂️ **Keep it short.** A one-paragraph description and a working MRE is the ideal bug report. Issues that are difficult to parse — due to length, speculation, or generated content — 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
diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml
index a803ec399..39c66647d 100644
--- a/.github/ISSUE_TEMPLATE/enhancement.yml
+++ b/.github/ISSUE_TEMPLATE/enhancement.yml
@@ -3,33 +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?
- - ✂️ **Keep it short.** Describe the problem you're trying to solve and why existing behavior falls short. Skip proposed implementations unless you have a specific, well-considered suggestion — we don't need LLM-generated API designs. Requests that are difficult to parse may be closed without response.
-
- 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
diff --git a/.github/actions/run-claude/action.yml b/.github/actions/run-claude/action.yml
index fff6788a6..b79131462 100644
--- a/.github/actions/run-claude/action.yml
+++ b/.github/actions/run-claude/action.yml
@@ -37,10 +37,15 @@ inputs:
required: false
default: ""
+ extra-allowed-tools:
+ description: "Additional comma-separated tools to append to allowed-tools"
+ required: false
+ default: ""
+
model:
description: "Model to use for Claude"
required: false
- default: "claude-opus-4-6"
+ default: "claude-opus-4-8"
allowed-bots:
description: "Allowed bot usernames, or '*' for all bots"
@@ -88,7 +93,7 @@ runs:
track_progress: ${{ inputs.track-progress }}
prompt: ${{ inputs.prompt }}
claude_args: |
- ${{ (inputs.allowed-tools != '' || inputs.extra-allowed-tools != '') && format('--allowedTools {0}{1}', inputs.allowed-tools, inputs.extra-allowed-tools != '' && format(',{0}', inputs.extra-allowed-tools) || '') || '' }}
+ ${{ (inputs.allowed-tools != '' || inputs.extra-allowed-tools != '') && format('--allowedTools ''{0}{1}''', inputs.allowed-tools, inputs.extra-allowed-tools != '' && format(',{0}', inputs.extra-allowed-tools) || '') || '' }}
${{ inputs.mcp-servers != '' && format('--mcp-config ''{0}''', inputs.mcp-servers) || '' }}
--model ${{ inputs.model }}
settings: |
diff --git a/.github/actions/run-pytest/action.yml b/.github/actions/run-pytest/action.yml
index ff429a4cc..c82e9c0bd 100644
--- a/.github/actions/run-pytest/action.yml
+++ b/.github/actions/run-pytest/action.yml
@@ -3,7 +3,7 @@ description: "Run pytest with appropriate flags for the test type and platform"
inputs:
test-type:
- description: "Type of tests to run: unit, integration, or client_process"
+ description: "Type of tests to run: unit, integration, client_process, or conformance"
required: false
default: "unit"
@@ -19,25 +19,47 @@ runs:
MAX_PROCS="2"
EXTRA_FLAGS=""
elif [ "${{ inputs.test-type }}" == "client_process" ]; then
- MARKER="client_process"
+ 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"
+ 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" ] && [ "${{ runner.os }}" != "Windows" ]; then
+ 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 \
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index 20d3ccecf..000000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-version: 2
-updates:
- - package-ecosystem: "pip"
- directory: "/"
- schedule:
- interval: "daily"
- labels:
- - "dependencies"
- - package-ecosystem: "github-actions"
- directory: "/"
- schedule:
- interval: "weekly"
- labels:
- - "dependencies"
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 68212c72f..1b3333782 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,28 +1,22 @@
## Description
-
+
-**Contributors Checklist**
-
+## 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
+
-**Review Checklist**
-
+- [ ] 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)
diff --git a/.github/release.yml b/.github/release.yml
index 5ff95aace..5397d75e4 100644
--- a/.github/release.yml
+++ b/.github/release.yml
@@ -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:
diff --git a/.github/scripts/triage-label.sh b/.github/scripts/triage-label.sh
new file mode 100755
index 000000000..c6bdda85c
--- /dev/null
+++ b/.github/scripts/triage-label.sh
@@ -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 ..." >&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
diff --git a/.github/workflows/auto-close-duplicates.yml b/.github/workflows/auto-close-duplicates.yml
index d358a3a1e..a5606e5ff 100644
--- a/.github/workflows/auto-close-duplicates.yml
+++ b/.github/workflows/auto-close-duplicates.yml
@@ -16,11 +16,11 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ 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 }}
diff --git a/.github/workflows/auto-close-needs-mre.yml b/.github/workflows/auto-close-needs-mre.yml
index 4338c58d8..08428ab0c 100644
--- a/.github/workflows/auto-close-needs-mre.yml
+++ b/.github/workflows/auto-close-needs-mre.yml
@@ -16,11 +16,11 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ 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 }}
diff --git a/.github/workflows/marvin-comment-on-issue.yml b/.github/workflows/marvin-comment-on-issue.yml
index 8d297a226..72c38cdf7 100644
--- a/.github/workflows/marvin-comment-on-issue.yml
+++ b/.github/workflows/marvin-comment-on-issue.yml
@@ -25,7 +25,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
- name: Install UV
uses: astral-sh/setup-uv@v7
@@ -36,14 +36,9 @@ jobs:
- name: Install dependencies
run: uv sync --python 3.12
- - name: Run prek
- uses: j178/prek-action@v1
- env:
- SKIP: no-commit-to-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 }}
@@ -56,6 +51,9 @@ jobs:
- 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 }}
@@ -66,13 +64,13 @@ jobs:
Repository: ${{ github.repository }}
Issue Number: #${{ github.event.issue.number }}
- Issue Title: ${{ github.event.issue.title }}
+ Issue Title: ${{ env.ISSUE_TITLE }}
Issue Author: ${{ github.event.issue.user.login }}
Comment Author: ${{ github.event.comment.user.login }}
- ${{ github.event.comment.body }}
+ ${{ env.COMMENT_BODY }}
@@ -80,9 +78,7 @@ jobs:
- You CAN: Read/analyze code, modify files, write code, run tests, execute commands
- You CAN: Commit code, push changes, create branches, create pull requests
-
+ You CAN: Read/analyze code, modify files, write code, run tests, execute commands, commit code, push changes, create branches, create pull requests
@@ -112,20 +108,30 @@ jobs:
- Answer questions about the codebase
- - Help debug reported problems (make changes locally to test, cannot push)
+ - 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
- - Be concise and actionable
- - If the request is unclear, ask clarifying questions
- - If the request requires actions you cannot perform (like pushing changes), explain what you can and cannot do
- - When making code changes, explain that they are local only and cannot be pushed
+ - 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 `` 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.
+
+ - 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.
+
+
Always end your comment with a new line, three dashes, and the footer message:
diff --git a/.github/workflows/marvin-comment-on-pr.yml b/.github/workflows/marvin-comment-on-pr.yml
index 09f699522..369a90c6b 100644
--- a/.github/workflows/marvin-comment-on-pr.yml
+++ b/.github/workflows/marvin-comment-on-pr.yml
@@ -24,7 +24,7 @@ jobs:
steps:
- name: Checkout PR head branch
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
# do not set to pull_request.head.ref, claude will pull the branch if needed
fetch-depth: 0
@@ -38,14 +38,9 @@ jobs:
- name: Install dependencies
run: uv sync --python 3.12
- - name: Run prek
- uses: j178/prek-action@v1
- env:
- SKIP: no-commit-to-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 }}
@@ -77,6 +72,8 @@ jobs:
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 }}
@@ -87,7 +84,7 @@ jobs:
Repository: ${{ github.repository }}
PR Number: #${{ steps.pr-info.outputs.pr_number }}
- PR Title: ${{ github.event.issue.title }}
+ PR Title: ${{ env.PR_TITLE }}
PR Author: ${{ github.event.issue.user.login }}
Comment Author: ${{ github.event.comment.user.login }}
@@ -95,7 +92,7 @@ jobs:
- ${{ github.event.comment.body }}
+ ${{ env.COMMENT_BODY }}
@@ -103,12 +100,10 @@ jobs:
- This workflow allows read, write, and execute capabilities but cannot push changes.
+ 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
- You CAN: Read/analyze code, modify files, write code, run tests, execute commands, resolve review threads
- You CANNOT: Commit code, push changes, create branches, checkout branches, create pull requests
-
- **Important**: You cannot push changes to the repository - you can only make changes locally and provide feedback or recommendations.
+ When making changes, commit and push to the PR's head branch so the author gets the fix directly.
@@ -137,10 +132,10 @@ jobs:
- - Address review feedback and fix issues (make changes locally, cannot push)
+ - Address review feedback and fix issues (commit and push to the PR branch)
- Answer questions about the changes
- - Make additional code changes (local only)
- - Resolve review threads after addressing feedback (if changes are made separately)
+ - Make code changes and push them
+ - Resolve review threads after addressing feedback
- Perform PR reviews when asked (use the PR review process below)
@@ -229,6 +224,25 @@ jobs:
6. Breaking changes to public APIs without migration path
7. Missing or incorrect test coverage for critical paths
+
+
+ **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.
+
@@ -249,14 +263,18 @@ jobs:
- `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: Since you cannot push changes, you can resolve threads to acknowledge feedback, but actual fixes would need to be applied separately.
+ Note: You can resolve threads after pushing fixes, or resolve them to acknowledge feedback that will be addressed separately.
- - Be concise and actionable
- - If the request is unclear, ask clarifying questions
- - If the request requires actions you cannot perform (like pushing changes), explain what you can and cannot do
- - When making code changes, explain that they are local only and cannot be pushed
+ - 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 `` 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:
@@ -268,6 +286,11 @@ jobs:
Keep the comment short, e.g., "I've submitted my review requesting changes. See the review for details."
+
+ - 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.
+
+
Always end your comment with a new line, three dashes, and the footer message:
diff --git a/.github/workflows/marvin-dedupe-issues.yml b/.github/workflows/marvin-dedupe-issues.yml
index a71c590c0..a9de7d0d3 100644
--- a/.github/workflows/marvin-dedupe-issues.yml
+++ b/.github/workflows/marvin-dedupe-issues.yml
@@ -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@v6
+ 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<> "$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
+ --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-6",
+ "model": "claude-sonnet-5",
"env": {
"GH_TOKEN": "${{ steps.marvin-token.outputs.token }}"
}
diff --git a/.github/workflows/marvin-label-triage.yml b/.github/workflows/marvin-label-triage.yml
index 8a3f5f47d..2cd4fe7f5 100644
--- a/.github/workflows/marvin-label-triage.yml
+++ b/.github/workflows/marvin-label-triage.yml
@@ -27,16 +27,32 @@ 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@v6
+ 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 }}
@@ -49,7 +65,16 @@ jobs:
PROMPT<> "$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 }}
@@ -139,11 +180,114 @@ jobs:
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-6",
+ "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
diff --git a/.github/workflows/martian-test-failure.yml b/.github/workflows/marvin-test-failure.yml
similarity index 63%
rename from .github/workflows/martian-test-failure.yml
rename to .github/workflows/marvin-test-failure.yml
index 9f7724fbd..c0c532b23 100644
--- a/.github/workflows/martian-test-failure.yml
+++ b/.github/workflows/marvin-test-failure.yml
@@ -11,7 +11,7 @@ concurrency:
cancel-in-progress: true
jobs:
- martian-test-failure:
+ marvin-test-failure:
# Only run if the test workflow failed
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
runs-on: ubuntu-latest
@@ -23,19 +23,19 @@ jobs:
actions: read # Required for Claude to read CI results
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ uses: actions/checkout@v7
with:
fetch-depth: 1
- 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 }}
- name: Set up Python 3.10
- uses: actions/setup-python@v6
+ uses: actions/setup-python@v7
with:
python-version: "3.10"
@@ -60,6 +60,17 @@ jobs:
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 }}
@@ -75,60 +86,61 @@ jobs:
5. Search the codebase for relevant files, tests, and implementations
# Your Response
- Post a comment on the pull request with your analysis. Your comment should include:
+ Post a comment on the pull request with your analysis.
- ## Test Failure 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.
- **Summary**: A brief 1-2 sentence summary of what failed.
+ Push supporting detail into collapsible `` 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.
- **Root Cause**: A clear explanation of why the tests failed, based on your analysis of the logs and code.
+ For real (non-trivial) failures, use this structure:
- **Suggested Solution**: Specific, actionable steps to fix the failure(s). Include:
- - Which files need to be modified
- - What changes are needed
- - Why these changes will fix the issue
+ **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)
- Detailed Analysis
-
- Include here:
- - Relevant log excerpts showing the failure
- - Code snippets that are causing the issue
- - Any related issues or PRs that might be relevant
+ Log excerpts
+ Relevant failure output
- Related Files
-
- List files that are relevant to the failure with brief explanations of their relevance.
+ Related files
+ Files relevant to the failure
- # Important Guidelines
- - Be concise and actionable - developers want to quickly understand and fix the issue. Provide
- additional context, references, etc in collapsible details blocks to ensure that the comment you're adding
- is short and easy to read but additional information is a click away.
- - Focus on facts from the logs and code, not speculation
- - If you can't determine the root cause, say so clearly
- - If your only suggestion is a bad suggestion (disable the test, change the timeout, etc), indicate that you've run out of ideas and
- that they probably don't want to do that.
- - Provide specific file names, line numbers, and code references when possible
- - 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
+ # 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.
- # CRITICAL: ANGRY USERS
- **IMPORTANT**: If the user is angry with you, the triage bot, don't respond. Just exit immediately without further action.
- If at any point in the conversation the user has asked you to stop replying to the thread, just exit immediately.
+ # 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 `` 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
+ 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
@@ -181,5 +193,5 @@ jobs:
prompt: ${{ steps.analysis-prompt.outputs.PROMPT }}
claude_args: |
- --allowed-tools mcp__repository-summary,mcp__code-search,mcp__github-research,WebSearch,WebFetch,Bash(make:*,git:*)
+ --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
diff --git a/.github/workflows/martian-triage-issue.yml b/.github/workflows/marvin-triage-issue.yml
similarity index 68%
rename from .github/workflows/martian-triage-issue.yml
rename to .github/workflows/marvin-triage-issue.yml
index 4c7ef711e..17d35fefe 100644
--- a/.github/workflows/martian-triage-issue.yml
+++ b/.github/workflows/marvin-triage-issue.yml
@@ -26,14 +26,14 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v6
+ 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 }}
@@ -46,6 +46,9 @@ jobs:
- 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 }}
@@ -54,12 +57,12 @@ jobs:
Repository: ${{ github.repository }}
Issue Number: #${{ github.event.issue.number }}
- Issue Title: ${{ github.event.issue.title }}
+ Issue Title: ${{ env.ISSUE_TITLE }}
Issue Author: ${{ github.event.issue.user.login }}
- ${{ github.event.issue.body }}
+ ${{ env.ISSUE_BODY }}
@@ -119,8 +122,26 @@ jobs:
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 `` 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.
+
+ 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.
+
+
+
+ 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.
+
+
Populate the following sections in your response:
Recommendation (or "No recommendation" with reason)
@@ -133,12 +154,17 @@ jobs:
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 and 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.
+ 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 `` 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.
- # 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.
+ # 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.
Findings
@@ -147,7 +173,7 @@ jobs:
Verification
- I ran the existing tests (if execution commands are available in ``) and confirmed the current behavior:
+
```bash
$ pytest test_calculator.py::test_divide_by_zero
FAILED - raises ValueError instead of DivisionByZeroError
@@ -156,36 +182,25 @@ jobs:
- Detailed Action Plan
+ Action Plan
...a detailed plan that a junior developer could follow to implement the recommendation...
- # Example Output for "Related Items" part of the response
-
Related Issues and Pull Requests
- | Repository | Issue or PR | Relevance |
- | --- | --- | --- |
- | PrefectHQ/fastmcp | [Add matrix operations support](https://github.com/PrefectHQ/fastmcp/pull/680) | This pull request directly addresses the feature request for adding matrix operations to the calculator. |
- | PrefectHQ/fastmcp | [Add matrix operations support](https://github.com/PrefectHQ/fastmcp/issues/681) | This issue directly addresses the feature request for adding matrix operations to the calculator. |
+ | Issue or PR | Relevance |
+ | --- | --- |
+ | [Add matrix operations support](https://github.com/PrefectHQ/fastmcp/pull/680) | Directly addresses the feature request |
Related Files
- | 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) |
-
-
-
- Related Webpages
-
- | 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. |
+ | 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) |
@@ -202,4 +217,5 @@ jobs:
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.
diff --git a/.github/workflows/minimize-resolved-reviews.yml b/.github/workflows/minimize-resolved-reviews.yml
index 0c34bc26f..26e00be35 100644
--- a/.github/workflows/minimize-resolved-reviews.yml
+++ b/.github/workflows/minimize-resolved-reviews.yml
@@ -14,8 +14,12 @@ on:
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 }}
+ group: minimize-reviews-${{ github.event.pull_request.number || github.event.issue.number }}-${{ github.event_name }}
cancel-in-progress: true
permissions:
diff --git a/.github/workflows/publish-fastmcp-remote.yml b/.github/workflows/publish-fastmcp-remote.yml
new file mode 100644
index 000000000..9e2c67990
--- /dev/null
+++ b/.github/workflows/publish-fastmcp-remote.yml
@@ -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
diff --git a/.github/workflows/publish-fastmcp-slim.yml b/.github/workflows/publish-fastmcp-slim.yml
new file mode 100644
index 000000000..9fdc69628
--- /dev/null
+++ b/.github/workflows/publish-fastmcp-slim.yml
@@ -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
diff --git a/.github/workflows/publish-fastmcp-tasks.yml b/.github/workflows/publish-fastmcp-tasks.yml
new file mode 100644
index 000000000..29fc38554
--- /dev/null
+++ b/.github/workflows/publish-fastmcp-tasks.yml
@@ -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
diff --git a/.github/workflows/publish-fastmcp.yml b/.github/workflows/publish-fastmcp.yml
new file mode 100644
index 000000000..8b2ce33b2
--- /dev/null
+++ b/.github/workflows/publish-fastmcp.yml
@@ -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>"
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
deleted file mode 100644
index 5f2fe8d53..000000000
--- a/.github/workflows/publish.yml
+++ /dev/null
@@ -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@v6
- 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/*
diff --git a/.github/workflows/require-issue-link.yml b/.github/workflows/require-issue-link.yml
new file mode 100644
index 000000000..6d674d0e2
--- /dev/null
+++ b/.github/workflows/require-issue-link.yml
@@ -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 = '';
+ // 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 #`, `Closes #`, or `Resolves #` 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 = '';
+ // 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}`);
+ }
+ }
diff --git a/.github/workflows/run-schema-crash-test.yml b/.github/workflows/run-schema-crash-test.yml
new file mode 100644
index 000000000..6c9c766fb
--- /dev/null
+++ b/.github/workflows/run-schema-crash-test.yml
@@ -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
diff --git a/.github/workflows/run-static.yml b/.github/workflows/run-static.yml
index 7acc3aac6..8297ef413 100644
--- a/.github/workflows/run-static.yml
+++ b/.github/workflows/run-static.yml
@@ -7,10 +7,12 @@ 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
@@ -27,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Setup uv
uses: ./.github/actions/setup-uv
@@ -35,6 +37,6 @@ jobs:
resolution: locked
- name: Run prek
- uses: j178/prek-action@v1
+ uses: j178/prek-action@v2
env:
SKIP: no-commit-to-branch
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index 7799e7aa2..7db2b5865 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -7,10 +7,11 @@ 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
@@ -36,7 +37,7 @@ jobs:
timeout-minutes: 10
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Setup uv
uses: ./.github/actions/setup-uv
@@ -47,7 +48,7 @@ jobs:
- name: Run unit tests
uses: ./.github/actions/run-pytest
- - name: Run client process tests
+ - name: Run serial subprocess tests
uses: ./.github/actions/run-pytest
with:
test-type: client_process
@@ -58,7 +59,7 @@ jobs:
timeout-minutes: 10
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Setup uv (lowest-direct)
uses: ./.github/actions/setup-uv
@@ -68,18 +69,41 @@ jobs:
- name: Run unit tests
uses: ./.github/actions/run-pytest
- - name: Run client process tests
+ - 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@v6
+ - uses: actions/checkout@v7
- name: Setup uv
uses: ./.github/actions/setup-uv
@@ -94,3 +118,150 @@ jobs:
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
diff --git a/.github/workflows/run-upgrade-checks.yml b/.github/workflows/run-upgrade-checks.yml
index 3a8162129..485cf1919 100644
--- a/.github/workflows/run-upgrade-checks.yml
+++ b/.github/workflows/run-upgrade-checks.yml
@@ -7,10 +7,10 @@ on:
push:
branches: ["main"]
paths:
- - "src/**"
+ - "fastmcp_slim/**"
- "tests/**"
- - "uv.lock"
- "pyproject.toml"
+ - "uv.lock"
- ".github/workflows/**"
schedule:
@@ -30,7 +30,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Setup uv (upgrade)
uses: ./.github/actions/setup-uv
@@ -38,7 +38,7 @@ jobs:
resolution: upgrade
- name: Run prek
- uses: j178/prek-action@v1
+ uses: j178/prek-action@v2
env:
SKIP: no-commit-to-branch
@@ -56,7 +56,7 @@ jobs:
timeout-minutes: 10
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Setup uv (upgrade)
uses: ./.github/actions/setup-uv
@@ -67,7 +67,7 @@ jobs:
- name: Run unit tests
uses: ./.github/actions/run-pytest
- - name: Run client process tests
+ - name: Run serial subprocess tests
uses: ./.github/actions/run-pytest
with:
test-type: client_process
@@ -78,7 +78,7 @@ jobs:
timeout-minutes: 10
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v7
- name: Setup uv (upgrade)
uses: ./.github/actions/setup-uv
@@ -105,7 +105,7 @@ jobs:
uses: jayqi/failed-build-issue-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
- label: "build-failure"
+ label: "build failed"
title-template: "Upgrade checks failing on main branch"
body-template: |
## Upgrade Checks Failure on Main Branch
@@ -121,7 +121,7 @@ jobs:
- **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).
+ - **MCP SDK**: Breaking changes in the `mcp` package (new method signatures, renamed types).
### What to do
@@ -131,3 +131,27 @@ jobs:
---
*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
diff --git a/.github/workflows/update-config-schema.yml b/.github/workflows/update-config-schema.yml
index ed66ea209..6600981da 100644
--- a/.github/workflows/update-config-schema.yml
+++ b/.github/workflows/update-config-schema.yml
@@ -7,8 +7,8 @@ on:
push:
branches: ["main"]
paths:
- - "src/fastmcp/utilities/mcp_server_config/**"
- - "!src/fastmcp/utilities/mcp_server_config/v1/schema.json"
+ - "fastmcp_slim/fastmcp/utilities/mcp_server_config/**"
+ - "!fastmcp_slim/fastmcp/utilities/mcp_server_config/v1/schema.json"
workflow_dispatch:
permissions:
@@ -23,12 +23,12 @@ jobs:
steps:
- 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@v6
+ - uses: actions/checkout@v7
with:
token: ${{ steps.marvin-token.outputs.token }}
@@ -47,7 +47,7 @@ jobs:
from fastmcp.utilities.mcp_server_config import generate_schema
generate_schema('docs/public/schemas/fastmcp.json/latest.json')
generate_schema('docs/public/schemas/fastmcp.json/v1.json')
- generate_schema('src/fastmcp/utilities/mcp_server_config/v1/schema.json')
+ generate_schema('fastmcp_slim/fastmcp/utilities/mcp_server_config/v1/schema.json')
"
- name: Create Pull Request
@@ -59,7 +59,7 @@ 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.
diff --git a/.github/workflows/update-sdk-docs.yml b/.github/workflows/update-sdk-docs.yml
index 6ca5eb61d..9d05684d4 100644
--- a/.github/workflows/update-sdk-docs.yml
+++ b/.github/workflows/update-sdk-docs.yml
@@ -7,7 +7,7 @@ on:
push:
branches: ["main"]
paths:
- - "src/**"
+ - "fastmcp_slim/**"
- "pyproject.toml"
workflow_dispatch:
@@ -23,12 +23,12 @@ jobs:
steps:
- 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@v6
+ - uses: actions/checkout@v7
with:
token: ${{ steps.marvin-token.outputs.token }}
@@ -42,7 +42,7 @@ 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: just api-ref-all
diff --git a/.gitignore b/.gitignore
index 0c636d5a0..129fe99dd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,6 +65,7 @@ dmypy.json
# Claude worktree management
.claude-wt/worktrees
+.claude/worktrees/
# Agents
/PLAN.md
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index dd3574432..8321b5bde 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,8 +6,8 @@ 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]
@@ -29,7 +29,7 @@ repos:
entry: uv run --isolated ty check
language: system
types: [python]
- files: ^src/|^tests/
+ files: ^fastmcp_slim/|^tests/|^examples/
pass_filenames: false
require_serial: true
diff --git a/CLAUDE.md b/CLAUDE.md
index 9b9ca8214..79b040531 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -27,7 +27,7 @@ uv run prek run --all-files # Ruff + Prettier + ty
| Path | Purpose |
| ----------------- | -------------------------------------- |
-| `src/fastmcp/` | Library source code |
+| `fastmcp_slim/fastmcp/` | Library source code |
| `├─server/` | Server implementation |
| `│ ├─auth/` | Authentication providers |
| `│ └─middleware/` | Error handling, logging, rate limiting |
@@ -50,17 +50,83 @@ When modifying MCP functionality, changes typically need to be applied across al
- **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
-- Apply PR labels: bugs/breaking/enhancements/features
+- 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 --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=
+ 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` (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: ` 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 `.** 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...v`.
+
+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 `) 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 `` block with: a bold linked title (`**[v: ]()**`), a condensed 1-paragraph intro (one sentence for patches), the full categorized PR list reformatted from the `--generate-notes` output (`* by [@user](https://github.com/user) in [#NNNN]()`), a `## New Contributors` list (plain `@user`, linked PR), and a `**Full Changelog**: [vA...vB]()` line.
+- `docs/updates.mdx` is the skimmable card feed. Add an `` wrapping a `` 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`, `compare/v...v`) 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
@@ -79,6 +145,14 @@ When modifying MCP functionality, changes typically need to be applied across al
- 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
@@ -89,6 +163,7 @@ When modifying MCP functionality, changes typically need to be applied across al
### 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`)
@@ -100,8 +175,9 @@ When modifying MCP functionality, changes typically need to be applied across al
- 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 `src/fastmcp/utilities/mcp_server_config/v1/schema.json` — these are auto-generated and maintained via a long-lived PR.
+- 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
@@ -110,6 +186,21 @@ When modifying MCP functionality, changes typically need to be applied across al
- **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
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..745bab294
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -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.
diff --git a/README.md b/README.md
index a5c7cd1fd..920996af9 100644
--- a/README.md
+++ b/README.md
@@ -17,15 +17,16 @@
[](https://gofastmcp.com)
[](https://discord.gg/uu8dJCgttd)
[](https://pypi.org/project/fastmcp)
+[](https://github.com/PrefectHQ/fastmcp-ts)
[](https://github.com/PrefectHQ/fastmcp/actions/workflows/run-tests.yml)
[](https://github.com/PrefectHQ/fastmcp/blob/main/LICENSE)
-
+
---
-The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) connects LLMs to tools and data. FastMCP gives you everything you need to go from prototype to production:
+The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) connects LLMs to tools and data. FastMCP is a full MCP application framework for servers, clients, and interactive apps. A server starts with ordinary Python:
```python
from fastmcp import FastMCP
@@ -77,22 +78,35 @@ FastMCP has three pillars:
**[Servers](https://gofastmcp.com/servers/server)** wrap your Python functions into MCP-compliant tools, resources, and prompts. **[Clients](https://gofastmcp.com/clients/client)** connect to any server with full protocol support. And **[Apps](https://gofastmcp.com/apps/overview)** give your tools interactive UIs rendered directly in the conversation.
-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). When you're ready to deploy, [Prefect Horizon](https://www.prefect.io/horizon) offers free hosting for FastMCP users.
+**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 installing FastMCP with [uv](https://docs.astral.sh/uv/):
+We recommend adding FastMCP to your project with [uv](https://docs.astral.sh/uv/):
```bash
-uv pip install fastmcp
+uv add fastmcp
```
For full installation instructions, including verification and upgrading, see the [**Installation Guide**](https://gofastmcp.com/getting-started/installation).
**Upgrading?** We have guides for:
-- [Upgrading from FastMCP v2](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2)
-- [Upgrading from the MCP Python SDK](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk)
-- [Upgrading from the low-level SDK](https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk)
+- [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
diff --git a/SECURITY.md b/SECURITY.md
index 8e1943ad8..656867d37 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -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/PrefectHQ/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).
diff --git a/docs/development/v3-notes/auth-provider-env-vars.mdx b/dev-docs/v3-notes/auth-provider-env-vars.md
similarity index 100%
rename from docs/development/v3-notes/auth-provider-env-vars.mdx
rename to dev-docs/v3-notes/auth-provider-env-vars.md
diff --git a/v3-notes/get-methods-consolidation.md b/dev-docs/v3-notes/get-methods-consolidation.md
similarity index 100%
rename from v3-notes/get-methods-consolidation.md
rename to dev-docs/v3-notes/get-methods-consolidation.md
diff --git a/v3-notes/prompt-internal-types.md b/dev-docs/v3-notes/prompt-internal-types.md
similarity index 100%
rename from v3-notes/prompt-internal-types.md
rename to dev-docs/v3-notes/prompt-internal-types.md
diff --git a/v3-notes/provider-architecture.md b/dev-docs/v3-notes/provider-architecture.md
similarity index 100%
rename from v3-notes/provider-architecture.md
rename to dev-docs/v3-notes/provider-architecture.md
diff --git a/v3-notes/provider-test-pattern.md b/dev-docs/v3-notes/provider-test-pattern.md
similarity index 100%
rename from v3-notes/provider-test-pattern.md
rename to dev-docs/v3-notes/provider-test-pattern.md
diff --git a/v3-notes/resource-internal-types.md b/dev-docs/v3-notes/resource-internal-types.md
similarity index 100%
rename from v3-notes/resource-internal-types.md
rename to dev-docs/v3-notes/resource-internal-types.md
diff --git a/v3-notes/task-meta-parameter.md b/dev-docs/v3-notes/task-meta-parameter.md
similarity index 100%
rename from v3-notes/task-meta-parameter.md
rename to dev-docs/v3-notes/task-meta-parameter.md
diff --git a/dev-docs/v3-notes/v3-features.md b/dev-docs/v3-notes/v3-features.md
new file mode 100644
index 000000000..62d11e0b1
--- /dev/null
+++ b/dev-docs/v3-notes/v3-features.md
@@ -0,0 +1,1481 @@
+---
+title: v3.0 Feature Tracking
+---
+
+This document tracks major features in FastMCP v3.0 for release notes preparation.
+
+## 3.0.0rc1
+
+### SamplingTool Conversion Helpers
+
+Server tools (FunctionTool and TransformedTool) can now be passed directly to sampling methods via `SamplingTool.from_callable_tool()` ([#3062](https://github.com/PrefectHQ/fastmcp/pull/3062)). Previously, tools defined with `@mcp.tool` had to be recreated as functions for use in `ctx.sample()`. Now `ctx.sample()` and `ctx.sample_step()` accept these tool instances directly.
+
+```python
+@mcp.tool
+def search(query: str) -> str:
+ """Search the web."""
+ return do_search(query)
+
+# Use tool directly in sampling
+result = await ctx.sample(
+ "Research Python frameworks",
+ tools=[search] # FunctionTool works directly!
+)
+```
+
+### Google GenAI Sampling Handler
+
+FastMCP now includes a sampling handler for Google's Gemini models ([#2977](https://github.com/jlowin/fastmcp/pull/2977)). This enables MCP clients to use Google's GenAI models with the sampling protocol, including full tool calling support.
+
+```python
+from fastmcp import Client
+from fastmcp.client.sampling.handlers.google_genai import GoogleGenaiSamplingHandler
+from google.genai import Client as GoogleGenaiClient
+
+# Initialize the handler
+handler = GoogleGenaiSamplingHandler(
+ default_model="gemini-2.0-flash-exp",
+ client=GoogleGenaiClient(), # Optional - creates one if not provided
+)
+
+# Use with MCP sampling (handler is configured at Client construction)
+async with Client("http://server/mcp", sampling_handler=handler) as client:
+ result = await client.sample(
+ messages=[...],
+ tools=[...],
+ )
+```
+
+Key features:
+- Converts MCP tool schemas to Google's function calling format
+- Supports all Google GenAI models that implement function calling
+- Handles nullable types, nested objects, and arrays in tool schemas
+- Properly maps tool choices (`auto`, `required`, `none`) to Google's configuration
+- Preserves model preferences from MCP sampling parameters
+
+The handler joins the existing Anthropic and OpenAI handlers, providing a consistent interface for model-agnostic sampling across providers.
+
+### Concurrent Tool Execution in Sampling
+
+When an LLM returns multiple tool calls in a single sampling response, they can now be executed concurrently ([#3022](https://github.com/PrefectHQ/fastmcp/pull/3022)). Default behavior remains sequential; opt in with `tool_concurrency`. Tools can declare `sequential=True` to force sequential execution even when concurrency is enabled.
+
+```python
+result = await context.sample(
+ messages="Fetch weather for NYC and LA",
+ tools=[fetch_weather],
+ tool_concurrency=0, # Unlimited parallel execution
+)
+```
+
+### OpenAPI `validate_output` Option
+
+`OpenAPIProvider` and `FastMCP.from_openapi()` now accept `validate_output=False` to skip output schema validation ([#3134](https://github.com/PrefectHQ/fastmcp/pull/3134)). Useful when backends don't conform to their own OpenAPI response schemas — structured JSON still flows through, only the strict schema checking is disabled.
+
+```python
+mcp = FastMCP.from_openapi(
+ openapi_spec=spec,
+ client=client,
+ validate_output=False,
+)
+```
+
+### Auth Token Injection and Azure OBO Dependencies
+
+New dependency injection for accessing the authenticated user's token directly in tool parameters ([#2918](https://github.com/PrefectHQ/fastmcp/pull/2918)). Works with any auth provider.
+
+```python
+from fastmcp.server.dependencies import CurrentAccessToken, TokenClaim
+from fastmcp.server.auth import AccessToken
+
+@mcp.tool()
+async def my_tool(
+ token: AccessToken = CurrentAccessToken,
+ user_id: str = TokenClaim("oid"),
+): ...
+```
+
+For Azure/Entra, the new `fastmcp[azure]` extra adds `EntraOBOToken`, which handles the On-Behalf-Of token exchange declaratively:
+
+```python
+from fastmcp.server.auth.providers.azure import EntraOBOToken
+
+@mcp.tool()
+async def get_emails(
+ graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"]),
+):
+ # graph_token is ready — OBO exchange happened automatically
+ ...
+```
+
+### `generate-cli` Agent Skill Generation
+
+`fastmcp generate-cli` now produces a `SKILL.md` alongside the CLI script ([#3115](https://github.com/PrefectHQ/fastmcp/pull/3115)) — a Claude Code agent skill with pre-computed invocation syntax for every tool. Agents reading the skill can call tools immediately without running `--help`. On by default; pass `--no-skill` to opt out.
+
+### Background Task Notification Queue
+
+Background tasks now use a distributed Redis notification queue for reliable delivery ([#2906](https://github.com/PrefectHQ/fastmcp/pull/2906)). Elicitation switches from polling to BLPOP (single blocking call instead of ~7,200 round-trips/hour), and notification delivery retries up to 3x with TTL-based expiration.
+
+### Async Auth Checks
+
+Auth check functions can now be `async`, enabling authorization decisions that depend on asynchronous operations like reading server state via `Context.get_state` or calling external services ([#3150](https://github.com/PrefectHQ/fastmcp/issues/3150)). Sync and async checks can be freely mixed. Previously, passing an async function as an auth check would silently pass (coroutine objects are truthy).
+
+### Optional `$ref` Dereferencing in Schemas
+
+Schema `$ref` dereferencing — which inlines all `$defs` for compatibility with MCP clients that don't handle `$ref` — is now controlled by the `dereference_schemas` constructor kwarg ([#3141](https://github.com/PrefectHQ/fastmcp/issues/3141)). Default is `True` (dereference on) because the non-compliant clients are popular and the failure mode is silent breakage that server authors can't diagnose. Opt out when you know your clients handle `$ref` and want smaller schemas:
+
+```python
+mcp = FastMCP("my-server", dereference_schemas=False)
+```
+
+Dereferencing is implemented as middleware (`DereferenceRefsMiddleware`) that runs at serve-time, so schemas are stored with `$ref` intact and only inlined when sent to clients.
+
+### Breaking: Deprecated `FastMCP()` Constructor Kwargs Removed
+
+Sixteen deprecated keyword arguments have been removed from `FastMCP.__init__`. Passing any of them now raises `TypeError` with a migration hint. Environment variables (e.g., `FASTMCP_HOST`) continue to work — only the constructor kwargs moved.
+
+**Transport/server settings** (`host`, `port`, `log_level`, `debug`, `sse_path`, `message_path`, `streamable_http_path`, `json_response`, `stateless_http`): Pass to `run()`, `run_http_async()`, or `http_app()` as appropriate, or set via environment variables.
+
+```python
+# Before
+mcp = FastMCP("server", host="0.0.0.0", port=8080)
+mcp.run()
+
+# After
+mcp = FastMCP("server")
+mcp.run(transport="http", host="0.0.0.0", port=8080)
+```
+
+**Duplicate handling** (`on_duplicate_tools`, `on_duplicate_resources`, `on_duplicate_prompts`): Use the unified `on_duplicate=` parameter.
+
+**Tag filtering** (`include_tags`, `exclude_tags`): Use `server.enable(tags=..., only=True)` and `server.disable(tags=...)` after construction.
+
+**Tool serializer** (`tool_serializer`): Return `ToolResult` from tools instead.
+
+**Tool transformations** (`tool_transformations`): Use `server.add_transform(ToolTransform(...))` after construction.
+
+The `_deprecated_settings` attribute and `.settings` property are also removed. `ExperimentalSettings` has been deleted (dead code).
+
+### Breaking: `ui=` Renamed to `app=`
+
+The MCP Apps decorator parameter has been renamed from `ui=ToolUI(...)` / `ui=ResourceUI(...)` to `app=AppConfig(...)` ([#3117](https://github.com/PrefectHQ/fastmcp/pull/3117)). `ToolUI` and `ResourceUI` are consolidated into a single `AppConfig` class. Wire format is unchanged. See the MCP Apps section under beta2 for full details.
+## 3.0.0beta2
+
+### CLI: `fastmcp list` and `fastmcp call`
+
+New client-side CLI commands for querying and invoking tools on any MCP server — remote URLs, local Python files, MCPConfig JSON, or arbitrary stdio commands. Especially useful for giving LLMs that don't have built-in MCP support access to MCP tools via shell commands.
+
+```bash
+# Discover tools on a server
+fastmcp list http://localhost:8000/mcp
+fastmcp list server.py
+fastmcp list --command 'npx -y @modelcontextprotocol/server-github'
+
+# Call a tool
+fastmcp call server.py greet name=World
+fastmcp call http://localhost:8000/mcp search query=hello limit=5
+fastmcp call server.py create_item '{"name": "Widget", "tags": ["a", "b"]}'
+```
+
+Key features:
+- Tool arguments are auto-coerced using the tool's JSON schema (`limit=5` → int)
+- Single JSON objects work as positional args alongside `key=value` and `--input-json`
+- `--input-schema` / `--output-schema` for full JSON schemas, `--json` for machine-readable output
+- `--transport sse` for SSE servers, `--command` for stdio servers
+- Auto OAuth for HTTP targets (no-ops if server doesn't require auth)
+- Fuzzy tool name matching suggests alternatives on typos
+- Interactive terminal elicitation for tools that request user input mid-execution
+
+Documentation: [CLI Querying](https://gofastmcp.com/v3/cli/client)
+
+### CLI: `fastmcp discover` and name-based resolution
+
+`fastmcp discover` scans editor configs (Claude Desktop, Claude Code, Cursor, Gemini CLI, Goose) and project-level `mcp.json` files for MCP server definitions. Discovered servers can be referenced by name — or `source:name` for precision — in `fastmcp list` and `fastmcp call`.
+
+```bash
+# See all configured servers
+fastmcp discover
+
+# Use a server by name
+fastmcp list weather
+fastmcp call weather get_forecast city=London
+
+# Target a specific source with source:name
+fastmcp list claude-code:my-server
+fastmcp call cursor:weather get_forecast city=London
+
+# Filter discovery to specific sources
+fastmcp discover --source claude-code --source cursor
+```
+
+Documentation: [CLI Querying](https://gofastmcp.com/v3/cli/client)
+
+### CLI: Expanded Reload File Watching
+
+The `--reload` flag now watches a comprehensive set of file types, making it suitable for MCP apps with frontend bundles ([#3028](https://github.com/PrefectHQ/fastmcp/pull/3028)). Previously limited to `.py` files, it now watches JavaScript, TypeScript, HTML, CSS, config files, and media assets.
+
+### CLI: fastmcp install stdio
+
+The new `fastmcp install stdio` command generates full `uv run` commands for running FastMCP servers over stdio ([#3032](https://github.com/PrefectHQ/fastmcp/pull/3032)).
+
+```bash
+# Generate command for a server
+fastmcp install stdio server.py
+
+# Outputs:
+# uv run --directory /path/to/project fastmcp run server.py
+```
+
+The command automatically detects the project directory and generates the appropriate `uv run` invocation, making it easy to integrate FastMCP servers with MCP clients.
+
+### CIMD (Client ID Metadata Documents)
+
+CIMD provides an alternative to Dynamic Client Registration for OAuth-authenticated MCP servers. Instead of registering with each server dynamically, clients host a static JSON document at an HTTPS URL. That URL becomes the client's `client_id`, and servers verify identity through domain ownership.
+
+**Client usage:**
+
+```python
+from fastmcp import Client
+from fastmcp.client.auth import OAuth
+
+async with Client(
+ "https://mcp-server.example.com/mcp",
+ auth=OAuth(
+ client_metadata_url="https://myapp.example.com/oauth/client.json",
+ ),
+) as client:
+ await client.ping()
+```
+
+The `OAuth` helper now supports deferred binding — `mcp_url` is optional when using `OAuth` with `Client(auth=...)`, since the transport provides the server URL automatically.
+
+**CLI tools for document management:**
+
+```bash
+# Generate a CIMD document
+fastmcp auth cimd create --name "My App" \
+ --redirect-uri "http://localhost:*/callback" \
+ --client-id "https://myapp.example.com/oauth/client.json" \
+ --output client.json
+
+# Validate a hosted document
+fastmcp auth cimd validate https://myapp.example.com/oauth/client.json
+```
+
+**Server-side support:**
+
+CIMD is enabled by default on `OAuthProxy` and its provider subclasses (GitHub, Google, etc.). The server-side implementation includes SSRF-hardened document fetching with DNS pinning, dual redirect URI validation (both CIMD document patterns and proxy patterns must match), HTTP cache-aware revalidation, and `private_key_jwt` assertion validation for clients that need stronger authentication than public client auth.
+
+Key details:
+- CIMD URLs must be HTTPS with a non-root path
+- `token_endpoint_auth_method` limited to `none` or `private_key_jwt` (no shared secrets)
+- `redirect_uris` in CIMD documents support wildcard port patterns (`http://localhost:*/callback`)
+- Servers fetch and cache documents with standard HTTP caching (ETag, Last-Modified, Cache-Control)
+- CIMD is a protocol-level feature — any auth provider implementing the spec can support it
+
+Documentation: [CIMD Authentication](https://gofastmcp.com/v3/clients/auth/cimd), [OAuth Proxy CIMD config](https://gofastmcp.com/v3/servers/auth/oauth-proxy#cimd-support)
+
+### Pre-Registered OAuth Clients
+
+The `OAuth` client helper now accepts `client_id` and `client_secret` parameters for servers where the client is already registered ([#3086](https://github.com/PrefectHQ/fastmcp/pull/3086)). This bypasses Dynamic Client Registration entirely — useful when DCR is disabled, or when the server has pre-provisioned credentials for your application.
+
+```python
+from fastmcp import Client
+from fastmcp.client.auth import OAuth
+
+async with Client(
+ "https://mcp-server.example.com/mcp",
+ auth=OAuth(
+ client_id="my-registered-app",
+ client_secret="my-secret",
+ scopes=["read", "write"],
+ ),
+) as client:
+ await client.ping()
+```
+
+The static credentials are injected before the OAuth flow begins, so the client never attempts DCR. If the server rejects the credentials, the error surfaces immediately rather than retrying with fresh registration (which can't help for fixed credentials). Public clients can omit `client_secret`.
+
+Documentation: [Pre-Registered Clients](https://gofastmcp.com/v3/clients/auth/oauth#pre-registered-clients)
+
+### CLI: `fastmcp generate-cli`
+
+`fastmcp generate-cli` connects to any MCP server, reads its tool schemas, and writes a standalone Python CLI script where every tool becomes a typed subcommand with flags, help text, and tab completion ([#3065](https://github.com/PrefectHQ/fastmcp/pull/3065)). The insight is that MCP tool schemas already contain everything a CLI framework needs — parameter names, types, descriptions, required/optional status — so the generator maps JSON Schema directly into [cyclopts](https://cyclopts.readthedocs.io/) commands.
+
+```bash
+# Generate from any server spec
+fastmcp generate-cli weather
+fastmcp generate-cli http://localhost:8000/mcp
+fastmcp generate-cli server.py my_weather_cli.py
+
+# Use the generated script
+python my_weather_cli.py call-tool get_forecast --city London --days 3
+python my_weather_cli.py list-tools
+python my_weather_cli.py read-resource docs://readme
+```
+
+The generated script embeds the resolved transport (URL or stdio command), so it's self-contained — users don't need to know about MCP or FastMCP to use it. Supports `-f` to overwrite existing files, and name-based resolution via `fastmcp discover`.
+
+Documentation: [Generate CLI](https://gofastmcp.com/v3/cli/generate-cli)
+
+### CLI: Goose Integration
+
+New `fastmcp install goose` command that generates a `goose://extension?...` deeplink URL and opens it, prompting Goose to install the server as a STDIO extension ([#3040](https://github.com/PrefectHQ/fastmcp/pull/3040)). Goose requires `uvx` rather than `uv run`, so the command builds the appropriate invocation automatically.
+
+```bash
+fastmcp install goose server.py
+fastmcp install goose server.py --with pandas --python 3.11
+```
+
+Also adds a full integration guide at [Goose Integration](https://gofastmcp.com/v3/integrations/goose).
+
+### ResponseLimitingMiddleware
+
+New middleware for controlling tool response sizes, preventing large outputs from overwhelming LLM context windows ([#3072](https://github.com/PrefectHQ/fastmcp/pull/3072)). Text responses are truncated at UTF-8 character boundaries; structured responses (tools with `output_schema`) raise `ToolError` since truncation would corrupt the schema.
+
+```python
+from fastmcp.server.middleware.response_limiting import ResponseLimitingMiddleware
+
+# Limit all tool responses to 500KB
+mcp.add_middleware(ResponseLimitingMiddleware(max_size=500_000))
+
+# Limit only specific tools, raise errors instead of truncating
+mcp.add_middleware(ResponseLimitingMiddleware(
+ max_size=100_000,
+ tools=["search", "fetch_data"],
+ raise_on_unstructured=True,
+))
+```
+
+Key features:
+- Configurable size limit (default 1MB)
+- Tool-specific filtering via `tools` parameter
+- Size metadata added to result's `meta` field for monitoring
+- Configurable `raise_on_structured` and `raise_on_unstructured` behavior
+
+Documentation: [Middleware](https://gofastmcp.com/v3/servers/middleware)
+
+### Background Task Context (SEP-1686)
+
+`Context` now works transparently in background tasks running in Docket workers ([#2905](https://github.com/PrefectHQ/fastmcp/pull/2905)). Previously, tools running as background tasks couldn't use `ctx.elicit()` because there was no active request context. Now, when a tool executes in a Docket worker, `Context` detects this via its `task_id` and routes elicitation through Redis-based coordination: the task sets its status to `input_required`, sends a `notifications/tasks/updated` notification with elicitation metadata, and waits for the client to respond via `tasks/sendInput`.
+
+```python
+@mcp.tool(task=True)
+async def interactive_task(ctx: Context) -> str:
+ # Works transparently in both foreground and background task modes
+ result = await ctx.elicit("Please provide additional input", str)
+
+ if isinstance(result, AcceptedElicitation):
+ return f"You provided: {result.data}"
+ else:
+ return "Elicitation was declined or cancelled"
+```
+
+`ctx.is_background_task` and `ctx.task_id` are available for tools that need to branch on execution mode.
+
+### `require_auth` Removed
+
+The `require_auth` authorization check introduced in beta1 has been removed in favor of scope-based authorization via `require_scopes` ([#3103](https://github.com/PrefectHQ/fastmcp/pull/3103)). Since configuring an `AuthProvider` already rejects unauthenticated requests at the transport level, `require_auth` was redundant — `require_scopes` provides the same guarantee with better granularity. The beta1 Component Authorization section has been updated to reflect this.
+
+### MCP Apps (SDK Compatibility)
+
+Support for [MCP Apps](https://modelcontextprotocol.io/specification/2025-06-18/server/apps) — the spec extension that lets MCP servers deliver interactive UIs via sandboxed iframes. Extension negotiation, typed UI metadata on tools and resources, and the `ui://` resource scheme. No component DSL, renderer, or `FastMCPApp` class yet — those are future phases.
+
+**Breaking change from beta 2:** The `ui=` parameter on `@mcp.tool()` and `@mcp.resource()` has been renamed to `app=`, and the `ToolUI`/`ResourceUI` classes have been consolidated into a single `AppConfig` class. This follows the established `task=True`/`TaskConfig` pattern. The wire format (`meta["ui"]`, `_meta.ui`) is unchanged.
+
+**Registering tools with app metadata:**
+
+```python
+from fastmcp import FastMCP
+from fastmcp.apps import AppConfig, ResourceCSP, ResourcePermissions
+
+mcp = FastMCP("My Server")
+
+# Register the HTML bundle as a ui:// resource with CSP
+@mcp.resource(
+ "ui://my-app/view.html",
+ app=AppConfig(
+ csp=ResourceCSP(resource_domains=["https://unpkg.com"]),
+ permissions=ResourcePermissions(clipboard_write={}),
+ ),
+)
+def app_html() -> str:
+ from pathlib import Path
+ return Path("./dist/index.html").read_text()
+
+# Tool with UI — clients render an iframe alongside the result
+@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
+async def list_users() -> list[dict]:
+ return [{"id": "1", "name": "Alice"}]
+
+# App-only tool — visible to the UI but hidden from the model
+@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html", visibility=["app"]))
+async def delete_user(id: str) -> dict:
+ return {"deleted": True}
+```
+
+The `app=` parameter accepts `True` (enable with defaults), an `AppConfig` instance, or a raw dict for forward compatibility. It merges into `meta["ui"]` — alongside any other metadata you set.
+
+**`ui://` resources** automatically get the correct MIME type (`text/html;profile=mcp-app`) unless you override it explicitly.
+
+**Extension negotiation**: The server advertises `io.modelcontextprotocol/ui` in `capabilities.extensions`. UI metadata (`_meta.ui`) always flows through to clients — the MCP Apps spec assigns visibility enforcement to the host, not the server. Tools can check whether the connected client supports a given extension at runtime via `ctx.client_supports_extension()`:
+
+```python
+from fastmcp import Context
+from fastmcp.apps import AppConfig, UI_EXTENSION_ID
+
+@mcp.tool(app=AppConfig(resource_uri="ui://dashboard"))
+async def dashboard(ctx: Context) -> dict:
+ data = compute_dashboard()
+ if ctx.client_supports_extension(UI_EXTENSION_ID):
+ return data
+ return {"summary": format_text(data)}
+```
+
+**Key details:**
+- `AppConfig` fields: `resource_uri`, `visibility`, `csp`, `permissions`, `domain`, `prefers_border` (all optional). On resources, `resource_uri` and `visibility` are validated as not-applicable and will raise `ValueError` if set.
+- `csp` accepts a `ResourceCSP` model with structured domain lists: `connect_domains`, `resource_domains`, `frame_domains`, `base_uri_domains`
+- `permissions` accepts a `ResourcePermissions` model: `camera`, `microphone`, `geolocation`, `clipboard_write` (each set to `{}` to request)
+- `AppConfig` uses `extra="allow"` for forward compatibility with future spec additions
+- Models use Pydantic aliases for wire format (`resourceUri`, `prefersBorder`, `connectDomains`, `clipboardWrite`)
+- Resource metadata (including CSP/permissions) is propagated to `resources/read` response content items so hosts can read it when rendering the iframe
+- `ctx.client_supports_extension(id)` is a general-purpose method — works for any extension, not just MCP Apps
+- `structuredContent` in tool results already works via `ToolResult` — MCP Apps clients use this to pass data into the iframe
+- The server does not strip `_meta.ui` for non-UI clients; per the spec, visibility enforcement is the host's responsibility
+
+**Future phases** will add a component DSL for building UIs declaratively, an in-repo renderer, and a `FastMCPApp` class.
+
+Implementation: `fastmcp_slim/fastmcp/apps/config.py` (models and constants), with integration points in `server.py` (decorator parameters), `low_level.py` (extension advertisement), and `context.py` (`client_supports_extension` method).
+
+---
+
+## 3.0.0beta1
+
+### Provider-Based Architecture
+
+v3.0 introduces a provider-based component system that replaces v2's static-only registration ([#2622](https://github.com/PrefectHQ/fastmcp/pull/2622)). Providers dynamically source tools, resources, templates, and prompts at runtime.
+
+**Core abstraction** (`fastmcp_slim/fastmcp/server/providers/base.py`):
+```python
+class Provider:
+ async def list_tools(self) -> Sequence[Tool]: ...
+ async def get_tool(self, name: str) -> Tool | None: ...
+ async def list_resources(self) -> Sequence[Resource]: ...
+ async def get_resource(self, uri: str) -> Resource | None: ...
+ async def list_resource_templates(self) -> Sequence[ResourceTemplate]: ...
+ async def get_resource_template(self, uri: str) -> ResourceTemplate | None: ...
+ async def list_prompts(self) -> Sequence[Prompt]: ...
+ async def get_prompt(self, name: str) -> Prompt | None: ...
+```
+
+Providers support:
+- **Lifecycle management**: `async def lifespan()` for setup/teardown
+- **Visibility control**: `enable()` / `disable()` with name, version, tags, components, and allowlist mode
+- **Transform stacking**: `provider.add_transform(Namespace(...))`, `provider.add_transform(ToolTransform(...))`
+
+### LocalProvider
+
+`LocalProvider` (`fastmcp_slim/fastmcp/server/providers/local_provider.py`) manages components registered via decorators. Can be used standalone and attached to multiple servers:
+
+```python
+from fastmcp.server.providers import LocalProvider
+
+provider = LocalProvider()
+
+@provider.tool
+def greet(name: str) -> str:
+ return f"Hello, {name}!"
+
+# Attach to multiple servers
+server1 = FastMCP("Server1", providers=[provider])
+server2 = FastMCP("Server2", providers=[provider])
+```
+
+### ProxyProvider
+
+`ProxyProvider` (`fastmcp_slim/fastmcp/server/providers/proxy.py`) proxies components from remote MCP servers via a client factory. Used by `create_proxy()` and `FastMCP.mount()` for remote server integration.
+
+```python
+from fastmcp.server import create_proxy
+
+# Create proxy to remote server
+server = create_proxy("http://remote-server/mcp")
+```
+
+### OpenAPIProvider
+
+`OpenAPIProvider` (`fastmcp_slim/fastmcp/server/providers/openapi/provider.py`) creates MCP components from OpenAPI specifications. Routes map HTTP operations to tools, resources, or templates based on configurable rules.
+
+```python
+from fastmcp.server.providers.openapi import OpenAPIProvider
+import httpx
+
+client = httpx.AsyncClient(base_url="https://api.example.com")
+provider = OpenAPIProvider(openapi_spec=spec, client=client)
+
+mcp = FastMCP("API Server", providers=[provider])
+```
+
+Features:
+- Automatic route-to-component mapping (GET → resource, POST/PUT/DELETE → tool)
+- Custom route mappings via `route_maps` or `route_map_fn`
+- Component customization via `mcp_component_fn`
+- Name collision detection and handling
+
+### FastMCPProvider
+
+`FastMCPProvider` (`fastmcp_slim/fastmcp/server/providers/fastmcp_provider.py`) wraps a FastMCP server to enable mounting one server onto another. Components delegate execution through the wrapped server's middleware chain.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.providers import FastMCPProvider
+from fastmcp.server.transforms import Namespace
+
+main = FastMCP("Main")
+sub = FastMCP("Sub")
+
+@sub.tool
+def greet(name: str) -> str:
+ return f"Hello, {name}!"
+
+# Mount with namespace
+provider = FastMCPProvider(sub)
+provider.add_transform(Namespace("sub"))
+main.add_provider(provider)
+# Tool accessible as "sub_greet"
+```
+
+### Transforms
+
+Transforms modify components (tools, resources, prompts) as they flow from providers to clients ([#2836](https://github.com/PrefectHQ/fastmcp/pull/2836)). They use a middleware pattern where each transform receives a `call_next` callable to continue the chain.
+
+**Built-in transforms** (`fastmcp_slim/fastmcp/server/transforms/`):
+
+- `Namespace` - adds prefixes to names (`tool` → `api_tool`) and path segments to URIs (`data://x` → `data://api/x`)
+- `ToolTransform` - modifies tool schemas (rename, description, tags, argument transforms)
+- `Visibility` - sets visibility state on components by key or tag (backs `enable()`/`disable()` API)
+- `VersionFilter` - filters components by version range (`version_gte`, `version_lt`)
+- `ResourcesAsTools` - exposes resources as tools for tool-only clients
+- `PromptsAsTools` - exposes prompts as tools for tool-only clients
+
+```python
+from fastmcp.server.transforms import Namespace, ToolTransform
+from fastmcp.tools.tool_transform import ToolTransformConfig
+
+provider = SomeProvider()
+provider.add_transform(Namespace("api"))
+provider.add_transform(ToolTransform({
+ "api_verbose_tool_name": ToolTransformConfig(name="short")
+}))
+
+# Stacking composes transformations
+# "foo" → "api_foo" (namespace) → "short" (rename)
+```
+
+**Custom transforms** subclass `Transform` and override needed methods:
+
+```python
+from collections.abc import Sequence
+from fastmcp.server.transforms import Transform, GetToolNext
+from fastmcp.tools import Tool
+
+class TagFilter(Transform):
+ def __init__(self, required_tags: set[str]):
+ self.required_tags = required_tags
+
+ async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
+ return [t for t in tools if t.tags & self.required_tags]
+
+ async def get_tool(self, name: str, call_next: GetToolNext) -> Tool | None:
+ tool = await call_next(name)
+ return tool if tool and tool.tags & self.required_tags else None
+```
+
+Transforms apply at two levels:
+- **Provider-level**: `provider.add_transform()` - affects only that provider's components
+- **Server-level**: `server.add_transform()` - affects all components from all providers
+
+Documentation: `docs/servers/transforms/transforms.mdx`, `docs/servers/visibility.mdx`
+
+### ResourcesAsTools and PromptsAsTools
+
+These transforms expose resources and prompts as tools for clients that only support the tools protocol. Each transform generates two tools that provide listing and access functionality.
+
+**ResourcesAsTools** generates `list_resources` and `read_resource` tools:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.transforms import ResourcesAsTools
+
+mcp = FastMCP("Server")
+
+@mcp.resource("data://config")
+def get_config() -> dict:
+ return {"setting": "value"}
+
+mcp.add_transform(ResourcesAsTools(mcp))
+# Now has list_resources and read_resource tools
+```
+
+The `list_resources` tool returns JSON with resource metadata. The `read_resource` tool accepts a URI and returns the resource content, preserving both text and binary data through base64 encoding.
+
+**PromptsAsTools** generates `list_prompts` and `get_prompt` tools:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.transforms import PromptsAsTools
+
+mcp = FastMCP("Server")
+
+@mcp.prompt
+def analyze_code(code: str, language: str = "python") -> str:
+ return f"Analyze this {language} code:\n{code}"
+
+mcp.add_transform(PromptsAsTools(mcp))
+# Now has list_prompts and get_prompt tools
+```
+
+The `list_prompts` tool returns JSON with prompt metadata including argument information. The `get_prompt` tool accepts a prompt name and optional arguments dict, returning the rendered prompt as a messages array. Non-text content (like embedded resources) is preserved as structured JSON.
+
+Both transforms:
+- Capture a provider reference at construction for deferred querying
+- Route through `FastMCP.read_resource()` / `FastMCP.render_prompt()` when the provider is FastMCP, ensuring middleware chains execute
+- Fall back to direct provider methods for plain providers
+- Return JSON for easy parsing by tool-only clients
+
+Documentation: `docs/servers/transforms/resources-as-tools.mdx`, `docs/servers/transforms/prompts-as-tools.mdx`
+
+---
+
+### Session-Scoped State
+
+v3.0 changes context state from request-scoped to session-scoped. State now persists across multiple tool calls within the same MCP session.
+
+```python
+@mcp.tool
+async def increment_counter(ctx: Context) -> int:
+ count = await ctx.get_state("counter") or 0
+ await ctx.set_state("counter", count + 1)
+ return count + 1
+```
+
+State is automatically keyed by session ID, ensuring isolation between different clients. The implementation uses [pykeyvalue](https://github.com/strawgate/py-key-value) for pluggable storage backends:
+
+```python
+from key_value.aio.stores.redis import RedisStore
+
+# Use Redis for distributed deployments
+mcp = FastMCP("server", session_state_store=RedisStore(...))
+```
+
+**Key details:**
+- Methods are now async: `await ctx.get_state()`, `await ctx.set_state()`, `await ctx.delete_state()`
+- State expires after 1 day (TTL) to prevent unbounded memory growth
+- Works during `on_initialize` middleware when using the same session object
+- For distributed HTTP, session identity comes from the `mcp-session-id` header
+
+Documentation: `docs/servers/context.mdx`
+
+---
+
+### Visibility System
+
+Components can be enabled/disabled using the visibility system. Each `enable()` or `disable()` call adds a stateless Visibility transform that marks components via internal metadata. Later transforms override earlier ones.
+
+```python
+mcp = FastMCP("Server")
+
+# Disable by name and component type
+mcp.disable(names={"dangerous_tool"}, components=["tool"])
+
+# Disable by tag
+mcp.disable(tags={"admin"})
+
+# Disable by version
+mcp.disable(names={"old_tool"}, version="1.0", components=["tool"])
+
+# Allowlist mode - only show components with these tags
+mcp.enable(tags={"public"}, only=True)
+
+# Enable overrides earlier disable (later transform wins)
+mcp.disable(tags={"internal"})
+mcp.enable(names={"safe_tool"}) # safe_tool is visible despite internal tag
+```
+
+Works at both server and provider level. Supports:
+- **Blocklist mode** (default): All components visible except explicitly disabled
+- **Allowlist mode** (`only=True`): Only explicitly enabled components visible
+- **Tag-based filtering**: Enable/disable groups of components by tag
+- **Override semantics**: Later transforms override earlier marks (enable after disable = enabled)
+- **Transform ordering**: Visibility transforms are injected at the point you call them, so component state is known
+
+#### Per-Session Visibility
+
+Server-level visibility changes affect all connected clients. For per-session control, use `Context` methods that apply rules only to the current session ([#2917](https://github.com/PrefectHQ/fastmcp/pull/2917)):
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.context import Context
+
+mcp = FastMCP("Server")
+
+@mcp.tool(tags={"premium"})
+def premium_analysis(data: str) -> str:
+ return f"Premium analysis of: {data}"
+
+@mcp.tool
+async def unlock_premium(ctx: Context) -> str:
+ """Unlock premium features for this session only."""
+ await ctx.enable_components(tags={"premium"})
+ return "Premium features unlocked"
+
+@mcp.tool
+async def reset_features(ctx: Context) -> str:
+ """Reset to default feature set."""
+ await ctx.reset_visibility()
+ return "Features reset to defaults"
+
+# Globally disabled - sessions unlock individually
+mcp.disable(tags={"premium"})
+```
+
+Session visibility methods:
+- `await ctx.enable_components(...)`: Enable components for this session
+- `await ctx.disable_components(...)`: Disable components for this session
+- `await ctx.reset_visibility()`: Clear session rules, return to global defaults
+
+Session rules override global transforms. FastMCP automatically sends `ToolListChangedNotification` (and resource/prompt equivalents) to affected sessions when visibility changes.
+
+Documentation: `docs/servers/visibility.mdx`
+
+---
+
+### Component Versioning
+
+v3.0 introduces versioning support for tools, resources, and prompts. Components can declare a version, and when multiple versions of the same component exist, the highest version is automatically exposed to clients.
+
+**Declaring versions:**
+
+```python
+@mcp.tool(version="1.0")
+def add(x: int, y: int) -> int:
+ return x + y
+
+@mcp.tool(version="2.0")
+def add(x: int, y: int, z: int = 0) -> int:
+ return x + y + z
+
+# Only v2.0 is exposed to clients via list_tools()
+# Calling "add" invokes the v2.0 implementation
+```
+
+**Version comparison:**
+- Uses PEP 440 semantic versioning (1.10 > 1.9 > 1.2)
+- Falls back to string comparison for non-PEP 440 versions (dates like `2025-01-15` work)
+- Unversioned components sort lower than any versioned component
+- The `v` prefix is normalized (`v1.0` equals `1.0`)
+
+**Version visibility in meta:**
+
+List operations expose all available versions in the component's `meta` field:
+
+```python
+tools = await client.list_tools()
+# Each tool's meta includes:
+# - meta["fastmcp"]["version"]: the version of this component ("2.0")
+# - meta["fastmcp"]["versions"]: all available versions ["2.0", "1.0"]
+```
+
+**Retrieving and calling specific versions:**
+
+```python
+# Get the highest version (default)
+tool = await server.get_tool("add")
+
+# Get a specific version
+tool_v1 = await server.get_tool("add", version="1.0")
+
+# Call a specific version
+result = await server.call_tool("add", {"x": 1, "y": 2}, version="1.0")
+```
+
+**Client version requests:**
+
+The FastMCP client supports version selection:
+
+```python
+async with Client(server) as client:
+ # Call specific tool version
+ result = await client.call_tool("add", {"x": 1, "y": 2}, version="1.0")
+
+ # Get specific prompt version
+ prompt = await client.get_prompt("my_prompt", {"text": "..."}, version="2.0")
+```
+
+For generic MCP clients, pass version via `_meta` in arguments:
+
+```json
+{
+ "x": 1,
+ "y": 2,
+ "_meta": {
+ "fastmcp": {
+ "version": "1.0"
+ }
+ }
+}
+```
+
+**VersionFilter transform:**
+
+The `VersionFilter` transform enables serving different API versions from a single codebase:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.providers import LocalProvider
+from fastmcp.server.transforms import VersionFilter
+
+# Define components on a shared provider
+components = LocalProvider()
+
+@components.tool(version="1.0")
+def calculate(x: int, y: int) -> int:
+ return x + y
+
+@components.tool(version="2.0")
+def calculate(x: int, y: int, z: int = 0) -> int:
+ return x + y + z
+
+# Create servers that share the provider with different filters
+api_v1 = FastMCP("API v1", providers=[components])
+api_v1.add_transform(VersionFilter(version_lt="2.0"))
+
+api_v2 = FastMCP("API v2", providers=[components])
+api_v2.add_transform(VersionFilter(version_gte="2.0"))
+```
+
+Parameters mirror comparison operators:
+- `version_gte`: Versions >= this value pass through
+- `version_lt`: Versions < this value pass through
+
+**Key format:**
+
+Component keys now include a version suffix using `@` as a delimiter:
+- Versioned: `tool:add@1.0`, `resource:data://config@2.0`
+- Unversioned: `tool:add@`, `resource:data://config@`
+
+The `@` is always present (even for unversioned components) to enable unambiguous parsing of URIs that may contain `@`.
+
+---
+
+### Type-Safe Canonical Results
+
+v3.0 introduces type-safe result classes that provide explicit control over component responses while supporting MCP runtime metadata: `ToolResult` ([#2736](https://github.com/PrefectHQ/fastmcp/pull/2736)), `ResourceResult` ([#2734](https://github.com/PrefectHQ/fastmcp/pull/2734)), and `PromptResult` ([#2738](https://github.com/PrefectHQ/fastmcp/pull/2738)).
+
+#### ToolResult
+
+`ToolResult` (`fastmcp_slim/fastmcp/tools/tool.py:79`) provides structured tool responses:
+
+```python
+from fastmcp.tools import ToolResult
+
+@mcp.tool
+def process(data: str) -> ToolResult:
+ return ToolResult(
+ content=[TextContent(type="text", text="Done")],
+ structured_content={"status": "success", "count": 42},
+ meta={"processing_time_ms": 150}
+ )
+```
+
+Fields:
+- `content`: List of MCP ContentBlocks (text, images, etc.)
+- `structured_content`: Dict matching tool's output schema
+- `meta`: Runtime metadata passed to MCP as `_meta`
+
+#### ResourceResult
+
+`ResourceResult` (`fastmcp_slim/fastmcp/resources/resource.py:117`) provides structured resource responses:
+
+```python
+from fastmcp.resources import ResourceResult, ResourceContent
+
+@mcp.resource("data://items")
+def get_items() -> ResourceResult:
+ return ResourceResult(
+ contents=[
+ ResourceContent({"key": "value"}), # auto-serialized to JSON
+ ResourceContent(b"binary data"),
+ ],
+ meta={"count": 2}
+ )
+```
+
+Accepts strings, bytes, or `list[ResourceContent]` for flexible content handling.
+
+#### PromptResult
+
+`PromptResult` (`fastmcp_slim/fastmcp/prompts/prompt.py:109`) provides structured prompt responses:
+
+```python
+from fastmcp.prompts import PromptResult, Message
+
+@mcp.prompt
+def conversation() -> PromptResult:
+ return PromptResult(
+ messages=[
+ Message("What's the weather?"),
+ Message("It's sunny today.", role="assistant"),
+ ],
+ meta={"generated_at": "2024-01-01"}
+ )
+```
+
+---
+
+### Background Tasks (SEP-1686)
+
+v3.0 implements MCP SEP-1686 for background task execution via Docket integration.
+
+**Configuration** (`fastmcp_slim/fastmcp/server/tasks/config.py`):
+
+```python
+from fastmcp.utilities.tasks import TaskConfig
+
+@mcp.tool(task=TaskConfig(mode="required"))
+async def long_running_task():
+ # Must be executed as background task
+ ...
+
+@mcp.tool(task=TaskConfig(mode="optional"))
+async def flexible_task():
+ # Supports both sync and task execution
+ ...
+
+@mcp.tool(task=True) # Shorthand for mode="optional"
+async def simple_task():
+ ...
+```
+
+Task modes:
+- `"forbidden"`: Component does not support task execution (default)
+- `"optional"`: Supports both synchronous and task execution
+- `"required"`: Must be executed as background task
+
+Requires Docket server for task scheduling and result polling.
+
+---
+
+### Decorators Return Functions
+
+v3.0 changes what decorators (`@tool`, `@resource`, `@prompt`) return ([#2856](https://github.com/PrefectHQ/fastmcp/pull/2856)). Decorators now return the original function unchanged, rather than transforming it into a component object.
+
+**v3 behavior (default):**
+```python
+@mcp.tool
+def greet(name: str) -> str:
+ return f"Hello, {name}!"
+
+# greet is still your function - call it directly
+greet("World") # "Hello, World!"
+```
+
+**Why this matters:**
+- Functions stay callable - useful for testing and reuse
+- Instance methods just work: `mcp.add_tool(obj.method)`
+- Matches how Flask, FastAPI, and Typer decorators behave
+
+**For v2 compatibility:**
+
+```python
+import fastmcp
+
+# v2 behavior: decorators return FunctionTool/FunctionResource/FunctionPrompt objects
+fastmcp.settings.decorator_mode = "object"
+```
+
+Environment variable: `FASTMCP_DECORATOR_MODE=object`
+
+---
+
+### CLI Auto-Reload
+
+The `--reload` flag enables file watching with automatic server restarts for development ([#2816](https://github.com/PrefectHQ/fastmcp/pull/2816)).
+
+```bash
+# Watch for changes and restart
+fastmcp run server.py --reload
+
+# Watch specific directories
+fastmcp run server.py --reload --reload-dir ./src --reload-dir ./lib
+
+# Works with any transport
+fastmcp run server.py --reload --transport http --port 8080
+```
+
+Implementation (`fastmcp_slim/fastmcp/cli/run.py`):
+- Uses `watchfiles` for efficient file monitoring
+- Runs server as subprocess for clean restarts
+- Stateless mode for seamless reconnection after restart
+- stdio: Full MCP features including elicitation
+- HTTP: Limited bidirectional features during reload
+
+Also available with `fastmcp dev inspector`:
+```bash
+fastmcp dev inspector server.py # Includes --reload by default
+```
+
+---
+
+### Component Authorization
+
+v3.0 introduces callable-based authorization for tools, resources, and prompts ([#2855](https://github.com/PrefectHQ/fastmcp/pull/2855)).
+
+**Component-level auth**:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import require_scopes
+
+mcp = FastMCP()
+
+@mcp.tool(auth=require_scopes("write"))
+def protected_tool(): ...
+
+@mcp.resource("data://secret", auth=require_scopes("read"))
+def secret_data(): ...
+
+@mcp.prompt(auth=require_scopes("admin"))
+def admin_prompt(): ...
+```
+
+**Server-wide auth via middleware**:
+
+```python
+from fastmcp.server.middleware import AuthMiddleware
+from fastmcp.server.auth import require_scopes, restrict_tag
+
+# Require specific scope for all components
+mcp = FastMCP(middleware=[AuthMiddleware(auth=require_scopes("api"))])
+
+# Tag-based restrictions
+mcp = FastMCP(middleware=[
+ AuthMiddleware(auth=restrict_tag("admin", scopes=["admin"]))
+])
+```
+
+Built-in checks:
+- `require_scopes(*scopes)`: Requires specific OAuth scopes
+- `restrict_tag(tag, scopes)`: Requires scopes only for tagged components
+
+Custom checks receive `AuthContext` with `token` and `component`:
+
+```python
+def custom_check(ctx: AuthContext) -> bool:
+ return ctx.token is not None and "admin" in ctx.token.scopes
+```
+
+STDIO transport bypasses all auth checks (no OAuth concept).
+
+---
+
+### FileSystemProvider
+
+v3.0 introduces `FileSystemProvider`, a fundamentally different approach to organizing MCP servers. Instead of importing a server instance and decorating functions with `@server.tool`, you use standalone decorators in separate files and let the provider discover them.
+
+**The problem it solves**: Traditional servers require coordination between files—either tool files import the server (creating coupling) or the server imports all tool modules (creating a registry bottleneck). FileSystemProvider removes this coupling entirely.
+
+**Usage** ([#2823](https://github.com/PrefectHQ/fastmcp/pull/2823)):
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.providers import FileSystemProvider
+
+# Scans mcp/ directory for decorated functions
+mcp = FastMCP("server", providers=[FileSystemProvider("mcp/")])
+```
+
+**Tool files are self-contained**:
+
+```python
+# mcp/tools/greet.py
+from fastmcp.tools import tool
+
+@tool
+def greet(name: str) -> str:
+ """Greet someone by name."""
+ return f"Hello, {name}!"
+```
+
+Features:
+- **Standalone decorators**: `@tool`, `@resource`, `@prompt` from `fastmcp.tools`, `fastmcp.resources`, `fastmcp.prompts` ([#2832](https://github.com/PrefectHQ/fastmcp/pull/2832))
+- **Reload mode**: `FileSystemProvider("mcp/", reload=True)` re-scans on every request for development
+- **Package support**: Directories with `__init__.py` support relative imports
+- **Warning deduplication**: Broken imports warn once per file modification
+
+Documentation: [FileSystemProvider](https://gofastmcp.com/v3/servers/providers/filesystem)
+
+---
+
+### SkillsProvider
+
+v3.0 introduces `SkillsProvider` for exposing agent skills as MCP resources ([#2944](https://github.com/PrefectHQ/fastmcp/pull/2944)). Skills are directories containing instructions and supporting files that teach AI assistants how to perform tasks—used by Claude Code, Cursor, VS Code Copilot, and other AI coding tools.
+
+**Usage**:
+
+```python
+from pathlib import Path
+from fastmcp import FastMCP
+from fastmcp.server.providers.skills import SkillsDirectoryProvider
+
+mcp = FastMCP("Skills Server")
+mcp.add_provider(SkillsDirectoryProvider(roots=Path.home() / ".claude" / "skills"))
+```
+
+Each subdirectory with a `SKILL.md` file becomes a discoverable skill. Clients see:
+- `skill://{name}/SKILL.md` - Main instruction file
+- `skill://{name}/_manifest` - JSON listing of all files with sizes and hashes
+- `skill://{name}/{path}` - Supporting files (via template or resources)
+
+**Two-layer architecture**:
+- `SkillProvider` - Handles a single skill folder
+- `SkillsDirectoryProvider` - Scans directories, creates a `SkillProvider` per valid skill
+
+**Vendor providers** with locked default paths:
+
+| Provider | Directory |
+|----------|-----------|
+| `ClaudeSkillsProvider` | `~/.claude/skills/` |
+| `CursorSkillsProvider` | `~/.cursor/skills/` |
+| `VSCodeSkillsProvider` | `~/.copilot/skills/` |
+| `CodexSkillsProvider` | `/etc/codex/skills/`, `~/.codex/skills/` |
+| `GeminiSkillsProvider` | `~/.gemini/skills/` |
+| `GooseSkillsProvider` | `~/.config/agents/skills/` |
+| `CopilotSkillsProvider` | `~/.copilot/skills/` |
+| `OpenCodeSkillsProvider` | `~/.config/opencode/skills/` |
+
+**Progressive disclosure**: By default, supporting files are hidden from `list_resources()` and accessed via template. Set `supporting_files="resources"` for full enumeration.
+
+Documentation: [Skills Provider](https://gofastmcp.com/v3/servers/providers/skills)
+
+---
+
+### OpenTelemetry Tracing
+
+v3.0 adds OpenTelemetry instrumentation for observability into server and client operations ([#2869](https://github.com/PrefectHQ/fastmcp/pull/2869)).
+
+**Server spans**: Created for tool calls, resource reads, and prompt renders with attributes including component key, provider type, session ID, and auth context.
+
+**Client spans**: Wrap outgoing calls with W3C trace context propagation via request meta.
+
+```python
+# Tracing is passive - configure an OTel SDK to export spans
+from opentelemetry import trace
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace.export import BatchSpanProcessor
+from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
+
+provider = TracerProvider()
+provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
+trace.set_tracer_provider(provider)
+
+# Use fastmcp normally - spans export to your configured backend
+```
+
+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, etc.).
+
+Documentation: [Telemetry](https://gofastmcp.com/v3/servers/telemetry)
+
+---
+
+### Pagination
+
+v3.0 adds pagination support for list operations when servers expose many components ([#2903](https://github.com/PrefectHQ/fastmcp/pull/2903)).
+
+```python
+from fastmcp import FastMCP
+
+# Enable pagination with 50 items per page
+server = FastMCP("ComponentRegistry", list_page_size=50)
+```
+
+When `list_page_size` is set, `tools/list`, `resources/list`, `resources/templates/list`, and `prompts/list` paginate responses with `nextCursor` for subsequent pages.
+
+**Client behavior**: The FastMCP Client fetches all pages automatically—`list_tools()` and similar methods return the complete list. For manual pagination (memory constraints, progress reporting), use `_mcp` variants:
+
+```python
+async with Client(server) as client:
+ result = await client.list_tools_mcp()
+ while result.next_cursor:
+ result = await client.list_tools_mcp(cursor=result.next_cursor)
+```
+
+Documentation: [Pagination](https://gofastmcp.com/v3/servers/pagination)
+
+---
+
+### Composable Lifespans
+
+Lifespans can be combined with the `|` operator for modular setup/teardown ([#2828](https://github.com/PrefectHQ/fastmcp/pull/2828)):
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.lifespan import lifespan
+
+@lifespan
+async def db_lifespan(server):
+ db = await connect_db()
+ try:
+ yield {"db": db}
+ finally:
+ await db.close()
+
+@lifespan
+async def cache_lifespan(server):
+ cache = await connect_cache()
+ try:
+ yield {"cache": cache}
+ finally:
+ await cache.close()
+
+mcp = FastMCP("server", lifespan=db_lifespan | cache_lifespan)
+```
+
+Both enter lifespans in order and exit in reverse (LIFO). Context dicts are merged.
+
+Also adds `combine_lifespans()` utility for FastAPI integration:
+
+```python
+from fastmcp.utilities.lifespan import combine_lifespans
+
+app = FastAPI(lifespan=combine_lifespans(app_lifespan, mcp_app.lifespan))
+```
+
+Documentation: [Lifespan](https://gofastmcp.com/v3/servers/lifespan)
+
+---
+
+### Tool Timeout
+
+Tools can limit foreground execution time with a `timeout` parameter ([#2872](https://github.com/PrefectHQ/fastmcp/pull/2872)):
+
+```python
+@mcp.tool(timeout=30.0)
+async def fetch_data(url: str) -> dict:
+ """Fetch with 30-second timeout."""
+ ...
+```
+
+When exceeded, clients receive MCP error code `-32000`. Both sync and async tools are supported—sync functions run in thread pools so the timeout applies regardless of execution model.
+
+Note: This timeout applies to foreground execution only. Background tasks (`task=True`) execute in Docket workers where this timeout isn't enforced.
+
+---
+
+### PingMiddleware
+
+Sends periodic server-to-client pings to keep long-lived connections alive ([#2838](https://github.com/PrefectHQ/fastmcp/pull/2838)):
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.middleware import PingMiddleware
+
+mcp = FastMCP("server")
+mcp.add_middleware(PingMiddleware(interval_ms=5000))
+```
+
+The middleware starts a background ping task on first message from each session, using the session's existing task group for automatic cleanup when the session ends.
+
+---
+
+### Context.transport Property
+
+Tools can detect which transport is active ([#2850](https://github.com/PrefectHQ/fastmcp/pull/2850)):
+
+```python
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP("example")
+
+@mcp.tool
+def my_tool(ctx: Context) -> str:
+ if ctx.transport == "stdio":
+ return "short response"
+ return "detailed response with more context"
+```
+
+Returns `Literal["stdio", "sse", "streamable-http"]` when running, or `None` outside a server context.
+
+---
+
+### Automatic Threadpool for Sync Functions
+
+Synchronous tools, resources, and prompts now automatically run in a threadpool, preventing event loop blocking during concurrent requests ([#2865](https://github.com/PrefectHQ/fastmcp/pull/2865)):
+
+```python
+import time
+
+@mcp.tool
+def slow_tool():
+ time.sleep(10) # No longer blocks other requests
+ return "done"
+```
+
+Three concurrent calls now execute in parallel (~10s) rather than sequentially (30s). Uses `anyio.to_thread.run_sync()` which properly propagates contextvars, so `Context` and `Depends` continue to work.
+
+---
+
+### CLI Update Notifications
+
+The CLI notifies users when a newer FastMCP version is available on PyPI ([#2840](https://github.com/PrefectHQ/fastmcp/pull/2840)).
+
+**Setting**: `FASTMCP_CHECK_FOR_UPDATES`
+- `"stable"` - Check for stable releases (default)
+- `"prerelease"` - Include alpha/beta/rc versions
+- `"off"` - Disable
+
+12-hour cache, 2-second timeout, fails silently on network errors.
+
+---
+
+### Deprecated Features
+
+These emit deprecation warnings but continue to work.
+
+#### Mount Prefix Parameter
+
+The `prefix` parameter for `mount()` renamed to `namespace`:
+
+```python
+# Deprecated
+main.mount(subserver, prefix="api")
+
+# New
+main.mount(subserver, namespace="api")
+```
+
+#### Tag Filtering, Tool Serializer, Tool Transformations Init Parameters
+
+These constructor parameters have been **removed** (not just deprecated) as of rc1. See "Breaking: Deprecated `FastMCP()` Constructor Kwargs Removed" in the rc1 section above. The `add_tool_transformation()` and `remove_tool_transformation()` methods remain as deprecated shims.
+
+---
+
+### Breaking Changes
+
+#### WSTransport Removed
+
+The deprecated `WSTransport` client transport has been removed ([#2826](https://github.com/PrefectHQ/fastmcp/pull/2826)). Use `StreamableHttpTransport` instead.
+
+#### Decorators Return Functions
+
+Decorators (`@tool`, `@resource`, `@prompt`) now return the original function instead of component objects. Code that treats the decorated function as a `FunctionTool`, `FunctionResource`, or `FunctionPrompt` will break.
+
+```python
+# v2.x
+@mcp.tool
+def greet(name: str) -> str:
+ return f"Hello, {name}!"
+
+isinstance(greet, FunctionTool) # True
+
+# v3.0
+@mcp.tool
+def greet(name: str) -> str:
+ return f"Hello, {name}!"
+
+isinstance(greet, FunctionTool) # False
+callable(greet) # True - it's still your function
+greet("World") # "Hello, World!"
+```
+
+Set `FASTMCP_DECORATOR_MODE=object` or `fastmcp.settings.decorator_mode = "object"` for v2 behavior.
+
+#### Component Enable/Disable Moved to Server/Provider
+
+The `enabled` field and `enable()`/`disable()` methods removed from component objects:
+
+```python
+# v2.x
+tool = await server.get_tool("my_tool")
+tool.disable()
+
+# v3.0
+server.disable(names={"my_tool"}, components=["tool"])
+```
+
+#### Component Lookup Methods
+
+Server lookup and listing methods have updated signatures:
+
+- Parameter names: `get_tool(name=...)`, `get_resource(uri=...)`, etc. (was `key`)
+- Plural listing methods renamed: `get_tools()` → `list_tools()`, `get_resources()` → `list_resources()`, etc.
+- Return types: `list_tools()`, `list_resources()`, etc. return lists instead of dicts
+
+```python
+# v2.x
+tools = await server.get_tools()
+tool = tools["my_tool"]
+
+# v3.0
+tools = await server.list_tools()
+tool = next((t for t in tools if t.name == "my_tool"), None)
+```
+
+#### Prompt Return Types
+
+Prompt functions now use `Message` instead of `mcp.types.PromptMessage`:
+
+```python
+# v2.x
+from mcp.types import PromptMessage, TextContent
+
+@mcp.prompt
+def my_prompt() -> PromptMessage:
+ return PromptMessage(role="user", content=TextContent(type="text", text="Hello"))
+
+# v3.0
+from fastmcp.prompts import Message
+
+@mcp.prompt
+def my_prompt() -> Message:
+ return Message("Hello") # role defaults to "user"
+```
+
+#### Auth Provider Environment Variables Removed
+
+Auth providers no longer auto-load from environment variables ([#2752](https://github.com/PrefectHQ/fastmcp/pull/2752)):
+
+```python
+# v2.x - auto-loaded from FASTMCP_SERVER_AUTH_GITHUB_*
+auth = GitHubProvider()
+
+# v3.0 - explicit configuration
+import os
+auth = GitHubProvider(
+ client_id=os.environ["GITHUB_CLIENT_ID"],
+ client_secret=os.environ["GITHUB_CLIENT_SECRET"],
+)
+```
+
+See `dev-docs/v3-notes/auth-provider-env-vars.md` for rationale.
+
+#### Server Banner Environment Variable
+
+`FASTMCP_SHOW_CLI_BANNER` → `FASTMCP_SHOW_SERVER_BANNER` ([#2771](https://github.com/PrefectHQ/fastmcp/pull/2771))
+
+Now applies to all server startup methods, not just the CLI.
+
+#### Context State Methods Are Async
+
+`ctx.set_state()` and `ctx.get_state()` are now async and session-scoped:
+
+```python
+# v2.x
+ctx.set_state("key", "value")
+value = ctx.get_state("key")
+
+# v3.0
+await ctx.set_state("key", "value")
+value = await ctx.get_state("key")
+```
+
+State now persists across requests within a session. See "Session-Scoped State" above.
diff --git a/v3-notes/visibility.md b/dev-docs/v3-notes/visibility.md
similarity index 100%
rename from v3-notes/visibility.md
rename to dev-docs/v3-notes/visibility.md
diff --git a/dev-docs/v4-notes/background-tasks.md b/dev-docs/v4-notes/background-tasks.md
new file mode 100644
index 000000000..125c8ba73
--- /dev/null
+++ b/dev-docs/v4-notes/background-tasks.md
@@ -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: ` 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).
diff --git a/dev-docs/v4-notes/change-register.md b/dev-docs/v4-notes/change-register.md
new file mode 100644
index 000000000..eb1db6549
--- /dev/null
+++ b/dev-docs/v4-notes/change-register.md
@@ -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)
+
+
+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.
+
+
+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
+
+
+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.
+
+
+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 ` 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 ` 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)
+
+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.` 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`.
diff --git a/dev-docs/v4-notes/feature-program.md b/dev-docs/v4-notes/feature-program.md
new file mode 100644
index 000000000..8c7b9f022
--- /dev/null
+++ b/dev-docs/v4-notes/feature-program.md
@@ -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.
diff --git a/dev-docs/v4-notes/index.md b/dev-docs/v4-notes/index.md
new file mode 100644
index 000000000..282c37af2
--- /dev/null
+++ b/dev-docs/v4-notes/index.md
@@ -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: `), 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.
diff --git a/dev-docs/v4-notes/known-gaps.md b/dev-docs/v4-notes/known-gaps.md
new file mode 100644
index 000000000..0fb699029
--- /dev/null
+++ b/dev-docs/v4-notes/known-gaps.md
@@ -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: ` 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.
diff --git a/dev-docs/v4-notes/protocol-2026.md b/dev-docs/v4-notes/protocol-2026.md
new file mode 100644
index 000000000..fb3ef5428
--- /dev/null
+++ b/dev-docs/v4-notes/protocol-2026.md
@@ -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.
diff --git a/dev-docs/v4-notes/stateless-session-state.md b/dev-docs/v4-notes/stateless-session-state.md
new file mode 100644
index 000000000..5b0bea73c
--- /dev/null
+++ b/dev-docs/v4-notes/stateless-session-state.md
@@ -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`.
diff --git a/docs/apps/architecture.mdx b/docs/apps/architecture.mdx
new file mode 100644
index 000000000..727697094
--- /dev/null
+++ b/docs/apps/architecture.mdx
@@ -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'
+
+
+
+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//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 `_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 `_` 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//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.
diff --git a/docs/apps/demos/bar-chart.py b/docs/apps/demos/bar-chart.py
new file mode 100644
index 000000000..e2430b981
--- /dev/null
+++ b/docs/apps/demos/bar-chart.py
@@ -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,
+ )
diff --git a/docs/apps/demos/contacts.py b/docs/apps/demos/contacts.py
new file mode 100644
index 000000000..0cbe60c0b
--- /dev/null
+++ b/docs/apps/demos/contacts.py
@@ -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")
diff --git a/docs/apps/demos/dashboard.py b/docs/apps/demos/dashboard.py
new file mode 100644
index 000000000..06fe6285d
--- /dev/null
+++ b/docs/apps/demos/dashboard.py
@@ -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,
+ )
diff --git a/docs/apps/demos/data-table.py b/docs/apps/demos/data-table.py
new file mode 100644
index 000000000..5100237bf
--- /dev/null
+++ b/docs/apps/demos/data-table.py
@@ -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,
+ )
diff --git a/docs/apps/demos/hitchhikers.py b/docs/apps/demos/hitchhikers.py
new file mode 100644
index 000000000..1554e5165
--- /dev/null
+++ b/docs/apps/demos/hitchhikers.py
@@ -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 3–4: 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,
+ )
diff --git a/docs/apps/demos/pie-chart.py b/docs/apps/demos/pie-chart.py
new file mode 100644
index 000000000..c1fb489e4
--- /dev/null
+++ b/docs/apps/demos/pie-chart.py
@@ -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,
+ )
diff --git a/docs/apps/demos/reactive.py b/docs/apps/demos/reactive.py
new file mode 100644
index 000000000..16f2f9829
--- /dev/null
+++ b/docs/apps/demos/reactive.py
@@ -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",
+ )
diff --git a/docs/apps/demos/team-directory-reactive.py b/docs/apps/demos/team-directory-reactive.py
new file mode 100644
index 000000000..b6aa004f7
--- /dev/null
+++ b/docs/apps/demos/team-directory-reactive.py
@@ -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"))
diff --git a/docs/apps/demos/team-directory.py b/docs/apps/demos/team-directory.py
new file mode 100644
index 000000000..7cfe21bc9
--- /dev/null
+++ b/docs/apps/demos/team-directory.py
@@ -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,
+ )
diff --git a/docs/apps/development.mdx b/docs/apps/development.mdx
new file mode 100644
index 000000000..f5c683d14
--- /dev/null
+++ b/docs/apps/development.mdx
@@ -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'
+
+
+
+
+
+
+
+`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
+```
diff --git a/docs/apps/examples.mdx b/docs/apps/examples.mdx
new file mode 100644
index 000000000..5078120e7
--- /dev/null
+++ b/docs/apps/examples.mdx
@@ -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'
+
+
+
+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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## 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.
diff --git a/docs/apps/fastmcp-app.mdx b/docs/apps/fastmcp-app.mdx
new file mode 100644
index 000000000..b3facd212
--- /dev/null
+++ b/docs/apps/fastmcp-app.mdx
@@ -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'
+
+
+
+
+
+
+
+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
diff --git a/docs/apps/generative.mdx b/docs/apps/generative.mdx
new file mode 100644
index 000000000..b6293d32b
--- /dev/null
+++ b/docs/apps/generative.mdx
@@ -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'
+
+
+
+
+
+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`
diff --git a/docs/apps/images/app-approval.png b/docs/apps/images/app-approval.png
new file mode 100644
index 000000000..162f4847f
Binary files /dev/null and b/docs/apps/images/app-approval.png differ
diff --git a/docs/apps/images/app-chart.png b/docs/apps/images/app-chart.png
new file mode 100644
index 000000000..cfc816d0e
Binary files /dev/null and b/docs/apps/images/app-chart.png differ
diff --git a/docs/apps/images/app-choice.png b/docs/apps/images/app-choice.png
new file mode 100644
index 000000000..178f6a2b0
Binary files /dev/null and b/docs/apps/images/app-choice.png differ
diff --git a/docs/apps/images/app-contacts.png b/docs/apps/images/app-contacts.png
new file mode 100644
index 000000000..5d74f7cb9
Binary files /dev/null and b/docs/apps/images/app-contacts.png differ
diff --git a/src/fastmcp/client/sampling/handlers/__init__.py b/docs/apps/images/app-datatable.png
similarity index 100%
rename from src/fastmcp/client/sampling/handlers/__init__.py
rename to docs/apps/images/app-datatable.png
diff --git a/docs/apps/images/app-example-map.png b/docs/apps/images/app-example-map.png
new file mode 100644
index 000000000..5859c59c2
Binary files /dev/null and b/docs/apps/images/app-example-map.png differ
diff --git a/docs/apps/images/app-example-quiz.png b/docs/apps/images/app-example-quiz.png
new file mode 100644
index 000000000..b16bcaf43
Binary files /dev/null and b/docs/apps/images/app-example-quiz.png differ
diff --git a/docs/apps/images/app-example-sales-dashboard.png b/docs/apps/images/app-example-sales-dashboard.png
new file mode 100644
index 000000000..e0fe709a9
Binary files /dev/null and b/docs/apps/images/app-example-sales-dashboard.png differ
diff --git a/docs/apps/images/app-example-system-dashboard.png b/docs/apps/images/app-example-system-dashboard.png
new file mode 100644
index 000000000..7b85d7ac1
Binary files /dev/null and b/docs/apps/images/app-example-system-dashboard.png differ
diff --git a/docs/apps/images/app-file-upload.png b/docs/apps/images/app-file-upload.png
new file mode 100644
index 000000000..1178c09af
Binary files /dev/null and b/docs/apps/images/app-file-upload.png differ
diff --git a/docs/apps/images/app-form.png b/docs/apps/images/app-form.png
new file mode 100644
index 000000000..30567e37e
Binary files /dev/null and b/docs/apps/images/app-form.png differ
diff --git a/docs/apps/images/app-greet.png b/docs/apps/images/app-greet.png
new file mode 100644
index 000000000..70a0e4412
Binary files /dev/null and b/docs/apps/images/app-greet.png differ
diff --git a/docs/apps/images/app-overview.png b/docs/apps/images/app-overview.png
new file mode 100644
index 000000000..35f68fd58
Binary files /dev/null and b/docs/apps/images/app-overview.png differ
diff --git a/docs/apps/images/app-quickstart-dev-2.png b/docs/apps/images/app-quickstart-dev-2.png
new file mode 100644
index 000000000..f04d96d72
Binary files /dev/null and b/docs/apps/images/app-quickstart-dev-2.png differ
diff --git a/docs/apps/images/app-quickstart-dev.png b/docs/apps/images/app-quickstart-dev.png
new file mode 100644
index 000000000..d043f0ed3
Binary files /dev/null and b/docs/apps/images/app-quickstart-dev.png differ
diff --git a/docs/apps/images/app-quickstart.png b/docs/apps/images/app-quickstart.png
new file mode 100644
index 000000000..ddca745cf
Binary files /dev/null and b/docs/apps/images/app-quickstart.png differ
diff --git a/docs/apps/images/app-showcase.png b/docs/apps/images/app-showcase.png
new file mode 100644
index 000000000..c03294bdb
Binary files /dev/null and b/docs/apps/images/app-showcase.png differ
diff --git a/docs/apps/images/dev-app.png b/docs/apps/images/dev-app.png
new file mode 100644
index 000000000..fdb05d69e
Binary files /dev/null and b/docs/apps/images/dev-app.png differ
diff --git a/docs/apps/images/generative-ui.mp4 b/docs/apps/images/generative-ui.mp4
new file mode 100644
index 000000000..ca610181e
Binary files /dev/null and b/docs/apps/images/generative-ui.mp4 differ
diff --git a/docs/apps/low-level.mdx b/docs/apps/low-level.mdx
index 944e48498..0cd1b0ea1 100644
--- a/docs/apps/low-level.mdx
+++ b/docs/apps/low-level.mdx
@@ -3,18 +3,17 @@ title: Custom HTML Apps
sidebarTitle: Custom HTML
description: Build apps with your own HTML, CSS, and JavaScript using the MCP Apps extension directly.
icon: code
-tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
-The [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) is an open protocol that lets tools return interactive UIs — an HTML page rendered in a sandboxed iframe inside the host client. [Prefab UI](/apps/prefab) builds on this protocol so you never have to think about it, but when you need full control — custom rendering, a specific JavaScript framework, maps, 3D, video — you can use the MCP Apps extension directly.
+Everything on this page is for when you want full control: your own HTML, your own JavaScript framework, a map library, a 3D viewer, custom video playback. [Interactive Tools](/apps/prefab) wrap the MCP Apps extension so you never have to think about it — this page is what you reach for when you need to think about it.
-This page covers how to write custom HTML apps and wire them up in FastMCP. You'll be working with the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) JavaScript SDK for host communication, and FastMCP's `AppConfig` for resource and CSP management.
+You'll be working with two things: the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) JavaScript SDK for host communication, and FastMCP's `AppConfig` for resources and CSP.
-## How It Works
+## How it works
An MCP App has two parts:
@@ -27,7 +26,7 @@ The tool declares which resource to use via `AppConfig`. When the host calls the
import json
from fastmcp import FastMCP
-from fastmcp.server.apps import AppConfig, ResourceCSP
+from fastmcp.apps import AppConfig, ResourceCSP
mcp = FastMCP("My App Server")
@@ -44,10 +43,10 @@ def chart_view() -> str:
## AppConfig
-`AppConfig` controls how a tool or resource participates in the Apps extension. Import it from `fastmcp.server.apps`:
+`AppConfig` controls how a tool or resource participates in the Apps extension. Import it from `fastmcp.apps`:
```python
-from fastmcp.server.apps import AppConfig
+from fastmcp.apps import AppConfig
```
On **tools**, you'll typically set `resource_uri` to point to the UI resource:
@@ -66,16 +65,20 @@ def my_tool() -> str:
return "result"
```
-### Tool Visibility
+### Tool visibility
The `visibility` field controls where a tool appears:
- `["model"]` — visible to the LLM (the default behavior)
-- `["app"]` — only callable from within the app UI, hidden from the LLM
+- `["app"]` — callable from within the app UI, kept out of the LLM's tool list
- `["model", "app"]` — both
This is useful when you have tools that only make sense as part of the app's interactive flow, not as standalone LLM actions.
+Visibility is a declaration, and on `tools/list` the host does the filtering — the division the MCP Apps specification defines. Every tool is advertised carrying its `visibility` metadata, which is also what lets a proxy or gateway forward it: an intermediary can only route to a tool it can see.
+
+That division assumes a host stands between the server and the model. Where one doesn't, FastMCP applies the declaration itself. [Tool search](/servers/transforms/tool-search) and code mode reach the model as ordinary tool output rather than as an advertised listing, and their call-tool proxies execute a name the model supplies — nothing downstream can filter either, so app-only tools are excluded from both. The app's own UI still reaches its backends, because a UI calling by identity is not the model.
+
```python
@mcp.tool(
app=AppConfig(
@@ -88,7 +91,7 @@ def refresh_data() -> str:
return fetch_latest()
```
-### AppConfig Fields
+### AppConfig fields
| Field | Type | Description |
|-------|------|-------------|
@@ -103,9 +106,9 @@ def refresh_data() -> str:
On **resources**, `resource_uri` and `visibility` must not be set — the resource *is* the UI. Use `AppConfig` on resources only for `csp`, `permissions`, and other display settings.
-## UI Resources
+## UI resources
-Resources using the `ui://` scheme are automatically served with the MIME type `text/html;profile=mcp-app`. You don't need to set this manually.
+Resources using the `ui://` scheme are automatically served with the MIME type `text/html;profile=mcp-app`. No need to set it manually.
```python
@mcp.resource("ui://my-app/view.html")
@@ -115,7 +118,7 @@ def my_view() -> str:
The HTML can be anything — a full single-page app, a simple display, or a complex interactive tool. The host renders it in a sandboxed iframe and establishes a `postMessage` channel for communication.
-### Writing the App HTML
+### Writing the app HTML
Your HTML app communicates with the host using the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) JavaScript SDK. The simplest approach is to load it from a CDN:
@@ -145,6 +148,8 @@ The `App` object provides:
- **`app.onhostcontextchanged`** — callback for host context changes (e.g., safe area insets)
- **`app.getHostContext()`** — get current host context
+See the full [ext-apps SDK documentation](https://github.com/modelcontextprotocol/ext-apps) for the complete API reference.
+
If your HTML loads external scripts, styles, or makes API calls, you need to declare those domains in the CSP configuration. See [Security](#security) below.
@@ -158,7 +163,7 @@ Apps run in sandboxed iframes with a deny-by-default Content Security Policy. By
If your app needs to load external resources (CDN scripts, API calls, embedded iframes), declare the allowed domains with `ResourceCSP`:
```python
-from fastmcp.server.apps import AppConfig, ResourceCSP
+from fastmcp.apps import AppConfig, ResourceCSP
@mcp.resource(
"ui://my-app/view.html",
@@ -185,7 +190,7 @@ def my_view() -> str:
If your app needs browser capabilities like camera or clipboard access, request them via `ResourcePermissions`:
```python
-from fastmcp.server.apps import AppConfig, ResourcePermissions
+from fastmcp.apps import AppConfig, ResourcePermissions
@mcp.resource(
"ui://my-app/view.html",
@@ -202,7 +207,7 @@ def my_view() -> str:
Hosts may or may not grant these permissions. Your app should use JavaScript feature detection as a fallback.
-## Example: QR Code Server
+## Example: a QR code server
This example creates a tool that generates QR codes and an app that renders them as images. It's based on the [official MCP Apps example](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server). Requires the `qrcode[pil]` package.
@@ -211,11 +216,11 @@ import base64
import io
import qrcode
-from mcp import types
from fastmcp import FastMCP
-from fastmcp.server.apps import AppConfig, ResourceCSP
+from fastmcp.apps import AppConfig, ResourceCSP
from fastmcp.tools import ToolResult
+from mcp.types import ImageContent
mcp = FastMCP("QR Code Server")
@@ -235,7 +240,7 @@ def generate_qr(text: str = "https://gofastmcp.com") -> ToolResult:
b64 = base64.b64encode(buffer.getvalue()).decode()
return ToolResult(
- content=[types.ImageContent(type="image", data=b64, mimeType="image/png")]
+ content=[ImageContent(type="image", data=b64, mime_type="image/png")]
)
@@ -284,13 +289,13 @@ def view() -> str:
The tool generates a QR code as a base64 PNG. The resource loads the MCP Apps JS SDK from unpkg (declared in the CSP), listens for tool results, and renders the image. The host wires them together — when the LLM calls `generate_qr`, the QR code appears in an interactive frame inside the conversation.
-## Checking Client Support
+## Checking client support
Not all hosts support the Apps extension. You can check at runtime using the tool's [context](/servers/context):
```python
from fastmcp import Context
-from fastmcp.server.apps import AppConfig, UI_EXTENSION_ID
+from fastmcp.apps import AppConfig, UI_EXTENSION_ID
@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
async def my_tool(ctx: Context) -> str:
diff --git a/docs/apps/overview.mdx b/docs/apps/overview.mdx
index d8dcfc3fd..ff9557058 100644
--- a/docs/apps/overview.mdx
+++ b/docs/apps/overview.mdx
@@ -3,74 +3,71 @@ title: Apps
sidebarTitle: Overview
description: Give your tools interactive UIs rendered directly in the conversation.
icon: grid-2
-tag: NEW
+mode: center
---
import { VersionBadge } from '/snippets/version-badge.mdx'
+import PrefabPinWarning from '/snippets/prefab-pin-warning.mdx'
+import { PrefabDemoFrame } from '/snippets/prefab-demo-frame.mdx'
-MCP Apps let your tools return interactive UIs — rendered in a sandboxed iframe right inside the host client's conversation. Instead of returning plain text, a tool can show a chart, a sortable table, a form, or anything you can build with HTML.
+A FastMCP app is a tool that returns an interactive UI instead of text. When the host calls it, the user sees a chart, a table, a form, or a whole dashboard rendered right inside the conversation, with working sort, search, tooltips, and state.
-FastMCP implements the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) and provides two approaches:
+
-## Prefab Apps (Recommended)
+The dashboard above is a [Prefab](https://prefab.prefect.io) showcase — a taste of what you can deliver from a FastMCP tool. Every card, chart, slider, dialog, and carousel is a Python component. Build a composition like this, add `@mcp.tool(app=True)`, and the host renders it inside the conversation.
-
+Under the hood, FastMCP builds on the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) and uses Prefab to describe UIs in Python.
-
-[Prefab](https://prefab.prefect.io) is in extremely early, active development — its API changes frequently and breaking changes can occur with any release. The FastMCP integration is equally new and under rapid development. These docs are included for users who want to work on the cutting edge; production use is not recommended. Always [pin `prefab-ui` to a specific version](/apps/prefab#getting-started) in your dependencies.
-
+```bash
+pip install "fastmcp[apps]"
+```
-[Prefab UI](https://prefab.prefect.io) is a declarative UI framework for Python. You describe layouts, charts, tables, forms, and interactive behaviors using a Python DSL — and the framework compiles them to a JSON protocol that a shared renderer interprets. It started as a component library inside FastMCP and grew into its own framework with [comprehensive documentation](https://prefab.prefect.io).
+
+
+## Pick your path
+
+Four patterns cover almost everything you'd want to build. Most apps start with Interactive Tools; you only reach for the others when you've hit a specific limit.
+
+### [Interactive Tools](/apps/prefab) — start here
+
+Add `app=True` to a tool and return a Prefab component. Charts, tables, dashboards, and client-side interactivity (toggles, tabs, filtering) all work without any server round-trips.
```python
-from prefab_ui.components import Column, Heading, BarChart, ChartSeries
-from prefab_ui.app import PrefabApp
-from fastmcp import FastMCP
-
-mcp = FastMCP("Dashboard")
-
@mcp.tool(app=True)
-def sales_chart(year: int) -> PrefabApp:
- """Show sales data as an interactive chart."""
- data = get_sales_data(year)
-
- with Column(gap=4, css_class="p-6") as view:
- Heading(f"{year} Sales")
- BarChart(
- data=data,
- series=[ChartSeries(data_key="revenue", label="Revenue")],
- x_axis="month",
- )
-
- return PrefabApp(view=view)
+def team_directory() -> DataTable:
+ return DataTable(columns=[...], rows=employees, search=True)
```
-Install with `pip install "fastmcp[apps]"` and see [Prefab Apps](/apps/prefab) for the integration guide.
+### [FastMCPApp](/apps/fastmcp-app) — when the UI calls back to the server
-## Custom HTML Apps
+Forms that save data, buttons that trigger backend work, search that hits a database. `FastMCPApp` manages the wiring between UI actions and backend tools, with stable tool identifiers that survive server composition.
-The [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) is an open protocol, and you can use it directly when you need full control. You write your own HTML/CSS/JavaScript and communicate with the host via the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) SDK.
+### [Generative UI](/apps/generative) — when the LLM writes the UI
-This is the right choice for custom rendering (maps, 3D, video), specific JavaScript frameworks, or capabilities beyond what the component library offers.
+Register one provider and the model can write Prefab code tailored to the current data and request. The user watches the UI build up as the model generates it.
```python
-from fastmcp import FastMCP
-from fastmcp.server.apps import AppConfig, ResourceCSP
-
-mcp = FastMCP("Custom App")
-
-@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
-def my_tool() -> str:
- return '{"values": [1, 2, 3]}'
-
-@mcp.resource(
- "ui://my-app/view.html",
- app=AppConfig(csp=ResourceCSP(resource_domains=["https://unpkg.com"])),
-)
-def view() -> str:
- return "..."
+mcp.add_provider(GenerativeUI())
```
-See [Custom HTML Apps](/apps/low-level) for the full reference.
+### [Custom HTML](/apps/low-level) — when you need full control
+
+Write your own HTML, CSS, and JavaScript. Use a specific framework, drop in a map or 3D viewer, embed video. You're talking to the MCP Apps protocol directly.
+
+## What's next
+
+- **[Quickstart](/apps/quickstart)** — build a working app in a minute
+- **[Examples](/apps/examples)** — complete working servers you can run today
+- **[Providers](/apps/providers/approval)** — ready-made capabilities (approvals, choice pickers, file upload, forms) you add with one line
+- **[Development](/apps/development)** — preview app tools locally with `fastmcp dev apps`
diff --git a/docs/apps/patterns.mdx b/docs/apps/patterns.mdx
deleted file mode 100644
index ffc699f89..000000000
--- a/docs/apps/patterns.mdx
+++ /dev/null
@@ -1,483 +0,0 @@
----
-title: Patterns
-sidebarTitle: Patterns
-description: Charts, tables, forms, and other common tool UIs.
-icon: grid-2-plus
-tag: SOON
----
-
-import { VersionBadge } from '/snippets/version-badge.mdx'
-
-
-
-
-[Prefab](https://prefab.prefect.io) is in extremely early, active development — its API changes frequently and breaking changes can occur with any release. The FastMCP integration is equally new and under rapid development. These docs are included for users who want to work on the cutting edge; production use is not recommended. Always pin `prefab-ui` to a specific version in your dependencies.
-
-
-The most common use of Prefab is giving your tools a visual representation — a chart instead of raw numbers, a sortable table instead of a text dump, a status dashboard instead of a list of booleans. Each pattern below is a complete, copy-pasteable tool.
-
-## Charts
-
-Prefab includes [bar, line, area, pie, radar, and radial charts](https://prefab.prefect.io/docs/components/charts). They all render client-side with tooltips, legends, and responsive sizing.
-
-### Bar Chart
-
-```python
-from prefab_ui.components import Column, Heading, BarChart, ChartSeries
-from prefab_ui.app import PrefabApp
-from fastmcp import FastMCP
-
-mcp = FastMCP("Charts")
-
-
-@mcp.tool(app=True)
-def quarterly_revenue(year: int) -> PrefabApp:
- """Show quarterly revenue as a bar chart."""
- 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 Column(gap=4, css_class="p-6") as view:
- Heading(f"{year} Revenue vs Costs")
- BarChart(
- data=data,
- series=[
- ChartSeries(data_key="revenue", label="Revenue"),
- ChartSeries(data_key="costs", label="Costs"),
- ],
- x_axis="quarter",
- show_legend=True,
- )
-
- return PrefabApp(view=view)
-```
-
-Multiple `ChartSeries` entries plot different data keys. Add `stacked=True` to stack bars, or `horizontal=True` to flip the axes.
-
-### Area Chart
-
-`LineChart` and `AreaChart` share the same API as `BarChart`, with `curve` for interpolation (`"linear"`, `"smooth"`, `"step"`) and `show_dots` for data points:
-
-```python
-from prefab_ui.components import Column, Heading, AreaChart, ChartSeries
-from prefab_ui.app import PrefabApp
-from fastmcp import FastMCP
-
-mcp = FastMCP("Charts")
-
-
-@mcp.tool(app=True)
-def usage_trend() -> PrefabApp:
- """Show API usage over time."""
- data = [
- {"date": "Feb 1", "requests": 1200},
- {"date": "Feb 2", "requests": 1350},
- {"date": "Feb 3", "requests": 980},
- {"date": "Feb 4", "requests": 1500},
- {"date": "Feb 5", "requests": 1420},
- ]
-
- with Column(gap=4, css_class="p-6") as view:
- Heading("API Usage")
- AreaChart(
- data=data,
- series=[ChartSeries(data_key="requests", label="Requests")],
- x_axis="date",
- curve="smooth",
- height=250,
- )
-
- return PrefabApp(view=view)
-```
-
-### Pie and Donut Charts
-
-`PieChart` uses `data_key` (the numeric value) and `name_key` (the label) instead of series. Set `inner_radius` for a donut:
-
-```python
-from prefab_ui.components import Column, Heading, PieChart
-from prefab_ui.app import PrefabApp
-from fastmcp import FastMCP
-
-mcp = FastMCP("Charts")
-
-
-@mcp.tool(app=True)
-def ticket_breakdown() -> PrefabApp:
- """Show open tickets by category."""
- data = [
- {"category": "Bug", "count": 23},
- {"category": "Feature", "count": 15},
- {"category": "Docs", "count": 8},
- {"category": "Infra", "count": 12},
- ]
-
- with Column(gap=4, css_class="p-6") as view:
- Heading("Open Tickets")
- PieChart(
- data=data,
- data_key="count",
- name_key="category",
- show_legend=True,
- inner_radius=60,
- )
-
- return PrefabApp(view=view)
-```
-
-## Data Tables
-
-[DataTable](https://prefab.prefect.io/docs/components/data-display/data-table) provides sortable columns, full-text search, and pagination — all running client-side in the browser.
-
-```python
-from prefab_ui.components import Column, Heading, DataTable, DataTableColumn
-from prefab_ui.app import PrefabApp
-from fastmcp import FastMCP
-
-mcp = FastMCP("Directory")
-
-
-@mcp.tool(app=True)
-def employee_directory() -> PrefabApp:
- """Show a searchable, sortable employee directory."""
- employees = [
- {"name": "Alice Chen", "department": "Engineering", "role": "Staff Engineer", "location": "SF"},
- {"name": "Bob Martinez", "department": "Design", "role": "Lead Designer", "location": "NYC"},
- {"name": "Carol Johnson", "department": "Engineering", "role": "Senior Engineer", "location": "London"},
- {"name": "David Kim", "department": "Product", "role": "Product Manager", "location": "SF"},
- {"name": "Eva Müller", "department": "Engineering", "role": "Engineer", "location": "Berlin"},
- ]
-
- with Column(gap=4, css_class="p-6") as view:
- Heading("Employee Directory")
- DataTable(
- columns=[
- DataTableColumn(key="name", header="Name", sortable=True),
- DataTableColumn(key="department", header="Department", sortable=True),
- DataTableColumn(key="role", header="Role"),
- DataTableColumn(key="location", header="Office", sortable=True),
- ],
- rows=employees,
- searchable=True,
- paginated=True,
- page_size=15,
- )
-
- return PrefabApp(view=view)
-```
-
-## Forms
-
-A form collects input, but it needs somewhere to send that input. The [`CallTool`](https://prefab.prefect.io/docs/concepts/actions) action connects a form to a tool on your MCP server — so you need two tools: one that renders the form, and one that handles the submission.
-
-```python
-from prefab_ui.components import (
- Column, Heading, Row, Muted, Badge, Input, Select,
- Textarea, Button, Form, ForEach, Separator,
-)
-from prefab_ui.actions import ShowToast
-from prefab_ui.actions.mcp import CallTool
-from prefab_ui.app import PrefabApp
-from fastmcp import FastMCP
-
-mcp = FastMCP("Contacts")
-
-contacts_db: list[dict] = [
- {"name": "Zaphod Beeblebrox", "email": "zaphod@galaxy.gov", "category": "Partner"},
-]
-
-
-@mcp.tool(app=True)
-def contact_form() -> PrefabApp:
- """Show a contact list with a form to add new contacts."""
- with Column(gap=6, css_class="p-6") as view:
- Heading("Contacts")
-
- with ForEach("contacts"):
- with Row(gap=2, align="center"):
- Muted("{{ name }}")
- Muted("{{ email }}")
- Badge("{{ category }}")
-
- Separator()
-
- with Form(
- on_submit=CallTool(
- "save_contact",
- result_key="contacts",
- on_success=ShowToast("Contact saved!", variant="success"),
- on_error=ShowToast("{{ $error }}", variant="error"),
- )
- ):
- Input(name="name", label="Full Name", required=True)
- Input(name="email", label="Email", input_type="email", required=True)
- Select(
- name="category",
- label="Category",
- options=["Customer", "Vendor", "Partner", "Other"],
- )
- Textarea(name="notes", label="Notes", placeholder="Optional notes...")
- Button("Save Contact")
-
- return PrefabApp(view=view, state={"contacts": list(contacts_db)})
-
-
-@mcp.tool
-def save_contact(
- name: str,
- email: str,
- category: str = "Other",
- notes: str = "",
-) -> list[dict]:
- """Save a new contact and return the updated list."""
- contacts_db.append({"name": name, "email": email, "category": category, "notes": notes})
- return list(contacts_db)
-```
-
-When the user submits the form, the renderer calls `save_contact` on the server with all named input values as arguments. Because `result_key="contacts"` is set, the returned list replaces the `contacts` state — and the `ForEach` re-renders with the new data automatically.
-
-The `save_contact` tool is a regular MCP tool. The LLM can also call it directly in conversation. Your UI actions and your conversational tools are the same thing.
-
-### Pydantic Model Forms
-
-For complex forms, `Form.from_model()` generates the entire form from a Pydantic model — inputs, labels, validation, and submit wiring:
-
-```python
-from typing import Literal
-
-from pydantic import BaseModel, Field
-from prefab_ui.components import Column, Heading, Form
-from prefab_ui.actions.mcp import CallTool
-from prefab_ui.app import PrefabApp
-from fastmcp import FastMCP
-
-mcp = FastMCP("Bug Tracker")
-
-
-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")
- steps_to_reproduce: str = Field(title="Steps to Reproduce")
-
-
-@mcp.tool(app=True)
-def report_bug() -> PrefabApp:
- """Show a bug report form."""
- with Column(gap=4, css_class="p-6") as view:
- Heading("Report a Bug")
- Form.from_model(BugReport, on_submit=CallTool("create_bug_report"))
-
- return PrefabApp(view=view)
-
-
-@mcp.tool
-def create_bug_report(data: dict) -> str:
- """Create a bug report from the form submission."""
- report = BugReport(**data)
- # save to database...
- return f"Created bug report: {report.title}"
-```
-
-`str` fields become text inputs, `Literal` becomes a select, `bool` becomes a checkbox. The `on_submit` CallTool receives all field values under a `data` key.
-
-## Status Displays
-
-Cards, badges, progress bars, and grids combine naturally for dashboards. See the [Prefab layout](https://prefab.prefect.io/docs/concepts/composition) and [container](https://prefab.prefect.io/docs/components/containers) docs for the full set of layout and display components.
-
-```python
-from prefab_ui.components import (
- Column, Row, Grid, Heading, Text, Muted, Badge,
- Card, CardContent, Progress, Separator,
-)
-from prefab_ui.app import PrefabApp
-from fastmcp import FastMCP
-
-mcp = FastMCP("Monitoring")
-
-
-@mcp.tool(app=True)
-def system_status() -> PrefabApp:
- """Show current system health."""
- services = [
- {"name": "API Gateway", "status": "healthy", "ok": True, "latency_ms": 12, "uptime_pct": 99.9},
- {"name": "Database", "status": "healthy", "ok": True, "latency_ms": 3, "uptime_pct": 99.99},
- {"name": "Cache", "status": "degraded", "ok": False, "latency_ms": 45, "uptime_pct": 98.2},
- {"name": "Queue", "status": "healthy", "ok": True, "latency_ms": 8, "uptime_pct": 99.8},
- ]
- all_ok = all(s["ok"] for s in services)
-
- with Column(gap=4, css_class="p-6") as view:
- with Row(gap=2, align="center"):
- Heading("System Status")
- Badge(
- "All Healthy" if all_ok else "Degraded",
- variant="success" if all_ok else "destructive",
- )
-
- Separator()
-
- with Grid(columns=2, gap=4):
- for svc in services:
- with Card():
- with CardContent():
- with Row(gap=2, align="center"):
- Text(svc["name"], css_class="font-medium")
- Badge(
- svc["status"],
- variant="success" if svc["ok"] else "destructive",
- )
- Muted(f"Response: {svc['latency_ms']}ms")
- Progress(value=svc["uptime_pct"])
-
- return PrefabApp(view=view)
-```
-
-## Conditional Content
-
-[`If`, `Elif`, and `Else`](https://prefab.prefect.io/docs/concepts/composition#conditional-rendering) show or hide content based on state. Changes are instant — no server round-trip.
-
-```python
-from prefab_ui.components import Column, Heading, Switch, Separator, Alert, If
-from prefab_ui.app import PrefabApp
-from fastmcp import FastMCP
-
-mcp = FastMCP("Flags")
-
-
-@mcp.tool(app=True)
-def feature_flags() -> PrefabApp:
- """Toggle feature flags with live preview."""
- with Column(gap=4, css_class="p-6") as view:
- Heading("Feature Flags")
-
- Switch(name="dark_mode", label="Dark Mode")
- Switch(name="beta_features", label="Beta Features")
-
- Separator()
-
- with If("{{ dark_mode }}"):
- Alert(title="Dark mode enabled", description="UI will use dark theme.")
- with If("{{ beta_features }}"):
- Alert(
- title="Beta features active",
- description="Experimental features are now visible.",
- variant="warning",
- )
-
- return PrefabApp(view=view, state={"dark_mode": False, "beta_features": False})
-```
-
-## Tabs
-
-[Tabs](https://prefab.prefect.io/docs/components/containers/tabs) organize content into switchable views. Switching is client-side — no server round-trip.
-
-```python
-from prefab_ui.components import (
- Column, Heading, Text, Muted, Badge, Row,
- DataTable, DataTableColumn, Tabs, Tab, ForEach,
-)
-from prefab_ui.app import PrefabApp
-from fastmcp import FastMCP
-
-mcp = FastMCP("Projects")
-
-
-@mcp.tool(app=True)
-def project_overview(project_id: str) -> PrefabApp:
- """Show project details organized in tabs."""
- project = {
- "name": "FastMCP v3",
- "description": "Next generation MCP framework with Apps support.",
- "status": "Active",
- "created_at": "2025-01-15",
- "members": [
- {"name": "Alice Chen", "role": "Lead"},
- {"name": "Bob Martinez", "role": "Design"},
- ],
- "activity": [
- {"timestamp": "2 hours ago", "message": "Merged PR #342"},
- {"timestamp": "1 day ago", "message": "Released v3.0.1"},
- ],
- }
-
- with Column(gap=4, css_class="p-6") as view:
- Heading(project["name"])
-
- with Tabs():
- with Tab("Overview"):
- Text(project["description"])
- with Row(gap=4):
- Badge(project["status"])
- Muted(f"Created: {project['created_at']}")
-
- with Tab("Members"):
- DataTable(
- columns=[
- DataTableColumn(key="name", header="Name", sortable=True),
- DataTableColumn(key="role", header="Role"),
- ],
- rows=project["members"],
- )
-
- with Tab("Activity"):
- with ForEach("activity"):
- with Row(gap=2):
- Muted("{{ timestamp }}")
- Text("{{ message }}")
-
- return PrefabApp(view=view, state={"activity": project["activity"]})
-```
-
-## Accordion
-
-[Accordion](https://prefab.prefect.io/docs/components/containers/accordion) collapses sections to save space. `multiple=True` lets users expand several items at once:
-
-```python
-from prefab_ui.components import (
- Column, Heading, Row, Text, Badge, Progress,
- Accordion, AccordionItem,
-)
-from prefab_ui.app import PrefabApp
-from fastmcp import FastMCP
-
-mcp = FastMCP("API Monitor")
-
-
-@mcp.tool(app=True)
-def api_health() -> PrefabApp:
- """Show health details for each API endpoint."""
- endpoints = [
- {"path": "/api/users", "status": 200, "healthy": True, "avg_ms": 45, "p99_ms": 120, "uptime_pct": 99.9},
- {"path": "/api/orders", "status": 200, "healthy": True, "avg_ms": 82, "p99_ms": 250, "uptime_pct": 99.7},
- {"path": "/api/search", "status": 200, "healthy": True, "avg_ms": 150, "p99_ms": 500, "uptime_pct": 99.5},
- {"path": "/api/webhooks", "status": 503, "healthy": False, "avg_ms": 2000, "p99_ms": 5000, "uptime_pct": 95.1},
- ]
-
- with Column(gap=4, css_class="p-6") as view:
- Heading("API Health")
-
- with Accordion(multiple=True):
- for ep in endpoints:
- with AccordionItem(ep["path"]):
- with Row(gap=4):
- Badge(
- f"{ep['status']}",
- variant="success" if ep["healthy"] else "destructive",
- )
- Text(f"Avg: {ep['avg_ms']}ms")
- Text(f"P99: {ep['p99_ms']}ms")
- Progress(value=ep["uptime_pct"])
-
- return PrefabApp(view=view)
-```
-
-## Next Steps
-
-- **[Custom HTML Apps](/apps/low-level)** — When you need your own HTML, CSS, and JavaScript
-- **[Prefab UI Docs](https://prefab.prefect.io)** — Components, state, expressions, and actions
diff --git a/docs/apps/prefab.mdx b/docs/apps/prefab.mdx
index 907670d4b..e6ff7070f 100644
--- a/docs/apps/prefab.mdx
+++ b/docs/apps/prefab.mdx
@@ -1,145 +1,282 @@
---
-title: Prefab Apps
-sidebarTitle: Prefab Apps
-description: Build interactive tool UIs in pure Python — no HTML or JavaScript required.
+title: Interactive Tools
+sidebarTitle: Interactive Tools
+description: Turn your tools into interactive UIs with charts, tables, and dashboards.
icon: palette
-tag: SOON
+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'
-
-[Prefab](https://prefab.prefect.io) is in extremely early, active development — its API changes frequently and breaking changes can occur with any release. The FastMCP integration is equally new and under rapid development. These docs are included for users who want to work on the cutting edge; production use is not recommended. Always pin `prefab-ui` to a specific version in your dependencies (see below).
-
+
-[Prefab UI](https://prefab.prefect.io) is a declarative UI framework for Python. You describe what your interface should look like — a chart, a table, a form — and return it from your tool. FastMCP takes care of everything else: registering the renderer, wiring the protocol metadata, and delivering the component tree to the host.
+
-Prefab started as a component library inside FastMCP and grew into a full framework for building interactive applications — with its own state management, reactive expression system, and action model. The [Prefab documentation](https://prefab.prefect.io) covers all of this in depth. This page focuses on the FastMCP integration: what you return from a tool, and what FastMCP does with it.
+Believe it or not, that dashboard is a FastMCP tool. The chart has tooltips. The table is sortable. The badges are styled by deal stage. The whole thing is about 40 lines of Python, and the user sees it right inside their conversation instead of a wall of JSON.
-```bash
-pip install "fastmcp[apps]"
-```
+The pattern behind every example on this page is the same: add `app=True` to your tool, build a UI with [Prefab](https://prefab.prefect.io) components, and return it as a `PrefabApp`. Prefab has [100+ components](https://prefab.prefect.io/docs/components), from data tables and charts to forms and progress bars. You compose them in Python; the host renders them as a live, interactive application.
-
-Prefab UI is in active early development and its API changes frequently. We strongly recommend pinning `prefab-ui` to a specific version in your project's dependencies. Installing `fastmcp[apps]` pulls in `prefab-ui` but won't pin it — so a routine `pip install --upgrade` could introduce breaking changes.
+## Start with a table
-```toml
-# pyproject.toml
-dependencies = [
- "fastmcp[apps]",
- "prefab-ui==0.8.0", # pin to a known working version
-]
-```
-
+Most tools return data the user wants to explore. A `DataTable` is often the smallest useful upgrade — your data goes from a JSON blob to a searchable, sortable table:
-Here's the simplest possible Prefab App — a tool that returns a bar chart:
+
```python
-from prefab_ui.components import Column, Heading, BarChart, ChartSeries
-from prefab_ui.app import PrefabApp
+from prefab_ui.components import DataTable, DataTableColumn
from fastmcp import FastMCP
-mcp = FastMCP("Dashboard")
+mcp = FastMCP("Directory")
@mcp.tool(app=True)
-def revenue_chart(year: int) -> PrefabApp:
- """Show annual revenue as an interactive bar chart."""
- data = [
- {"quarter": "Q1", "revenue": 42000},
- {"quarter": "Q2", "revenue": 51000},
- {"quarter": "Q3", "revenue": 47000},
- {"quarter": "Q4", "revenue": 63000},
+def team_directory() -> DataTable:
+ """Browse the team directory."""
+ 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 Column(gap=4, css_class="p-6") as view:
- Heading(f"{year} Revenue")
- BarChart(
- data=data,
- series=[ChartSeries(data_key="revenue", label="Revenue")],
- x_axis="quarter",
- )
-
- return PrefabApp(view=view)
+ return 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,
+ )
```
-That's it — you declare a layout using Python's `with` statement, and return it. When the host calls this tool, the user sees an interactive bar chart instead of a JSON blob. The [Patterns](/apps/patterns) page has more examples: area charts, data tables, forms, status dashboards, and more.
+That's it. Add `app=True`, return a Prefab component instead of raw dicts. FastMCP handles the rendering, sandboxing, and security. No wrapper class needed for simple cases like this.
-## What You Return
+## Add charts
-### Components
+When numbers tell a better story as a visual, swap in a chart. The API is the same: pass your data as a list of dicts, tell the chart which keys to plot.
-The simplest way to get started. If you're returning a visual representation of data and don't need Prefab's more advanced features like initial state or stylesheets, just return the components directly. FastMCP wraps them in a `PrefabApp` automatically:
+
```python
-from prefab_ui.components import Column, Heading, Badge
-from fastmcp import FastMCP
+@mcp.tool(app=True)
+def quarterly_revenue(year: int) -> BarChart:
+ """Show quarterly revenue as a bar chart."""
+ 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},
+ ]
-mcp = FastMCP("Status")
+ return BarChart(
+ data=data,
+ series=[
+ ChartSeries(data_key="revenue", label="Revenue"),
+ ChartSeries(data_key="costs", label="Costs"),
+ ],
+ x_axis="quarter",
+ show_legend=True,
+ )
+```
+Each `ChartSeries` plots a different key from the data. `BarChart`, `LineChart`, `AreaChart`, `PieChart`, `RadarChart`, and `RadialChart` all follow the same pattern. Hover over the bars to see tooltips.
+
+
+
+```python
+@mcp.tool(app=True)
+def ticket_breakdown() -> PieChart:
+ """Show open tickets by category."""
+ data = [
+ {"category": "Bug", "count": 42},
+ {"category": "Feature", "count": 28},
+ {"category": "Docs", "count": 15},
+ {"category": "Infra", "count": 10},
+ ]
+
+ return PieChart(
+ data=data,
+ data_key="count",
+ name_key="category",
+ inner_radius=50,
+ show_legend=True,
+ )
+```
+
+See the [Prefab chart docs](https://prefab.prefect.io/docs/components) for stacking, curves, custom colors, and more.
+
+## Compose a dashboard
+
+Tables and charts are useful on their own, but the real power comes from composing them. `Column` stacks children vertically, `Row` lays them out side by side, and `with` blocks establish nesting — the indentation is the layout.
+
+
+
+```python expandable
+@mcp.tool(app=True)
+def sales_dashboard() -> PrefabApp:
+ """Show sales KPIs, trends, and deals."""
+ 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,
+ )
+ Separator()
+ DataTable(
+ columns=[
+ DataTableColumn(key="account", header="Account", sortable=True),
+ DataTableColumn(key="value", header="Value", sortable=True),
+ DataTableColumn(key="stage", header="Stage"),
+ ],
+ rows=rows,
+ )
+
+ return app
+```
+
+Notice how `Badge` components can be placed inside table cells — any Prefab component works as a cell value, so you can put progress bars, icons, or buttons in your tables too.
+
+## Make it reactive
+
+Everything above renders once from the data your Python provides. But interactive tools can also respond to user input in real time, without any server round-trips. Prefab's state system lets components read and write client-side values, so the UI updates instantly as the user interacts with it.
+
+
+
+Try switching regions in the dropdown, and toggling the switch on and off.
+
+```python expandable
+from prefab_ui.rx import Rx
@mcp.tool(app=True)
-def status_badge() -> Column:
- """Show system status."""
- with Column(gap=2) as view:
- Heading("All Systems Operational")
- Badge("Healthy", variant="success")
- return view
+def regional_sales() -> PrefabApp:
+ """Sales by region with a live filter."""
+ 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",
+ )
+ with If(Rx("show_target")):
+ Metric(label="Q1 Target", value="$75,000")
+
+ return app
```
-Want a chart? Return a chart. Want a table? Return a table. FastMCP handles the wiring.
+The `state` dict on `PrefabApp` declares initial values. The `Select` writes to the `region` key on every change. A `let` binding picks the matching dataset, and the chart re-renders. The `Switch` toggles a `Metric` on and off through `If(Rx("show_target"))`. All of this happens in the browser — no calls back to your server.
-### PrefabApp
+`Rx` is a reactive reference: `Rx("region")` compiles to an expression the renderer evaluates live. It supports arithmetic, comparisons, formatting pipes (`.currency()`, `.percent()`), and ternary conditionals (`.then()`). For the full state system, see the [Prefab state docs](https://prefab.prefect.io/docs/concepts/state) and [expression docs](https://prefab.prefect.io/docs/concepts/expressions).
-When you need more control — setting initial state values that components can read and react to, or configuring the rendering engine — return a `PrefabApp` explicitly:
+## Content Security Policy
+
+Interactive tools render in a sandboxed iframe with a strict CSP. If your tool loads external resources — embedding iframes, fetching from APIs, loading scripts — add the required domains:
```python
-from prefab_ui.components import Column, Heading, Text, Button, If, Badge
-from prefab_ui.actions import ToggleState
-from prefab_ui.app import PrefabApp
-from fastmcp import FastMCP
+from fastmcp.apps import PrefabAppConfig, ResourceCSP
-mcp = FastMCP("Demo")
-
-
-@mcp.tool(app=True)
-def toggle_demo() -> PrefabApp:
- """Interactive toggle with state."""
- with Column(gap=4, css_class="p-6") as view:
- Button("Toggle", on_click=ToggleState("show"))
- with If("{{ show }}"):
- Badge("Visible!", variant="success")
-
- return PrefabApp(view=view, state={"show": False})
+@mcp.tool(app=PrefabAppConfig(
+ csp=ResourceCSP(frame_domains=["https://example.com"]),
+))
+def dashboard_with_embed() -> PrefabApp:
+ ...
```
-The `state` dict provides the initial values. Components reference state with `{{ expression }}` templates. State mutations like `ToggleState` happen entirely in the browser — no server round-trip. The [Prefab state guide](https://prefab.prefect.io/docs/concepts/state) covers this in detail.
+`PrefabAppConfig()` with no arguments is equivalent to `app=True`.
-### ToolResult
+## Giving the LLM context
-Every tool result has two audiences: the renderer (which displays the UI) and the LLM (which reads the text content to understand what happened). By default, Prefab Apps send `"[Rendered Prefab UI]"` as the text content, which tells the LLM almost nothing.
-
-If you want the LLM to understand the result — so it can reference the data in conversation, summarize it, or decide what to do next — wrap your return in a `ToolResult` with a meaningful `content` string:
+By default, the LLM sees `"[Rendered Prefab UI]"` as the tool result. If the model needs to reason about the data, return a `ToolResult` with a text summary alongside the UI:
```python
-from prefab_ui.components import Column, Heading, BarChart, ChartSeries
-from prefab_ui.app import PrefabApp
-from fastmcp import FastMCP
from fastmcp.tools import ToolResult
-mcp = FastMCP("Sales")
-
-
@mcp.tool(app=True)
def sales_overview(year: int) -> ToolResult:
- """Show sales data visually and summarize for the model."""
+ """Show sales visually, summarize for the model."""
data = get_sales_data(year)
total = sum(row["revenue"] for row in data)
with Column(gap=4, css_class="p-6") as view:
- Heading("Sales Overview")
BarChart(data=data, series=[ChartSeries(data_key="revenue")])
return ToolResult(
@@ -148,48 +285,13 @@ def sales_overview(year: int) -> ToolResult:
)
```
-The user sees the chart. The LLM sees `"Total revenue for 2025: $203,000 across 4 quarters"` and can reason about it.
+The user sees the chart. The model sees the summary.
-## Type Inference
+## Next steps
-If your tool's return type annotation is a Prefab type — `PrefabApp`, `Component`, or their `Optional` variants — FastMCP detects this and enables app rendering automatically:
-
-```python
-@mcp.tool
-def greet(name: str) -> PrefabApp:
- return PrefabApp(view=Heading(f"Hello, {name}!"))
-```
-
-This is equivalent to `@mcp.tool(app=True)`. Explicit `app=True` is recommended for clarity, and is required when the return type doesn't reveal a Prefab type (e.g., `-> ToolResult`).
-
-## How It Works
-
-Behind the scenes, when a tool returns a Prefab component or `PrefabApp`, FastMCP:
-
-1. **Registers a shared renderer** — a `ui://prefab/renderer.html` resource containing the JavaScript rendering engine, fetched once by the host and reused across all your Prefab tools.
-2. **Wires the tool metadata** — so the host knows to load the renderer iframe when displaying the tool result.
-3. **Serializes the component tree** — your Python components become `structuredContent` on the tool result, which the renderer interprets and displays.
-
-None of this requires any configuration. The `app=True` flag (or type inference) is the only thing you need.
-
-## Mixing with Custom HTML Apps
-
-Prefab tools and [custom HTML tools](/apps/low-level) coexist in the same server. Prefab tools share a single renderer resource; custom tools point to their own. Both use the same MCP Apps protocol:
-
-```python
-from fastmcp.server.apps import AppConfig
-
-@mcp.tool(app=True)
-def team_directory() -> PrefabApp:
- ...
-
-@mcp.tool(app=AppConfig(resource_uri="ui://my-app/map.html"))
-def map_view() -> str:
- ...
-```
-
-## Next Steps
-
-- **[Patterns](/apps/patterns)** — Charts, tables, forms, and other common tool UIs
-- **[Custom HTML Apps](/apps/low-level)** — When you need your own HTML, CSS, and JavaScript
-- **[Prefab UI Docs](https://prefab.prefect.io)** — Components, state, expressions, and actions
+- **[FastMCPApp](/apps/fastmcp-app)** — when your UI needs to call backend tools (forms, search, CRUD)
+- **[Generative UI](/apps/generative)** — let the LLM design the UI at runtime
+- **[Custom HTML](/apps/low-level)** — when Prefab isn't enough (maps, 3D, your own framework)
+- **[Examples](/apps/examples)** — complete working servers you can run today
+- **[Development](/apps/development)** — preview your tools locally with `fastmcp dev apps`
+- **[Prefab UI](https://prefab.prefect.io)** — full component reference with 100+ components, theming, and advanced patterns
diff --git a/docs/apps/providers/approval.mdx b/docs/apps/providers/approval.mdx
new file mode 100644
index 000000000..8ac7b8dd1
--- /dev/null
+++ b/docs/apps/providers/approval.mdx
@@ -0,0 +1,80 @@
+---
+title: Approval
+sidebarTitle: Approval
+description: Human-in-the-loop approval gates for agent actions
+icon: shield-check
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+`Approval` adds a human-in-the-loop confirmation step to any server. The LLM presents what it's about to do, the user approves or rejects via buttons, and the decision flows back into the conversation as a message.
+
+
+
+
+
+```python
+from fastmcp import FastMCP
+from fastmcp.apps.approval import Approval
+
+mcp = FastMCP("My Server")
+mcp.add_provider(Approval())
+```
+
+This registers a single tool:
+
+| Tool | Visibility | Purpose |
+|------|-----------|---------|
+| `request_approval` | Model | Shows an approval card, sends the user's decision back as a message |
+
+The LLM calls `request_approval` with a summary (and optional details) whenever it's about to take a significant action. The user sees a card with Approve and Reject buttons. Clicking either sends a message back into the conversation via `SendMessage`, which triggers the LLM's next turn.
+
+The message looks like it came from the user:
+
+```
+"Deploy v3.2 to production" — I selected: Approve
+```
+
+
+Approval is an advisory gate, not an enforcement mechanism. The conversation isn't blocked while the card is open — the user can keep typing, and a determined LLM could proceed without waiting. Think of it as a strong UX signal that encourages confirmation, not a security boundary. For hard enforcement, implement approval logic server-side in your tool implementations.
+
+
+## Configuration
+
+The constructor sets defaults; the LLM can override all of these per-call via tool arguments.
+
+```python
+Approval(
+ name="Approval", # App name
+ title="Approval Required", # Card heading
+ approve_text="Approve", # Approve button label
+ reject_text="Reject", # Reject button label
+ approve_variant="default", # "default", "destructive", "success", "info"
+ reject_variant="outline", # same options plus "outline"
+)
+```
+
+The LLM can customize each invocation:
+
+```python
+request_approval(
+ summary="Delete 47 files from /tmp",
+ details="This cannot be undone.",
+ title="Destructive Action",
+ approve_text="Delete",
+ approve_variant="destructive",
+ reject_text="Keep files",
+)
+```
+
+## How it works
+
+When the user clicks a button, two things happen:
+
+1. `SendMessage` pushes the decision into the conversation as a user message
+2. `SetState("decided", True)` replaces the buttons with "Response sent."
+
+The tool description instructs the LLM to stop and wait for the "I selected:" message before proceeding. If approved, it continues. If rejected, it acknowledges and asks how to proceed.
diff --git a/docs/apps/providers/choice.mdx b/docs/apps/providers/choice.mdx
new file mode 100644
index 000000000..c29c1b2bc
--- /dev/null
+++ b/docs/apps/providers/choice.mdx
@@ -0,0 +1,72 @@
+---
+title: Choice
+sidebarTitle: Choice
+description: Present clickable options instead of free-text responses
+icon: list-check
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+`Choice` lets the LLM present a set of options as clickable buttons instead of asking the user to type a response. The selection flows back into the conversation as a message, giving the LLM clean structured input.
+
+
+
+
+
+```python
+from fastmcp import FastMCP
+from fastmcp.apps.choice import Choice
+
+mcp = FastMCP("My Server")
+mcp.add_provider(Choice())
+```
+
+This registers a single tool:
+
+| Tool | Visibility | Purpose |
+|------|-----------|---------|
+| `choose` | Model | Shows a card with clickable options, sends the selection back as a message |
+
+The LLM calls `choose` with a prompt and a list of options. The user sees a card with one button per option. Clicking one sends a message back into the conversation:
+
+```
+"Which deployment strategy?" — I selected: Blue-green
+```
+
+
+This is an advisory interaction, not an enforcement mechanism. The conversation isn't blocked while the card is open — the user can keep typing, and the LLM could proceed without waiting. The tool description instructs the LLM to stop and wait for the "I selected:" response, but for hard enforcement, implement selection logic server-side.
+
+
+## Configuration
+
+The constructor sets defaults; the LLM can override `title` per-call.
+
+```python
+Choice(
+ name="Choice", # App name
+ title="Choose an Option", # Default card heading
+ variant="outline", # Button style for all options
+)
+```
+
+The LLM provides the options per-call:
+
+```python
+choose(
+ prompt="What should we have for lunch?",
+ options=["Pizza", "Tacos", "Ramen", "Salad"],
+ title="The Important Questions",
+)
+```
+
+## How it works
+
+Each option renders as a full-width button in a vertical stack. When the user clicks one:
+
+1. `SendMessage` pushes the selection into the conversation as a user message
+2. `SetState("decided", True)` replaces the buttons with "Response sent."
+
+The tool description instructs the LLM to stop and wait for the "I selected:" message before proceeding with whatever the user chose.
diff --git a/docs/apps/providers/file-upload.mdx b/docs/apps/providers/file-upload.mdx
new file mode 100644
index 000000000..f10ef0da7
--- /dev/null
+++ b/docs/apps/providers/file-upload.mdx
@@ -0,0 +1,144 @@
+---
+title: File Upload
+sidebarTitle: File Upload
+description: Drag-and-drop file upload for any MCP server
+icon: upload
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+`FileUpload` adds drag-and-drop file upload to any server. Users upload files through an interactive UI, bypassing the LLM context window entirely. The LLM can then list and read uploaded files through model-visible tools.
+
+
+
+
+
+```python
+from fastmcp import FastMCP
+from fastmcp.apps.file_upload import FileUpload
+
+mcp = FastMCP("My Server")
+mcp.add_provider(FileUpload())
+```
+
+This registers four tools:
+
+| Tool | Visibility | Purpose |
+|------|-----------|---------|
+| `file_manager` | Model | Opens the drag-and-drop upload UI |
+| `store_files` | App only | Called by the UI when the user clicks Upload |
+| `list_files` | Model | Returns metadata for all uploaded files |
+| `read_file` | Model | Returns a file's contents by name |
+
+The LLM sees `file_manager`, `list_files`, and `read_file`. It calls `file_manager` to show the upload interface, then uses `list_files` and `read_file` to work with whatever the user uploaded. `store_files` is app-only — the UI calls it directly and the LLM never needs to know about it.
+
+## Configuration
+
+```python
+FileUpload(
+ name="Files", # App name (used in tool routing)
+ max_file_size=10 * 1024 * 1024, # 10 MB default, enforced server-side
+ title="File Upload", # Heading shown in the UI
+ description="Drop files to...", # Description text below the heading
+ drop_label="Drop files here", # Label inside the drop zone
+)
+```
+
+The `max_file_size` limit is enforced both in the UI (the DropZone rejects oversized files) and on the server (the `store_files` tool validates before calling `on_store`).
+
+## Storage scoping
+
+By default, files are stored in memory and scoped by MCP session ID. Each session gets its own isolated file store — files uploaded in one conversation aren't visible in another.
+
+This works with **stdio**, **SSE**, and **stateful HTTP** transports, where sessions persist across requests.
+
+
+In **stateless HTTP** mode, each request creates a new session object with a new ID. Files stored during one request (e.g. the UI upload) will be invisible to the next request (e.g. the LLM calling `list_files`). You **must** override `_get_scope_key` to use a stable identifier like a user ID from your auth token.
+
+
+For stateless deployments, override `_get_scope_key` to return a stable identifier. To scope files by authenticated user, read the caller from `get_access_token()`.
+
+Reject the request when there is no subject to key on. `get_access_token()` returns `None` on an unauthenticated request, and `subject` is optional even on a valid token, since not every verifier populates it. Returning a fallback in either case would put every such caller in one shared bucket, so they would see each other's uploads.
+
+```python
+from fastmcp.apps.file_upload import FileUpload
+from fastmcp.server.dependencies import get_access_token
+
+class UserScopedUpload(FileUpload):
+ def _get_scope_key(self, ctx):
+ token = get_access_token()
+ if token is None or not token.subject:
+ raise ValueError("File scoping requires an authenticated user with a subject")
+ return token.subject
+```
+
+If your provider carries the user identity in a different claim, read it from `token.claims` and validate it the same way.
+
+For process-wide shared storage (all users see all files):
+
+```python
+class SharedUpload(FileUpload):
+ def _get_scope_key(self, ctx):
+ return "__shared__"
+```
+
+## Custom storage
+
+The default implementation stores files in memory for the lifetime of the server process. For persistent storage, subclass `FileUpload` and override three methods. Each receives the current `Context`, giving you access to session IDs, auth tokens, and request metadata for partitioning and authorization.
+
+```python
+import base64
+
+from fastmcp.apps.file_upload import FileUpload
+from fastmcp.server.dependencies import get_access_token
+
+class S3Upload(FileUpload):
+ def _get_scope_key(self, ctx):
+ token = get_access_token()
+ if token is None or not token.subject:
+ raise ValueError("File scoping requires an authenticated user with a subject")
+ return token.subject
+
+ def on_store(self, files, ctx):
+ user_id = self._get_scope_key(ctx)
+ for f in files:
+ s3.put_object(
+ Bucket="uploads",
+ Key=f"{user_id}/{f['name']}",
+ Body=base64.b64decode(f["data"]),
+ )
+ return self.on_list(ctx)
+
+ def on_list(self, ctx):
+ user_id = self._get_scope_key(ctx)
+ objects = s3.list_objects(Bucket="uploads", Prefix=f"{user_id}/")
+ return [
+ {
+ "name": obj["Key"].split("/", 1)[1],
+ "type": "application/octet-stream",
+ "size": obj["Size"],
+ "size_display": f"{obj['Size']} B",
+ "uploaded_at": obj["LastModified"].isoformat(),
+ }
+ for obj in objects.get("Contents", [])
+ ]
+
+ def on_read(self, name, ctx):
+ user_id = self._get_scope_key(ctx)
+ obj = s3.get_object(Bucket="uploads", Key=f"{user_id}/{name}")
+ content = obj["Body"].read()
+ return {
+ "name": name,
+ "size": obj["ContentLength"],
+ "type": obj["ContentType"],
+ "uploaded_at": obj["LastModified"].isoformat(),
+ "content": content.decode("utf-8"),
+ }
+```
+
+Each file dict passed to `on_store` contains `name`, `size`, `type`, and `data` (base64-encoded content). The return value from `on_store` and `on_list` should be a list of summary dicts with `name`, `type`, `size`, `size_display`, and `uploaded_at` fields — these populate the file list in the UI.
+
+`on_read` returns a dict with file metadata and either `content` (decoded text) or `content_base64` (a base64 preview for binary files).
diff --git a/docs/apps/providers/form.mdx b/docs/apps/providers/form.mdx
new file mode 100644
index 000000000..e61dc0ce0
--- /dev/null
+++ b/docs/apps/providers/form.mdx
@@ -0,0 +1,105 @@
+---
+title: Form Input
+sidebarTitle: Form Input
+description: Collect structured data from users via Pydantic models
+icon: rectangle-list
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+`FormInput` generates a validated form from a Pydantic model. The user fills it out, and the submission is validated against the model before being returned. Structured elicitation that can't be hallucinated.
+
+
+
+
+
+```python
+from typing import Literal
+
+from pydantic import BaseModel, Field
+from fastmcp import FastMCP
+from fastmcp.apps.form import FormInput
+
+class BugReport(BaseModel):
+ title: str = Field(description="Brief summary")
+ severity: Literal["low", "medium", "high", "critical"]
+ description: str = Field(
+ description="Detailed description",
+ json_schema_extra={"ui": {"type": "textarea"}},
+ )
+
+mcp = FastMCP("My Server")
+mcp.add_provider(FormInput(model=BugReport))
+```
+
+This registers two tools:
+
+| Tool | Visibility | Purpose |
+|------|-----------|---------|
+| `collect_bugreport` | Model | Opens the form UI |
+| `submit_form` | App only | Validates and processes the submission |
+
+The tool name is derived from the model class name, lowercased: `collect_{modelname}`. So `BugReport` becomes `collect_bugreport`, `ShippingAddress` becomes `collect_shippingaddress`. Use `tool_name` to override if needed. The LLM calls it with a prompt explaining what it needs, and the user gets a form with fields matching the model.
+
+## Field mapping
+
+`FormInput` uses Prefab's `Form.from_model()`, which maps Pydantic types to form components:
+
+| Python type | Form component |
+|------------|---------------|
+| `str` | Text input |
+| `int`, `float` | Number input |
+| `bool` | Checkbox |
+| `datetime.date` | Date picker |
+| `Literal[...]` | Select dropdown |
+| `SecretStr` | Password input |
+
+Use `Field()` metadata to control labels (`title`), placeholders (`description`), and validation (`min_length`, `max_length`, `ge`, `le`). Use `json_schema_extra={"ui": {"type": "textarea"}}` for multiline text.
+
+## Callback
+
+By default, the validated model is returned as JSON. Provide an `on_submit` callback to process the data server-side:
+
+```python
+def save_report(report: BugReport) -> str:
+ db.insert(report.model_dump())
+ return f"Bug #{db.last_id} filed: {report.title}"
+
+mcp.add_provider(FormInput(model=BugReport, on_submit=save_report))
+```
+
+The callback receives a validated model instance and returns a string that becomes the tool result.
+
+## Configuration
+
+```python
+FormInput(
+ model=BugReport, # Required: the Pydantic model
+ name="BugTracker", # App name (default: model name)
+ title="File a Bug", # Card heading (default: model name)
+ tool_name="file_bug", # Tool name (default: collect_{model})
+ submit_text="Submit Report", # Button label (default: "Submit")
+ on_submit=save_report, # Optional callback
+ send_message=True, # Push result as a chat message
+)
+```
+
+Set `send_message=True` to push the result back into the conversation via `SendMessage`, triggering the LLM's next turn. Without it, the result is just the tool return value.
+
+## Multiple forms
+
+Add multiple providers for different models — each gets its own tool:
+
+```python
+mcp = FastMCP(
+ "My Server",
+ providers=[
+ FormInput(model=ShippingAddress),
+ FormInput(model=BugReport),
+ FormInput(model=ContactInfo),
+ ],
+)
+```
diff --git a/docs/apps/quickstart.mdx b/docs/apps/quickstart.mdx
new file mode 100644
index 000000000..2221b9de9
--- /dev/null
+++ b/docs/apps/quickstart.mdx
@@ -0,0 +1,197 @@
+---
+title: Quickstart
+sidebarTitle: Quickstart
+description: Build your first FastMCP app in under a minute.
+icon: rocket
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+import { PrefabDemoFrame } from '/snippets/prefab-demo-frame.mdx'
+
+
+
+By the end of this page, you'll have a working tool that returns this:
+
+
+
+A pie chart the user can hover, a table they can sort and search — and a single Python tool.
+
+## Install
+
+```bash
+pip install "fastmcp[apps]"
+```
+
+The `apps` extra pulls in [Prefab](https://prefab.prefect.io), the Python component library used to build app UIs.
+
+## Write the tool
+
+Create `server.py`. The interesting parts: `app=True` tells FastMCP this tool renders a UI, and `with PrefabApp() as app:` is the canonical pattern for composing one.
+
+```python server.py expandable
+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
+from fastmcp import FastMCP
+
+mcp = FastMCP("My First App")
+
+
+@mcp.tool(app=True)
+def team_directory() -> PrefabApp:
+ """Browse the team directory."""
+ 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,
+ )
+
+ return app
+```
+
+The Prefab code reads top-to-bottom. `PrefabApp()` is the root; everything inside its `with` block becomes the UI. `Column` stacks children vertically, `Grid` lays them out in columns. `DataTable` takes rows and column definitions and gives you sort and search for free.
+
+`app=True` does the rest: it sets up the renderer resource, the content security policy, and the metadata that tells the host "this tool returns a UI." The host loads the result in a sandboxed iframe where the user can interact with it — all client-side, no round-trips.
+
+## Preview it
+
+FastMCP ships a dev server that renders your app tools in a browser, no MCP host needed:
+
+```bash
+fastmcp dev apps server.py
+```
+
+Open `http://localhost:8080`, pick `team_directory`, and try sorting columns and searching.
+
+
+
+
+
+## Make it reactive
+
+The UI above renders once from your Python. Prefab apps can also respond to user input live, without any server round-trips. The key concept is **state**: a client-side key-value store that components read from and write to.
+
+Click a row in the demo below to see a detail card appear:
+
+
+
+Add a few imports, give each member a couple more fields, wire up a click handler, and render a detail card when something's selected:
+
+```python expandable server.py
+from collections import Counter
+
+from prefab_ui.actions import SetState
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import (
+ Badge, Card, CardContent, CardHeader, Column, DataTable, DataTableColumn,
+ Grid, H3, Row, Small, Text,
+)
+from prefab_ui.components.charts import PieChart
+from prefab_ui.components.control_flow import If
+from prefab_ui.rx import Rx, STATE
+from fastmcp import FastMCP
+
+mcp = FastMCP("My First App")
+
+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},
+ # ... more members ...
+]
+
+OFFICE_COUNTS = [
+ {"office": o, "count": c}
+ for o, c in Counter(m["office"] for m in MEMBERS).items()
+]
+
+
+@mcp.tool(app=True)
+def team_directory() -> PrefabApp:
+ """Browse the team directory."""
+ 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"))
+
+ return app
+```
+
+Three new ideas do all the work:
+
+- **`on_row_click=SetState("selected", Rx("$event"))`** — clicking a row writes its data into the `selected` state key. `$event` is the clicked row dict.
+- **`Rx("selected.name")`** — a reactive reference. It doesn't hold a Python value; it compiles to a browser-side expression that re-evaluates whenever `selected` changes, so `Text(Rx("selected.name"))` always shows the latest clicked name.
+- **`If(STATE.selected)`** — conditionally renders its body. Before any click, `selected` is `None` and the card stays hidden.
+
+The `state={"selected": None}` dict on `PrefabApp` sets the initial value. Everything else happens in the browser — no round-trips to your server when the user clicks.
+
+## Where to go next
+
+You've built a tool that returns an interactive, reactive UI. This pattern covers a huge range of use cases: build a visualization, return it, and the user gets it rendered right in the conversation.
+
+- **[Interactive Tools](/apps/prefab)** — charts, tables, dashboards, reactive state, with live demos
+- **[FastMCPApp](/apps/fastmcp-app)** — when the UI needs to call back to your server (forms, search, CRUD)
+- **[Examples](/apps/examples)** — complete working servers you can run today
diff --git a/docs/changelog.mdx b/docs/changelog.mdx
index ae9e94fe9..0f7efeb17 100644
--- a/docs/changelog.mdx
+++ b/docs/changelog.mdx
@@ -5,6 +5,978 @@ rss: true
tag: NEW
---
+
+
+**[v3.4.6: Trust, but Proxy](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.6)**
+
+FastMCP 3.4.6 backports trusted-proxy support for SSRF-protected OAuth metadata and JWKS fetches. Deployments can now route these requests through a mandated corporate proxy while preserving custom CA certificates; FastMCP refuses the fetch when no proxy is configured instead of risking an unprotected direct request.
+
+### Fixes 🐞
+* Backport #4412 to 3.x: support trusted SSRF proxies by [@jlowin](https://github.com/jlowin) in [#4755](https://github.com/PrefectHQ/fastmcp/pull/4755)
+
+### Docs 📚
+* Docs: add v3.4.6 changelog entries by [@jlowin](https://github.com/jlowin) in [#4761](https://github.com/PrefectHQ/fastmcp/pull/4761)
+
+**Full Changelog**: [v3.4.5...v3.4.6](https://github.com/PrefectHQ/fastmcp/compare/v3.4.5...v3.4.6)
+
+
+
+
+
+**[v4.0.0b1: Fourgone Conclusion](https://github.com/PrefectHQ/fastmcp/releases/tag/v4.0.0b1)**
+
+FastMCP 4 makes stateful MCP applications work on the sessionless `2026-07-28` protocol while one deployment continues serving handshake-era clients. Tools can ask follow-up questions across requests, preserve authenticated user state, and move long-running work into background tasks without sticky sessions. Protocol extensions and enterprise identity become first-class surfaces, and most FastMCP 3 servers upgrade unchanged even though MCP Python SDK v2 rewrote the engine underneath them. Server-initiated sampling and roots are removed from the server API; the [upgrade guide](/getting-started/upgrading/from-fastmcp-3) covers their replacements.
+
+### New Features 🎉
+* Migrate to MCP Python SDK v2 by [@jlowin](https://github.com/jlowin) in [#4437](https://github.com/PrefectHQ/fastmcp/pull/4437)
+* Teach fastmcp.Client the modern protocol: mode negotiation, MRTR driver, response cache by [@jlowin](https://github.com/jlowin) in [#4450](https://github.com/PrefectHQ/fastmcp/pull/4450)
+* Forward-port Hugging Face auth provider by [@jlowin](https://github.com/jlowin) in [#4475](https://github.com/PrefectHQ/fastmcp/pull/4475)
+* Add server-side identity assertion (SEP-990 ID-JAG) by [@jlowin](https://github.com/jlowin) in [#4483](https://github.com/PrefectHQ/fastmcp/pull/4483)
+* Add guard-mode multi-round-trip tools (SEP-2322) by [@jlowin](https://github.com/jlowin) in [#4544](https://github.com/PrefectHQ/fastmcp/pull/4544)
+* Add FastMCP-native server extension API (SEP-2133) by [@jlowin](https://github.com/jlowin) in [#4602](https://github.com/PrefectHQ/fastmcp/pull/4602)
+* Add stateless session state (UserSession / SessionId) by [@jlowin](https://github.com/jlowin) in [#4604](https://github.com/PrefectHQ/fastmcp/pull/4604)
+* Add background tasks via the io.modelcontextprotocol/tasks extension (SEP-2663) by [@jlowin](https://github.com/jlowin) in [#4603](https://github.com/PrefectHQ/fastmcp/pull/4603)
+### Breaking Changes ⚠️
+* Emit one SERVER span per request and adopt spec-correct error codes by [@jlowin](https://github.com/jlowin) in [#4445](https://github.com/PrefectHQ/fastmcp/pull/4445)
+* Remove 3.x deprecated module shims and dead parameters by [@jlowin](https://github.com/jlowin) in [#4447](https://github.com/PrefectHQ/fastmcp/pull/4447)
+* Remove 3.0-deprecated FastMCP server methods by [@jlowin](https://github.com/jlowin) in [#4451](https://github.com/PrefectHQ/fastmcp/pull/4451)
+* Remove 3.x deprecated parameters and object-mode decorators by [@jlowin](https://github.com/jlowin) in [#4453](https://github.com/PrefectHQ/fastmcp/pull/4453)
+* Migrate to MCP SDK v2.0.0b2 (httpx2) by [@jlowin](https://github.com/jlowin) in [#4503](https://github.com/PrefectHQ/fastmcp/pull/4503)
+* Fix typos by [@szepeviktor](https://github.com/szepeviktor) in [#4498](https://github.com/PrefectHQ/fastmcp/pull/4498)
+* Stop proxies from validating backend results or mutating shared transports by [@jlowin](https://github.com/jlowin) in [#4552](https://github.com/PrefectHQ/fastmcp/pull/4552)
+* Surface resource, prompt, and proxy errors on the modern protocol by [@jlowin](https://github.com/jlowin) in [#4579](https://github.com/PrefectHQ/fastmcp/pull/4579)
+* Negotiate the best mutual protocol era by default by [@jlowin](https://github.com/jlowin) in [#4572](https://github.com/PrefectHQ/fastmcp/pull/4572)
+* Remove server-initiated sampling and roots from the server API by [@jlowin](https://github.com/jlowin) in [#4648](https://github.com/PrefectHQ/fastmcp/pull/4648)
+* Remove 3.x-era compatibility shims by [@jlowin](https://github.com/jlowin) in [#4661](https://github.com/PrefectHQ/fastmcp/pull/4661)
+### Enhancements ✨
+* Deprecate ctx.sample and add clear errors for push features on 2026 connections by [@jlowin](https://github.com/jlowin) in [#4448](https://github.com/PrefectHQ/fastmcp/pull/4448)
+* Add server-level cache hints (SEP-2549) by [@jlowin](https://github.com/jlowin) in [#4464](https://github.com/PrefectHQ/fastmcp/pull/4464)
+* Add KeyValueResponseCacheStore for distributed client response caching by [@jlowin](https://github.com/jlowin) in [#4479](https://github.com/PrefectHQ/fastmcp/pull/4479)
+* Test lifespan fires once per process over HTTP by [@jlowin](https://github.com/jlowin) in [#4480](https://github.com/PrefectHQ/fastmcp/pull/4480)
+* Add telemetry off-switch and mcp.protocol.version span attribute by [@jlowin](https://github.com/jlowin) in [#4481](https://github.com/PrefectHQ/fastmcp/pull/4481)
+* Trace client task management requests by [@jlowin](https://github.com/jlowin) in [#4525](https://github.com/PrefectHQ/fastmcp/pull/4525)
+* Stabilize upgraded ty checks by [@jlowin](https://github.com/jlowin) in [#4526](https://github.com/PrefectHQ/fastmcp/pull/4526)
+* Improve DescopeProvider scope discovery and well-known URL support by [@gaokevin1](https://github.com/gaokevin1) in [#4489](https://github.com/PrefectHQ/fastmcp/pull/4489)
+* Add examples/ to the ty static-analysis gate by [@jlowin](https://github.com/jlowin) in [#4466](https://github.com/PrefectHQ/fastmcp/pull/4466)
+* Expose telemetry attributes on span start by [@zzstoatzz](https://github.com/zzstoatzz) in [#4487](https://github.com/PrefectHQ/fastmcp/pull/4487)
+* Fix-issue-4284 : Add Auth0MCPProvider for Auth0 Auth for MCP by [@vijaydeepsinha](https://github.com/vijaydeepsinha) in [#4411](https://github.com/PrefectHQ/fastmcp/pull/4411)
+* Run FastMCP middleware for every inbound message by [@jlowin](https://github.com/jlowin) in [#4553](https://github.com/PrefectHQ/fastmcp/pull/4553)
+* Add 'prs welcome' label to waive the PR assignment gate by [@jlowin](https://github.com/jlowin) in [#4557](https://github.com/PrefectHQ/fastmcp/pull/4557)
+* Rename martian workflows to marvin by [@jlowin](https://github.com/jlowin) in [#4558](https://github.com/PrefectHQ/fastmcp/pull/4558)
+* Bump pinned Claude models to current versions by [@jlowin](https://github.com/jlowin) in [#4561](https://github.com/PrefectHQ/fastmcp/pull/4561)
+* Make the unit suite fast: in-process HTTP tests, no real sleeps, parallel Windows CI by [@jlowin](https://github.com/jlowin) in [#4554](https://github.com/PrefectHQ/fastmcp/pull/4554)
+* Mirror the frontend's protocol era on a proxy's backend connection by [@jlowin](https://github.com/jlowin) in [#4573](https://github.com/PrefectHQ/fastmcp/pull/4573)
+* Drop forked client protocol helpers in favor of the SDK's by [@jlowin](https://github.com/jlowin) in [#4574](https://github.com/PrefectHQ/fastmcp/pull/4574)
+* Bring the v4 developer notes up to date with what shipped by [@jlowin](https://github.com/jlowin) in [#4581](https://github.com/PrefectHQ/fastmcp/pull/4581)
+* Trim fastmcp.types to FastMCP-unique types by [@jlowin](https://github.com/jlowin) in [#4584](https://github.com/PrefectHQ/fastmcp/pull/4584)
+* Let a server answer argument-completion requests by [@jlowin](https://github.com/jlowin) in [#4582](https://github.com/PrefectHQ/fastmcp/pull/4582)
+* Add machine-to-machine client authentication by [@jlowin](https://github.com/jlowin) in [#4583](https://github.com/PrefectHQ/fastmcp/pull/4583)
+* Expose era-neutral client server metadata by [@zzstoatzz](https://github.com/zzstoatzz) in [#4599](https://github.com/PrefectHQ/fastmcp/pull/4599)
+* Support routable transport headers for gateways (SEP-2243) by [@jlowin](https://github.com/jlowin) in [#4622](https://github.com/PrefectHQ/fastmcp/pull/4622)
+* Emit scope step-up challenges for incremental authorization (SEP-2350) by [@jlowin](https://github.com/jlowin) in [#4623](https://github.com/PrefectHQ/fastmcp/pull/4623)
+* Honor OAuth application_type in DCR (SEP-837) by [@jlowin](https://github.com/jlowin) in [#4621](https://github.com/PrefectHQ/fastmcp/pull/4621)
+* Drop stale label-noting instructions from CLAUDE.md by [@jlowin](https://github.com/jlowin) in [#4654](https://github.com/PrefectHQ/fastmcp/pull/4654)
+* Add require_roles auth check by [@jlowin](https://github.com/jlowin) in [#4656](https://github.com/PrefectHQ/fastmcp/pull/4656)
+* Add `valid_scopes` parameter to OIDC proxy valid scopes by [@Educg550](https://github.com/Educg550) in [#4660](https://github.com/PrefectHQ/fastmcp/pull/4660)
+* feat: Add telemetry interop mode for FastMCP by [@strawgate](https://github.com/strawgate) in [#4046](https://github.com/PrefectHQ/fastmcp/pull/4046)
+* Note that review comment threads should get an acknowledgement by [@jlowin](https://github.com/jlowin) in [#4678](https://github.com/PrefectHQ/fastmcp/pull/4678)
+* Soften the review-comment reply guidance by [@jlowin](https://github.com/jlowin) in [#4683](https://github.com/PrefectHQ/fastmcp/pull/4683)
+* Resolve review threads on fix, reply on decline by [@jlowin](https://github.com/jlowin) in [#4685](https://github.com/PrefectHQ/fastmcp/pull/4685)
+* Move to the stable MCP Python SDK 2.0.0 by [@jlowin](https://github.com/jlowin) in [#4655](https://github.com/PrefectHQ/fastmcp/pull/4655)
+### Security 🔒
+* Drive the FastMCP lifespan through the SDK session manager by [@jlowin](https://github.com/jlowin) in [#4446](https://github.com/PrefectHQ/fastmcp/pull/4446)
+* Route skill file access through SDK path-security primitives by [@jlowin](https://github.com/jlowin) in [#4449](https://github.com/PrefectHQ/fastmcp/pull/4449)
+* Screen templated resource parameters for path traversal by default by [@jlowin](https://github.com/jlowin) in [#4482](https://github.com/PrefectHQ/fastmcp/pull/4482)
+* [codex] Add OAuthProxy RFC 9207 issuer responses by [@jlowin](https://github.com/jlowin) in [#4438](https://github.com/PrefectHQ/fastmcp/pull/4438)
+* Apply app visibility where no host can by [@jlowin](https://github.com/jlowin) in [#4692](https://github.com/PrefectHQ/fastmcp/pull/4692)
+### Fixes 🐞
+* Capture SharedContext for task-enabled Docket servers by [@jlowin](https://github.com/jlowin) in [#4443](https://github.com/PrefectHQ/fastmcp/pull/4443)
+* Fix stale mcp.types imports in examples by [@jlowin](https://github.com/jlowin) in [#4452](https://github.com/PrefectHQ/fastmcp/pull/4452)
+* Forward-port HTTP host guard compatibility by [@jlowin](https://github.com/jlowin) in [#4474](https://github.com/PrefectHQ/fastmcp/pull/4474)
+* Fix Azure scope fallback by [@zzstoatzz](https://github.com/zzstoatzz) in [#4469](https://github.com/PrefectHQ/fastmcp/pull/4469)
+* fix(server): omit ScalarElicitationType wrapper title from elicitation schemas by [@syf2211](https://github.com/syf2211) in [#4502](https://github.com/PrefectHQ/fastmcp/pull/4502)
+* Skip unsupported JWKS keys instead of failing the whole key set (#4515) by [@earfman](https://github.com/earfman) in [#4517](https://github.com/PrefectHQ/fastmcp/pull/4517)
+* Don't mutate the caller's schema in compress_schema by [@winklemad](https://github.com/winklemad) in [#4492](https://github.com/PrefectHQ/fastmcp/pull/4492)
+* Forward upstream instructions through create_proxy by [@verdie-g](https://github.com/verdie-g) in [#4512](https://github.com/PrefectHQ/fastmcp/pull/4512)
+* Serialize deep object query parameters by [@jlowin](https://github.com/jlowin) in [#4523](https://github.com/PrefectHQ/fastmcp/pull/4523)
+* Reject positional-only tool parameters by [@jlowin](https://github.com/jlowin) in [#4524](https://github.com/PrefectHQ/fastmcp/pull/4524)
+* Clarify PR-reopen flow and fix label-race that broke auto-reopen by [@jlowin](https://github.com/jlowin) in [#4518](https://github.com/PrefectHQ/fastmcp/pull/4518)
+* Clean up disconnected task sessions by [@jlowin](https://github.com/jlowin) in [#4519](https://github.com/PrefectHQ/fastmcp/pull/4519)
+* Handle expired OAuth client registrations by [@jlowin](https://github.com/jlowin) in [#4520](https://github.com/PrefectHQ/fastmcp/pull/4520)
+* Fix OAuth request annotation after httpx2 migration by [@jlowin](https://github.com/jlowin) in [#4534](https://github.com/PrefectHQ/fastmcp/pull/4534)
+* Fix docs banner contrast by [@jlowin](https://github.com/jlowin) in [#4522](https://github.com/PrefectHQ/fastmcp/pull/4522)
+* Preserve component metadata in response cache by [@jlowin](https://github.com/jlowin) in [#4521](https://github.com/PrefectHQ/fastmcp/pull/4521)
+* Clean up task sessions on connection exit by [@jlowin](https://github.com/jlowin) in [#4535](https://github.com/PrefectHQ/fastmcp/pull/4535)
+* Include scopes in auth challenges by [@jlowin](https://github.com/jlowin) in [#4527](https://github.com/PrefectHQ/fastmcp/pull/4527)
+* Make examples/ actually trigger the ty gate by [@jlowin](https://github.com/jlowin) in [#4541](https://github.com/PrefectHQ/fastmcp/pull/4541)
+* Add subject field to AccessToken initialization by [@piaudonn](https://github.com/piaudonn) in [#4267](https://github.com/PrefectHQ/fastmcp/pull/4267)
+* Restore Mintlify's fixed banner positioning by [@jlowin](https://github.com/jlowin) in [#4542](https://github.com/PrefectHQ/fastmcp/pull/4542)
+* Fix #4292: SSRF guard breaks OAuth/JWKS fetches behind a corporate HTTP proxy by [@endofcake](https://github.com/endofcake) in [#4412](https://github.com/PrefectHQ/fastmcp/pull/4412)
+* Preserve telemetry attributes when a sampler does not forward them by [@jlowin](https://github.com/jlowin) in [#4539](https://github.com/PrefectHQ/fastmcp/pull/4539)
+* Speed up the unit test suite, and fix the task-notification race it surfaced by [@jlowin](https://github.com/jlowin) in [#4550](https://github.com/PrefectHQ/fastmcp/pull/4550)
+* Fix label triage applying no labels, and make blocked tool calls fail by [@jlowin](https://github.com/jlowin) in [#4555](https://github.com/PrefectHQ/fastmcp/pull/4555)
+* Fix AI workflow allowlists being destroyed by tokenization by [@jlowin](https://github.com/jlowin) in [#4560](https://github.com/PrefectHQ/fastmcp/pull/4560)
+* Make transformed tool `required` order deterministic by [@Kludex](https://github.com/Kludex) in [#4564](https://github.com/PrefectHQ/fastmcp/pull/4564)
+* Stop gather() from creating coroutines it may never schedule by [@jlowin](https://github.com/jlowin) in [#4559](https://github.com/PrefectHQ/fastmcp/pull/4559)
+* Restore upgraded dependency checks by [@zzstoatzz](https://github.com/zzstoatzz) in [#4576](https://github.com/PrefectHQ/fastmcp/pull/4576)
+* Fix skill frontmatter parsing with UTF-8 BOM by [@hxaxd](https://github.com/hxaxd) in [#4533](https://github.com/PrefectHQ/fastmcp/pull/4533)
+* Fix File helper extension handling by [@VectorPeak](https://github.com/VectorPeak) in [#4531](https://github.com/PrefectHQ/fastmcp/pull/4531)
+* Fix percent-encoded skill file names unreadable in resources mode by [@jlowin](https://github.com/jlowin) in [#4590](https://github.com/PrefectHQ/fastmcp/pull/4590)
+* Fix flaky stdio crash-recovery tests by [@jlowin](https://github.com/jlowin) in [#4594](https://github.com/PrefectHQ/fastmcp/pull/4594)
+* Bridge camelCase ToolAnnotations reads by [@zzstoatzz](https://github.com/zzstoatzz) in [#4597](https://github.com/PrefectHQ/fastmcp/pull/4597)
+* Preserve raw CallToolResult tool returns by [@LarryHu0217](https://github.com/LarryHu0217) in [#4587](https://github.com/PrefectHQ/fastmcp/pull/4587)
+* Advertise only supported token endpoint auth methods in OAuthProxy metadata by [@jlowin](https://github.com/jlowin) in [#4608](https://github.com/PrefectHQ/fastmcp/pull/4608)
+* Fix OAuth proxy override typing by [@zzstoatzz](https://github.com/zzstoatzz) in [#4612](https://github.com/PrefectHQ/fastmcp/pull/4612)
+* Pin burner-redis below the Windows-crashing 0.1.7 release by [@jlowin](https://github.com/jlowin) in [#4618](https://github.com/PrefectHQ/fastmcp/pull/4618)
+* fix : canonical mime type mapping from formats to remove inconsistency #4627 by [@Aman071106](https://github.com/Aman071106) in [#4628](https://github.com/PrefectHQ/fastmcp/pull/4628)
+* fix: accept callable roots handlers by [@ShuyingZhang](https://github.com/ShuyingZhang) in [#4639](https://github.com/PrefectHQ/fastmcp/pull/4639)
+* Pass the MCP conformance suite's draft and pending scenarios by [@jlowin](https://github.com/jlowin) in [#4650](https://github.com/PrefectHQ/fastmcp/pull/4650)
+* Use issuer_url for OAuth issuer identity by [@jlowin](https://github.com/jlowin) in [#4652](https://github.com/PrefectHQ/fastmcp/pull/4652)
+* Fix the ty failure blocking upgrade checks on main by [@jlowin](https://github.com/jlowin) in [#4657](https://github.com/PrefectHQ/fastmcp/pull/4657)
+* Bind CIMD assertion audience to the advertised token endpoint by [@jlowin](https://github.com/jlowin) in [#4659](https://github.com/PrefectHQ/fastmcp/pull/4659)
+* Record effective scopes on the OAuth transaction by [@jlowin](https://github.com/jlowin) in [#4670](https://github.com/PrefectHQ/fastmcp/pull/4670)
+* Copy schemas iteratively so deep nesting still compresses by [@jlowin](https://github.com/jlowin) in [#4671](https://github.com/PrefectHQ/fastmcp/pull/4671)
+* Fix OpenAPI allOf reference fields by [@hxaxd](https://github.com/hxaxd) in [#4653](https://github.com/PrefectHQ/fastmcp/pull/4653)
+* Flatten OpenAPI discriminator subtypes into request bodies by [@jlowin](https://github.com/jlowin) in [#4677](https://github.com/PrefectHQ/fastmcp/pull/4677)
+* Let maintenance releases publish without fastmcp-tasks by [@jlowin](https://github.com/jlowin) in [#4676](https://github.com/PrefectHQ/fastmcp/pull/4676)
+* Read CLI-scanned MCP config files as UTF-8 explicitly by [@jlowin](https://github.com/jlowin) in [#4690](https://github.com/PrefectHQ/fastmcp/pull/4690)
+* Late-bind app tool names so UIs survive composition by [@jlowin](https://github.com/jlowin) in [#4682](https://github.com/PrefectHQ/fastmcp/pull/4682)
+### Docs 📚
+* Docs: forward-port v3.4.4 changelog entries by [@jlowin](https://github.com/jlowin) in [#4476](https://github.com/PrefectHQ/fastmcp/pull/4476)
+* Document icon theme support by [@jlowin](https://github.com/jlowin) in [#4537](https://github.com/PrefectHQ/fastmcp/pull/4537)
+* Add missing 4.0.0 version badge to Path Security docs by [@jlowin](https://github.com/jlowin) in [#4540](https://github.com/PrefectHQ/fastmcp/pull/4540)
+* Align server component docs by [@strawgate](https://github.com/strawgate) in [#4260](https://github.com/PrefectHQ/fastmcp/pull/4260)
+* Align CLI, deployment, and config docs by [@strawgate](https://github.com/strawgate) in [#4259](https://github.com/PrefectHQ/fastmcp/pull/4259)
+* Align client, Apps, and integration docs by [@strawgate](https://github.com/strawgate) in [#4261](https://github.com/PrefectHQ/fastmcp/pull/4261)
+* Fix stale MRTR/elicitation framing in client and upgrade docs by [@jlowin](https://github.com/jlowin) in [#4551](https://github.com/PrefectHQ/fastmcp/pull/4551)
+* docs: quote pip extras install examples by [@RachGranville](https://github.com/RachGranville) in [#4568](https://github.com/PrefectHQ/fastmcp/pull/4568)
+* Document Windows CI parallelism and the subprocess_heavy marker by [@jlowin](https://github.com/jlowin) in [#4575](https://github.com/PrefectHQ/fastmcp/pull/4575)
+* Document v3->v4 removals and add upgrade-reality tests by [@jlowin](https://github.com/jlowin) in [#4585](https://github.com/PrefectHQ/fastmcp/pull/4585)
+* Archive v3 docs and publish v4 as the primary version by [@jlowin](https://github.com/jlowin) in [#4613](https://github.com/PrefectHQ/fastmcp/pull/4613)
+* Document targeted v4 prerelease installation by [@zzstoatzz](https://github.com/zzstoatzz) in [#4598](https://github.com/PrefectHQ/fastmcp/pull/4598)
+* Fix stale Mac/Windows-vs-Linux OAuth key/storage docs by [@jlowin](https://github.com/jlowin) in [#4617](https://github.com/PrefectHQ/fastmcp/pull/4617)
+* v4 docs quality pass: stale task/era claims, broken links, polish by [@jlowin](https://github.com/jlowin) in [#4619](https://github.com/PrefectHQ/fastmcp/pull/4619)
+* whats-new: add the argument completion capability by [@jlowin](https://github.com/jlowin) in [#4620](https://github.com/PrefectHQ/fastmcp/pull/4620)
+* docs: fix ProxyProvider docstring example calling nonexistent with_namespace() by [@andrew-stelmach-fleet](https://github.com/andrew-stelmach-fleet) in [#4633](https://github.com/PrefectHQ/fastmcp/pull/4633)
+* Unpublish v4 development notes; prep docs for beta 1 by [@jlowin](https://github.com/jlowin) in [#4644](https://github.com/PrefectHQ/fastmcp/pull/4644)
+* Expand the FAQ for the v4 transition by [@jlowin](https://github.com/jlowin) in [#4649](https://github.com/PrefectHQ/fastmcp/pull/4649)
+* Document the issuer_url identity change for upgraders by [@jlowin](https://github.com/jlowin) in [#4658](https://github.com/PrefectHQ/fastmcp/pull/4658)
+* Cover require_roles in the v4 highlights by [@jlowin](https://github.com/jlowin) in [#4666](https://github.com/PrefectHQ/fastmcp/pull/4666)
+* Fix FAQ: sampling/roots/elicitation legacy-mode advice, SessionProvider registration by [@jlowin](https://github.com/jlowin) in [#4672](https://github.com/PrefectHQ/fastmcp/pull/4672)
+* Audit v4 docs: fix missing version badges, fill whats-new gaps by [@jlowin](https://github.com/jlowin) in [#4668](https://github.com/PrefectHQ/fastmcp/pull/4668)
+* Docs: add v3.4.5 changelog entries to main by [@jlowin](https://github.com/jlowin) in [#4674](https://github.com/PrefectHQ/fastmcp/pull/4674)
+* Split the SDK upgrade guides by SDK version by [@jlowin](https://github.com/jlowin) in [#4684](https://github.com/PrefectHQ/fastmcp/pull/4684)
+### Dependencies 📦
+* chore(deps): bump mcp from 1.26.0 to 1.27.2 in /examples/testing_demo in the uv group across 1 directory by [@dependabot](https://github.com/apps/dependabot) in [#4514](https://github.com/PrefectHQ/fastmcp/pull/4514)
+* chore(deps): bump actions/setup-node from 6 to 7 by [@dependabot](https://github.com/apps/dependabot) in [#4546](https://github.com/PrefectHQ/fastmcp/pull/4546)
+* Bump actions/upload-artifact from 4 to 7 by [@dependabot](https://github.com/apps/dependabot) in [#4640](https://github.com/PrefectHQ/fastmcp/pull/4640)
+* Bump actions/setup-python from 6 to 7 by [@dependabot](https://github.com/apps/dependabot) in [#4641](https://github.com/PrefectHQ/fastmcp/pull/4641)
+* chore(deps): bump mcp from 1.27.2 to 1.28.1 in /examples/testing_demo in the uv group across 1 directory by [@dependabot](https://github.com/apps/dependabot) in [#4614](https://github.com/PrefectHQ/fastmcp/pull/4614)
+### Other Changes 🦾
+* Test: HTTP lifespan fires once per process across sessions by [@jlowin](https://github.com/jlowin) in [#4470](https://github.com/PrefectHQ/fastmcp/pull/4470)
+## New Contributors
+* @syf2211 made their first contribution in [#4502](https://github.com/PrefectHQ/fastmcp/pull/4502)
+* @earfman made their first contribution in [#4517](https://github.com/PrefectHQ/fastmcp/pull/4517)
+* @winklemad made their first contribution in [#4492](https://github.com/PrefectHQ/fastmcp/pull/4492)
+* @verdie-g made their first contribution in [#4512](https://github.com/PrefectHQ/fastmcp/pull/4512)
+* @vijaydeepsinha made their first contribution in [#4411](https://github.com/PrefectHQ/fastmcp/pull/4411)
+* @piaudonn made their first contribution in [#4267](https://github.com/PrefectHQ/fastmcp/pull/4267)
+* @szepeviktor made their first contribution in [#4498](https://github.com/PrefectHQ/fastmcp/pull/4498)
+* @endofcake made their first contribution in [#4412](https://github.com/PrefectHQ/fastmcp/pull/4412)
+* @Kludex made their first contribution in [#4564](https://github.com/PrefectHQ/fastmcp/pull/4564)
+* @RachGranville made their first contribution in [#4568](https://github.com/PrefectHQ/fastmcp/pull/4568)
+* @hxaxd made their first contribution in [#4533](https://github.com/PrefectHQ/fastmcp/pull/4533)
+* @VectorPeak made their first contribution in [#4531](https://github.com/PrefectHQ/fastmcp/pull/4531)
+* @LarryHu0217 made their first contribution in [#4587](https://github.com/PrefectHQ/fastmcp/pull/4587)
+* @andrew-stelmach-fleet made their first contribution in [#4633](https://github.com/PrefectHQ/fastmcp/pull/4633)
+* @Aman071106 made their first contribution in [#4628](https://github.com/PrefectHQ/fastmcp/pull/4628)
+* @ShuyingZhang made their first contribution in [#4639](https://github.com/PrefectHQ/fastmcp/pull/4639)
+* @Educg550 made their first contribution in [#4660](https://github.com/PrefectHQ/fastmcp/pull/4660)
+
+**Full Changelog**: [v3.4.5...v4.0.0b1](https://github.com/PrefectHQ/fastmcp/compare/v3.4.5...v4.0.0b1)
+
+
+
+
+
+**[v3.4.5: Key Change](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.5)**
+
+FastMCP 3.4.5 collects five fixes for the 3.x line, led by `JWTVerifier` no longer rejecting every token when an authorization server publishes an unrecognized key type such as Ed25519.
+
+### Fixes 🐞
+* Backport #4517 to release/3.x: skip unsupported JWKS keys (#4515) by [@kakiii](https://github.com/kakiii) in [#4631](https://github.com/PrefectHQ/fastmcp/pull/4631)
+* Backport #4469 to release/3.x: fix Azure scope fallback by [@jlowin](https://github.com/jlowin) in [#4662](https://github.com/PrefectHQ/fastmcp/pull/4662)
+* Backport #4523 to release/3.x: serialize deep object query parameters by [@jlowin](https://github.com/jlowin) in [#4664](https://github.com/PrefectHQ/fastmcp/pull/4664)
+* Backport #4564 to release/3.x: make transformed tool required order deterministic by [@jlowin](https://github.com/jlowin) in [#4665](https://github.com/PrefectHQ/fastmcp/pull/4665)
+* Backport #4492 to release/3.x: don't mutate the caller's schema in compress_schema by [@jlowin](https://github.com/jlowin) in [#4663](https://github.com/PrefectHQ/fastmcp/pull/4663)
+
+## New Contributors
+* @kakiii made their first contribution in [#4631](https://github.com/PrefectHQ/fastmcp/pull/4631)
+
+**Full Changelog**: [v3.4.4...v3.4.5](https://github.com/PrefectHQ/fastmcp/compare/v3.4.4...v3.4.5)
+
+
+
+
+
+**[v3.4.4: Host in Translation](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.4)**
+
+FastMCP 3.4.4 restores HTTP deployment compatibility after the 3.4.3 Host/Origin guard changed default behavior for existing ASGI, serverless, and reverse-proxy deployments. The guard implementation remains available for deployments that opt in with explicit trusted hosts and origins, while 3.x returns to accepting traffic that worked before the patch. This release also adds Hugging Face OAuth provider support, with docs and examples for public and private apps, PKCE, Dynamic Client Registration, and CIMD.
+
+### Enhancements ✨
+* Hugging Face Auth Integration by [@evalstate](https://github.com/evalstate) in [#4385](https://github.com/PrefectHQ/fastmcp/pull/4385)
+### Fixes 🐞
+* Relax host origin guard defaults by [@jlowin](https://github.com/jlowin) in [#4439](https://github.com/PrefectHQ/fastmcp/pull/4439)
+* Restore HTTP host guard compatibility by [@jlowin](https://github.com/jlowin) in [#4472](https://github.com/PrefectHQ/fastmcp/pull/4472)
+
+## New Contributors
+* @evalstate made their first contribution in [#4385](https://github.com/PrefectHQ/fastmcp/pull/4385)
+
+**Full Changelog**: [v3.4.3...v3.4.4](https://github.com/PrefectHQ/fastmcp/compare/v3.4.3...v3.4.4)
+
+
+
+
+
+**[v3.4.3: The Fast and the Secure-ious](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.3)**
+
+FastMCP 3.4.3 closes out a month of SSRF and OAuth hardening: NAT64, 6to4, Teredo, and ISATAP transition addresses can no longer smuggle private IPv4 targets past the SSRF allow-list, Streamable HTTP now validates Host and Origin before session handling to block DNS rebinding against localhost-bound servers, and OAuth redirect validation rejects unsafe schemes and unregistered DCR redirect URIs. Alongside the security work, this release also fixes proxy session teardown races, discriminator-tag handling in JSON schema conversion, and several smaller reliability issues.
+
+### Enhancements ✨
+* Dedupe discriminator-required helper across schema converters by [@jlowin](https://github.com/jlowin) in [#4362](https://github.com/PrefectHQ/fastmcp/pull/4362)
+* Add real Monty sandbox e2e coverage for CodeMode call_tool by [@AlexlaGuardia](https://github.com/AlexlaGuardia) in [#4274](https://github.com/PrefectHQ/fastmcp/pull/4274)
+* Switch prettier hook to rbubley/mirrors-prettier by [@jlowin](https://github.com/jlowin) in [#4366](https://github.com/PrefectHQ/fastmcp/pull/4366)
+* feat(remote): add --verify flag for TLS certificate verification by [@jlowin](https://github.com/jlowin) in [#4369](https://github.com/PrefectHQ/fastmcp/pull/4369)
+### Security 🔒
+* fix(deps): clear Dependabot security alerts via lockfile bumps by [@jlowin](https://github.com/jlowin) in [#4393](https://github.com/PrefectHQ/fastmcp/pull/4393)
+* Clarify resource path parameter safety by [@jlowin](https://github.com/jlowin) in [#4398](https://github.com/PrefectHQ/fastmcp/pull/4398)
+* Fix dev apps launch escaping by [@jlowin](https://github.com/jlowin) in [#4399](https://github.com/PrefectHQ/fastmcp/pull/4399)
+* Block NAT64 SSRF bypass by [@jlowin](https://github.com/jlowin) in [#4400](https://github.com/PrefectHQ/fastmcp/pull/4400)
+* [codex] Fix event store replay isolation by [@jlowin](https://github.com/jlowin) in [#4402](https://github.com/PrefectHQ/fastmcp/pull/4402)
+* Fix DCR redirect URI validation by [@jlowin](https://github.com/jlowin) in [#4408](https://github.com/PrefectHQ/fastmcp/pull/4408)
+* Protect streamable HTTP from DNS rebinding by [@jlowin](https://github.com/jlowin) in [#4405](https://github.com/PrefectHQ/fastmcp/pull/4405)
+* Block unsafe OAuth redirect schemes by [@jlowin](https://github.com/jlowin) in [#4419](https://github.com/PrefectHQ/fastmcp/pull/4419)
+* Block IPv6 transition SSRF bypasses by [@jlowin](https://github.com/jlowin) in [#4426](https://github.com/PrefectHQ/fastmcp/pull/4426)
+### Fixes 🐞
+* fix: caching middleware TypeError on cache miss due to mismatched call_next parameter by [@gmenziesint](https://github.com/gmenziesint) in [#4301](https://github.com/PrefectHQ/fastmcp/pull/4301)
+* Fix: async rate limiting middleware get_client_id callbacks by [@Chotom](https://github.com/Chotom) in [#4319](https://github.com/PrefectHQ/fastmcp/pull/4319)
+* Recognize all GitHub issue-link forms in require-issue-link workflow by [@jlowin](https://github.com/jlowin) in [#4359](https://github.com/PrefectHQ/fastmcp/pull/4359)
+* fix: preserve required discriminator tags by [@he-yufeng](https://github.com/he-yufeng) in [#4297](https://github.com/PrefectHQ/fastmcp/pull/4297)
+* fix(proxy): shield stateful proxy disconnect during session teardown by [@jlowin](https://github.com/jlowin) in [#4363](https://github.com/PrefectHQ/fastmcp/pull/4363)
+* fix(fs): isolate same-named package imports across providers by [@jlowin](https://github.com/jlowin) in [#4361](https://github.com/PrefectHQ/fastmcp/pull/4361)
+* fix: StatefulProxyClient.clear() no longer causes KeyError on session teardown by [@tcconnally](https://github.com/tcconnally) in [#4328](https://github.com/PrefectHQ/fastmcp/pull/4328)
+* fix: guard recursive refs in json_schema_to_type by [@Epochex](https://github.com/Epochex) in [#4312](https://github.com/PrefectHQ/fastmcp/pull/4312)
+* Forward IdP auth errors to MCP client instead of showing HTML error page by [@bobbyjames839](https://github.com/bobbyjames839) in [#4293](https://github.com/PrefectHQ/fastmcp/pull/4293)
+* fix(resources): round-trip path values with reserved characters in URI templates by [@jlowin](https://github.com/jlowin) in [#4368](https://github.com/PrefectHQ/fastmcp/pull/4368)
+* fix: bracket IPv6 hosts in server startup log URL by [@jlowin](https://github.com/jlowin) in [#4372](https://github.com/PrefectHQ/fastmcp/pull/4372)
+* fix: bound default OIDC discovery timeout and expose it on provider wrappers by [@jlowin](https://github.com/jlowin) in [#4374](https://github.com/PrefectHQ/fastmcp/pull/4374)
+* fix: validate task tool arguments against declared types by [@jlowin](https://github.com/jlowin) in [#4373](https://github.com/PrefectHQ/fastmcp/pull/4373)
+* fix(tools): honor serialize_by_alias in tool result serialization by [@jlowin](https://github.com/jlowin) in [#4391](https://github.com/PrefectHQ/fastmcp/pull/4391)
+* Fix/cimd flow issue by [@twjackysu](https://github.com/twjackysu) in [#4206](https://github.com/PrefectHQ/fastmcp/pull/4206)
+* Reject empty env var keys by [@CodingFeng101](https://github.com/CodingFeng101) in [#4410](https://github.com/PrefectHQ/fastmcp/pull/4410)
+* fix: correct replace_type docstring parameter descriptions by [@hiSandog](https://github.com/hiSandog) in [#4375](https://github.com/PrefectHQ/fastmcp/pull/4375)
+* Fix ty 0.0.55 diagnostics and prefab-ui protocol version drift by [@jlowin](https://github.com/jlowin) in [#4428](https://github.com/PrefectHQ/fastmcp/pull/4428)
+* [codex] Fix OpenAPI resource template requests by [@jlowin](https://github.com/jlowin) in [#4407](https://github.com/PrefectHQ/fastmcp/pull/4407)
+### Docs 📚
+* fix: RST docstrings in fastmcp.types render raw on gofastmcp.com by [@jlowin](https://github.com/jlowin) in [#4367](https://github.com/PrefectHQ/fastmcp/pull/4367)
+* docs: fix 5 broken internal links (auth & providers pages) by [@Michael-WhiteCapData](https://github.com/Michael-WhiteCapData) in [#4344](https://github.com/PrefectHQ/fastmcp/pull/4344)
+* docs: add audit/event-record recipe for tool-call middleware by [@AlexlaGuardia](https://github.com/AlexlaGuardia) in [#4345](https://github.com/PrefectHQ/fastmcp/pull/4345)
+### Dependencies 📦
+* chore(deps): bump actions/checkout from 6 to 7 by [@dependabot](https://github.com/apps/dependabot) in [#4343](https://github.com/PrefectHQ/fastmcp/pull/4343)
+* chore(deps): bump joserfc from 1.6.5 to 1.6.7 in the uv group across 1 directory by [@dependabot](https://github.com/apps/dependabot) in [#4394](https://github.com/PrefectHQ/fastmcp/pull/4394)
+* chore(deps): bump joserfc from 1.6.7 to 1.6.8 in the uv group across 1 directory by [@dependabot](https://github.com/apps/dependabot) in [#4429](https://github.com/PrefectHQ/fastmcp/pull/4429)
+### Other Changes 🦾
+* Raise fastmcp.ValidationError for invalid tool arguments by [@jlowin](https://github.com/jlowin) in [#4392](https://github.com/PrefectHQ/fastmcp/pull/4392)
+* Fix versioned auth middleware checks by [@jlowin](https://github.com/jlowin) in [#4401](https://github.com/PrefectHQ/fastmcp/pull/4401)
+
+## New Contributors
+* @gmenziesint made their first contribution in [#4301](https://github.com/PrefectHQ/fastmcp/pull/4301)
+* @Chotom made their first contribution in [#4319](https://github.com/PrefectHQ/fastmcp/pull/4319)
+* @he-yufeng made their first contribution in [#4297](https://github.com/PrefectHQ/fastmcp/pull/4297)
+* @AlexlaGuardia made their first contribution in [#4274](https://github.com/PrefectHQ/fastmcp/pull/4274)
+* @tcconnally made their first contribution in [#4328](https://github.com/PrefectHQ/fastmcp/pull/4328)
+* @Epochex made their first contribution in [#4312](https://github.com/PrefectHQ/fastmcp/pull/4312)
+* @Michael-WhiteCapData made their first contribution in [#4344](https://github.com/PrefectHQ/fastmcp/pull/4344)
+* @bobbyjames839 made their first contribution in [#4293](https://github.com/PrefectHQ/fastmcp/pull/4293)
+* @twjackysu made their first contribution in [#4206](https://github.com/PrefectHQ/fastmcp/pull/4206)
+* @CodingFeng101 made their first contribution in [#4410](https://github.com/PrefectHQ/fastmcp/pull/4410)
+* @hiSandog made their first contribution in [#4375](https://github.com/PrefectHQ/fastmcp/pull/4375)
+
+**Full Changelog**: [v3.4.2...v3.4.3](https://github.com/PrefectHQ/fastmcp/compare/v3.4.2...v3.4.3)
+
+
+
+
+
+**[v3.4.2: Heads Up](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.2)**
+
+FastMCP 3.4.2 restores JWT compatibility for providers that include private, non-critical JWS header parameters. Tokens from providers like Clerk can carry header metadata such as `cat` without being rejected before signature and claim validation, while unsupported critical headers are still rejected.
+
+### Fixes 🐞
+* Allow private JWT headers by [@jlowin](https://github.com/jlowin) in [#4290](https://github.com/PrefectHQ/fastmcp/pull/4290)
+### Docs 📚
+* Docs: add v3.4.1 changelog entries by [@jlowin](https://github.com/jlowin) in [#4289](https://github.com/PrefectHQ/fastmcp/pull/4289)
+
+**Full Changelog**: [v3.4.1...v3.4.2](https://github.com/PrefectHQ/fastmcp/compare/v3.4.1...v3.4.2)
+
+
+
+
+
+**[v3.4.1: Floor It](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.1)**
+
+FastMCP 3.4.1 floors Starlette at `>=1.0.1` so installs can no longer resolve to a version affected by CVE-2026-48710, which was previously only constrained transitively through `mcp`. It also makes OAuthProxy log refresh-token cache misses instead of failing silently.
+
+### Enhancements ✨
+* Log refresh-token misses in OAuthProxy instead of failing silently by [@jlowin](https://github.com/jlowin) in [#4276](https://github.com/PrefectHQ/fastmcp/pull/4276)
+### Security 🔒
+* Add explicit starlette>=1.0.1 floor (CVE-2026-48710) by [@jlowin](https://github.com/jlowin) in [#4286](https://github.com/PrefectHQ/fastmcp/pull/4286)
+### Docs 📚
+* Document --notes-start-tag in release instructions by [@jlowin](https://github.com/jlowin) in [#4275](https://github.com/PrefectHQ/fastmcp/pull/4275)
+
+**Full Changelog**: [v3.4.0...v3.4.1](https://github.com/PrefectHQ/fastmcp/compare/v3.4.0...v3.4.1)
+
+
+
+
+
+**[v3.4.0: Remote Control](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.0)**
+
+FastMCP 3.4 is about reaching servers that live somewhere else. The headline is `fastmcp-remote`, a standalone bridge that connects stdio-only MCP hosts to servers hosted over HTTP. Around it, the proxy layer those connections depend on is hardened: a proxy now forwards `initialize` upstream and fails loudly when the backend is missing or misconfigured, instead of reporting a connected-but-empty proxy. And FastMCP-issued access tokens can now outlive short-lived upstream tokens, so authenticated sessions survive the long idle periods remote clients are prone to.
+
+### New Features 🎉
+* Add fastmcp-remote bridge package by [@jlowin](https://github.com/jlowin) in [#4208](https://github.com/PrefectHQ/fastmcp/pull/4208)
+### Breaking Changes ⚠️
+* Forward proxy initialize as bridge behavior by [@jlowin](https://github.com/jlowin) in [#4228](https://github.com/PrefectHQ/fastmcp/pull/4228)
+### Enhancements ✨
+* ci: require external PRs to link a tracked issue by [@strawgate](https://github.com/strawgate) in [#4173](https://github.com/PrefectHQ/fastmcp/pull/4173)
+* feat: new options --host and --no-log-panel | --log-panel to cli dev apps by [@itaru2622](https://github.com/itaru2622) in [#4123](https://github.com/PrefectHQ/fastmcp/pull/4123)
+* Add valid_scopes and extra_authorize_params to WorkOSProvider by [@tiagoskaneta](https://github.com/tiagoskaneta) in [#4135](https://github.com/PrefectHQ/fastmcp/pull/4135)
+* Add token_expiry_threshold_seconds for proactive token refresh by [@mohankumarelec](https://github.com/mohankumarelec) in [#4142](https://github.com/PrefectHQ/fastmcp/pull/4142)
+* Add review-issue skill for triaging gated external contributions by [@jlowin](https://github.com/jlowin) in [#4212](https://github.com/PrefectHQ/fastmcp/pull/4212)
+* Add contract gate to review-issue skill by [@jlowin](https://github.com/jlowin) in [#4214](https://github.com/PrefectHQ/fastmcp/pull/4214)
+* Let ToolResult return an error result via is_error by [@jlowin](https://github.com/jlowin) in [#4217](https://github.com/PrefectHQ/fastmcp/pull/4217)
+* Update published docs after PyPI release by [@jlowin](https://github.com/jlowin) in [#4211](https://github.com/PrefectHQ/fastmcp/pull/4211)
+* Allow pre-bound HTTP sockets by [@jlowin](https://github.com/jlowin) in [#4222](https://github.com/PrefectHQ/fastmcp/pull/4222)
+* Add targeted coverage tests by [@strawgate](https://github.com/strawgate) in [#4230](https://github.com/PrefectHQ/fastmcp/pull/4230)
+* Upgrade ty to 0.0.39 by [@jlowin](https://github.com/jlowin) in [#4225](https://github.com/PrefectHQ/fastmcp/pull/4225)
+* Decouple FastMCP access token lifetime from upstream expires_in by [@jlowin](https://github.com/jlowin) in [#4254](https://github.com/PrefectHQ/fastmcp/pull/4254)
+### Security 🔒
+* feat(code-mode): default sandbox limits and per-execution tool-call cap by [@strawgate](https://github.com/strawgate) in [#4170](https://github.com/PrefectHQ/fastmcp/pull/4170)
+* Security: Fix 3 findings in GitHub Actions workflows by [@jpr5](https://github.com/jpr5) in [#4183](https://github.com/PrefectHQ/fastmcp/pull/4183)
+* Add outbound comment guardrails by [@jlowin](https://github.com/jlowin) in [#4196](https://github.com/PrefectHQ/fastmcp/pull/4196)
+* Add uv dependency cooldown by [@jlowin](https://github.com/jlowin) in [#4213](https://github.com/PrefectHQ/fastmcp/pull/4213)
+### Fixes 🐞
+* fix: VersionSpec eq matching normalizes versions and selects deterministically by [@strawgate](https://github.com/strawgate) in [#4058](https://github.com/PrefectHQ/fastmcp/pull/4058)
+* fix(tests): hoist azure-identity import out of the OBO test timeout window by [@strawgate](https://github.com/strawgate) in [#4176](https://github.com/PrefectHQ/fastmcp/pull/4176)
+* fix(auth): disambiguate auth-denied vs missing component messages by [@strawgate](https://github.com/strawgate) in [#4165](https://github.com/PrefectHQ/fastmcp/pull/4165)
+* fix: preserve annotations, meta, title, icons when creating resources from templates by [@strawgate](https://github.com/strawgate) in [#4061](https://github.com/PrefectHQ/fastmcp/pull/4061)
+* fix: add OTEL spans to sampling step and tool execution by [@strawgate](https://github.com/strawgate) in [#4059](https://github.com/PrefectHQ/fastmcp/pull/4059)
+* fix(config): read MCP config files as UTF-8 by [@pragnyanramtha](https://github.com/pragnyanramtha) in [#4164](https://github.com/PrefectHQ/fastmcp/pull/4164)
+* fix(schema): preserve root metadata on fallback by [@yuyua9](https://github.com/yuyua9) in [#4178](https://github.com/PrefectHQ/fastmcp/pull/4178)
+* fix(proxy): restore _current_server in _restore_request_context by [@strawgate](https://github.com/strawgate) in [#4168](https://github.com/PrefectHQ/fastmcp/pull/4168)
+* fix(auth): add /.well-known/openid-configuration alias for OAuth server metadata by [@shigechika](https://github.com/shigechika) in [#4167](https://github.com/PrefectHQ/fastmcp/pull/4167)
+* fix(code-mode): cancel Monty sandbox future on task cancellation by [@strawgate](https://github.com/strawgate) in [#4169](https://github.com/PrefectHQ/fastmcp/pull/4169)
+* fix(auth): unprefix Azure scopes echoed back to MCP clients by [@rgillinlz](https://github.com/rgillinlz) in [#4130](https://github.com/PrefectHQ/fastmcp/pull/4130)
+* fix(cli): forward stateless flag in uv run path by [@yuyua9](https://github.com/yuyua9) in [#4177](https://github.com/PrefectHQ/fastmcp/pull/4177)
+* fix(ci): scope minimize-reviews concurrency by event name by [@strawgate](https://github.com/strawgate) in [#4174](https://github.com/PrefectHQ/fastmcp/pull/4174)
+* Fix docs app demo iframe assets by [@jlowin](https://github.com/jlowin) in [#4194](https://github.com/PrefectHQ/fastmcp/pull/4194)
+* Guard require-issue-link check job to pull_request_target events by [@jlowin](https://github.com/jlowin) in [#4209](https://github.com/PrefectHQ/fastmcp/pull/4209)
+* Migrate auth JWTs to joserfc by [@jlowin](https://github.com/jlowin) in [#4221](https://github.com/PrefectHQ/fastmcp/pull/4221)
+* Skip published docs update for prereleases by [@jlowin](https://github.com/jlowin) in [#4224](https://github.com/PrefectHQ/fastmcp/pull/4224)
+* Surface proxy upstream failures by [@jlowin](https://github.com/jlowin) in [#4227](https://github.com/PrefectHQ/fastmcp/pull/4227)
+* Close upstream OAuth clients by [@jlowin](https://github.com/jlowin) in [#4248](https://github.com/PrefectHQ/fastmcp/pull/4248)
+* Fix GitHub MCP resource integration test by [@jlowin](https://github.com/jlowin) in [#4253](https://github.com/PrefectHQ/fastmcp/pull/4253)
+* Fix resource templates with query params on proxied servers by [@rene84](https://github.com/rene84) in [#4251](https://github.com/PrefectHQ/fastmcp/pull/4251)
+### Docs 📚
+* Document pip upgrade recovery for the fastmcp-slim package split by [@jlowin](https://github.com/jlowin) in [#4215](https://github.com/PrefectHQ/fastmcp/pull/4215)
+* Move pip upgrade recovery into a Troubleshooting section by [@jlowin](https://github.com/jlowin) in [#4219](https://github.com/PrefectHQ/fastmcp/pull/4219)
+* Restore Horizon docs banner by [@jlowin](https://github.com/jlowin) in [#4240](https://github.com/PrefectHQ/fastmcp/pull/4240)
+* fix: Trendshift link and badge in README.md by [@bhantos](https://github.com/bhantos) in [#4236](https://github.com/PrefectHQ/fastmcp/pull/4236)
+* docs: add tool fingerprinting recipe by [@dgenio](https://github.com/dgenio) in [#4233](https://github.com/PrefectHQ/fastmcp/pull/4233)
+### Dependencies 📦
+* chore(deps): bump the uv group across 2 directories with 1 update by [@dependabot](https://github.com/dependabot) in [#4113](https://github.com/PrefectHQ/fastmcp/pull/4113)
+* chore(deps-dev): bump pydantic-monty from 0.0.16 to 0.0.17 by [@dependabot](https://github.com/dependabot) in [#4023](https://github.com/PrefectHQ/fastmcp/pull/4023)
+### Other Changes 🦾
+* Exempt maintainers from MRE auto-close by [@jlowin](https://github.com/jlowin) in [#4220](https://github.com/PrefectHQ/fastmcp/pull/4220)
+
+## New Contributors
+* @pragnyanramtha made their first contribution in [#4164](https://github.com/PrefectHQ/fastmcp/pull/4164)
+* @yuyua9 made their first contribution in [#4178](https://github.com/PrefectHQ/fastmcp/pull/4178)
+* @tiagoskaneta made their first contribution in [#4135](https://github.com/PrefectHQ/fastmcp/pull/4135)
+* @mohankumarelec made their first contribution in [#4142](https://github.com/PrefectHQ/fastmcp/pull/4142)
+* @rgillinlz made their first contribution in [#4130](https://github.com/PrefectHQ/fastmcp/pull/4130)
+* @jpr5 made their first contribution in [#4183](https://github.com/PrefectHQ/fastmcp/pull/4183)
+* @bhantos made their first contribution in [#4236](https://github.com/PrefectHQ/fastmcp/pull/4236)
+* @rene84 made their first contribution in [#4251](https://github.com/PrefectHQ/fastmcp/pull/4251)
+
+**Full Changelog**: [v3.3.1...v3.4.0](https://github.com/PrefectHQ/fastmcp/compare/v3.3.1...v3.4.0)
+
+
+
+
+
+**[v3.3.1: Loop There It Is](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.3.1)**
+
+A hotfix for the 3.3 packaging split. Clean installs could fail on standalone component imports like `from fastmcp.tools import tool`, because component modules reached auth and task primitives through `fastmcp.server` and pulled in the full server/provider stack. Those primitives now live in lightweight utility modules, with the old server import paths preserved as compatibility re-exports.
+
+### Fixes 🐞
+* fix(docs): use valid FA icon on client-only package page by [@jlowin](https://github.com/jlowin) in [#4139](https://github.com/PrefectHQ/fastmcp/pull/4139)
+* Decouple component imports from server by [@jlowin](https://github.com/jlowin) in [#4150](https://github.com/PrefectHQ/fastmcp/pull/4150)
+
+
+**Full Changelog**: [v3.3.0...v3.3.1](https://github.com/PrefectHQ/fastmcp/compare/v3.3.0...v3.3.1)
+
+
+
+
+
+**[v3.3.0: Slim Reaper](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.3.0)**
+
+FastMCP 3.3 ships `fastmcp-slim`, a dependency-light distribution that separates the client from the server stack — install FastMCP's client and transport layer without Starlette, Uvicorn, or the rest of the server machinery. The import namespace is unchanged. It also closes out a backlog of OAuth proxy security hardening, MCP-compliant OTEL instrumentation, and auth additions that accumulated through the 3.2 cycle.
+
+### New Features 🎉
+* Add fastmcp-slim for client-only installs by [@jlowin](https://github.com/jlowin) in [#4122](https://github.com/PrefectHQ/fastmcp/pull/4122)
+### Enhancements ✨
+* Add default prefill to FormInput.collect_input by [@jlowin](https://github.com/jlowin) in [#3937](https://github.com/PrefectHQ/fastmcp/pull/3937)
+* OTEL: Fix attribute compliance with MCP semantic conventions by [@strawgate](https://github.com/strawgate) in [#3889](https://github.com/PrefectHQ/fastmcp/pull/3889)
+* OTEL: Instrument all MCP list operations and enrich delegate spans by [@strawgate](https://github.com/strawgate) in [#3890](https://github.com/PrefectHQ/fastmcp/pull/3890)
+* Improve real-world schema crash test: failure dump, cluster analysis, TypeErrors baseline ratchet by [@jlowin](https://github.com/jlowin) in [#3958](https://github.com/PrefectHQ/fastmcp/pull/3958)
+* feat: add AzureB2CProvider for Azure AD B2C user flows by [@carlos-rian](https://github.com/carlos-rian) in [#3995](https://github.com/PrefectHQ/fastmcp/pull/3995)
+* Add run_in_thread opt-out for sync tools with thread affinity by [@jlowin](https://github.com/jlowin) in [#4010](https://github.com/PrefectHQ/fastmcp/pull/4010)
+* Add missing return type annotation to __getattr__ by [@ZLeventer](https://github.com/ZLeventer) in [#4026](https://github.com/PrefectHQ/fastmcp/pull/4026)
+* Add experimental_capabilities kwarg to FastMCP constructor by [@jlowin](https://github.com/jlowin) in [#4042](https://github.com/PrefectHQ/fastmcp/pull/4042)
+* Add log_level parameter to FastMCP errors by [@daniel-tsiang](https://github.com/daniel-tsiang) in [#4036](https://github.com/PrefectHQ/fastmcp/pull/4036)
+* Bump pydocket to 0.20.0 by [@chrisguidry](https://github.com/chrisguidry) in [#4031](https://github.com/PrefectHQ/fastmcp/pull/4031)
+* enh: Add public API for updating OAuthProxy scopes after initialization by [@taylorwilsdon](https://github.com/taylorwilsdon) in [#4091](https://github.com/PrefectHQ/fastmcp/pull/4091)
+* Refine fastmcp-slim packaging by [@jlowin](https://github.com/jlowin) in [#4125](https://github.com/PrefectHQ/fastmcp/pull/4125)
+### Security 🔒
+* Harden OAuth Proxy silent consent against AS-in-the-middle by [@jlowin](https://github.com/jlowin) in [#3960](https://github.com/PrefectHQ/fastmcp/pull/3960)
+* Reject dot-segments in redirect URI allowlist matching by [@jlowin](https://github.com/jlowin) in [#3963](https://github.com/PrefectHQ/fastmcp/pull/3963)
+* Bump deps with open dependabot alerts by [@jlowin](https://github.com/jlowin) in [#3965](https://github.com/PrefectHQ/fastmcp/pull/3965)
+* Partition ResponseCachingMiddleware cache by access token by [@jlowin](https://github.com/jlowin) in [#4041](https://github.com/PrefectHQ/fastmcp/pull/4041)
+### Fixes 🐞
+* fix: reject self-mount to prevent infinite recursion by [@strawgate](https://github.com/strawgate) in [#3925](https://github.com/PrefectHQ/fastmcp/pull/3925)
+* fix: ProxyTool crashes on non-TextContent error responses by [@strawgate](https://github.com/strawgate) in [#3926](https://github.com/PrefectHQ/fastmcp/pull/3926)
+* fix: _prune_param and _convert_nullable_field mutate input schemas by [@strawgate](https://github.com/strawgate) in [#3927](https://github.com/PrefectHQ/fastmcp/pull/3927)
+* fix: narrow OpenAI audio format dict to Literal for ty by [@jlowin](https://github.com/jlowin) in [#3936](https://github.com/PrefectHQ/fastmcp/pull/3936)
+* fix: allow hyphens in resource template parameter names by [@strawgate](https://github.com/strawgate) in [#3929](https://github.com/PrefectHQ/fastmcp/pull/3929)
+* fix: OpenAPI request director sends multipart and form-urlencoded as JSON by [@strawgate](https://github.com/strawgate) in [#3932](https://github.com/PrefectHQ/fastmcp/pull/3932)
+* Fix raise_on_error handling for tool tasks by [@gnanirahulnutakki](https://github.com/gnanirahulnutakki) in [#3946](https://github.com/PrefectHQ/fastmcp/pull/3946)
+* fix: FileSystemProvider reload race condition by [@strawgate](https://github.com/strawgate) in [#3938](https://github.com/PrefectHQ/fastmcp/pull/3938)
+* fix tests that relied on task=True returning error results by [@jlowin](https://github.com/jlowin) in [#3954](https://github.com/PrefectHQ/fastmcp/pull/3954)
+* Restore task snapshot via a worker-level dependency by [@chrisguidry](https://github.com/chrisguidry) in [#3945](https://github.com/PrefectHQ/fastmcp/pull/3945)
+* Forward backend capabilities in ProxyProvider by [@jlowin](https://github.com/jlowin) in [#3956](https://github.com/PrefectHQ/fastmcp/pull/3956)
+* Allow upstream client_id to be used directly without DCR by [@jlowin](https://github.com/jlowin) in [#3957](https://github.com/PrefectHQ/fastmcp/pull/3957)
+* Graceful fallback for unsupported regex patterns in json_schema_to_type by [@jlowin](https://github.com/jlowin) in [#3959](https://github.com/PrefectHQ/fastmcp/pull/3959)
+* Revert "Forward backend capabilities in ProxyProvider (#3956)" by [@jlowin](https://github.com/jlowin) in [#3964](https://github.com/PrefectHQ/fastmcp/pull/3964)
+* fix: skip stdio subprocess test on Windows CI by [@jlowin](https://github.com/jlowin) in [#3966](https://github.com/PrefectHQ/fastmcp/pull/3966)
+* fix: bound _refresh_locks with LRU eviction to prevent memory leak by [@jlowin](https://github.com/jlowin) in [#3968](https://github.com/PrefectHQ/fastmcp/pull/3968)
+* fix: handle circular JSON Pointer $ref in dereference_refs by [@lawrence3699](https://github.com/lawrence3699) in [#3896](https://github.com/PrefectHQ/fastmcp/pull/3896)
+* fix: honor upstream refresh token expiry in OAuthProxy by [@jlowin](https://github.com/jlowin) in [#3990](https://github.com/PrefectHQ/fastmcp/pull/3990)
+* fix: narrow _token_validator with isinstance for ty in AzureProvider.from_b2c by [@jlowin](https://github.com/jlowin) in [#4007](https://github.com/PrefectHQ/fastmcp/pull/4007)
+* fix: cancel orphaned session_task when Client._disconnect times out by [@jlowin](https://github.com/jlowin) in [#4011](https://github.com/PrefectHQ/fastmcp/pull/4011)
+* fix: preserve @tool metadata in from_function by [@lawrence3699](https://github.com/lawrence3699) in [#4072](https://github.com/PrefectHQ/fastmcp/pull/4072)
+* fix(openapi): keep blank values in parse_qs (refs #4056) by [@MukundaKatta](https://github.com/MukundaKatta) in [#4076](https://github.com/PrefectHQ/fastmcp/pull/4076)
+* Fix #4056: keep blank query values, add token bucket regression test by [@MukundaKatta](https://github.com/MukundaKatta) in [#4069](https://github.com/PrefectHQ/fastmcp/pull/4069)
+* fix(ping): exit ping loop cleanly when session stream is closed by [@ashwin153](https://github.com/ashwin153) in [#4087](https://github.com/PrefectHQ/fastmcp/pull/4087)
+* Fix sampling from background tasks by [@cuyua9](https://github.com/cuyua9) in [#4068](https://github.com/PrefectHQ/fastmcp/pull/4068)
+* Make Docket reentrant; mounted servers enter their own lifespan by [@jlowin](https://github.com/jlowin) in [#4095](https://github.com/PrefectHQ/fastmcp/pull/4095)
+* fix(tool_transform): hoist $defs to schema root when ArgTransform introduces them by [@SarthakB11](https://github.com/SarthakB11) in [#4101](https://github.com/PrefectHQ/fastmcp/pull/4101)
+* fix(auth): silence authlib.jose DeprecationWarning at JWT import by [@SarthakB11](https://github.com/SarthakB11) in [#4100](https://github.com/PrefectHQ/fastmcp/pull/4100)
+* fix: don't cache import map in dev apps bundle by [@jlowin](https://github.com/jlowin) in [#4106](https://github.com/PrefectHQ/fastmcp/pull/4106)
+* #4084 [Issues] Windows startup crash due to UnicodeDecodeError when l… by [@doneman536](https://github.com/doneman536) in [#4092](https://github.com/PrefectHQ/fastmcp/pull/4092)
+* fix: drop exc_info for expected tool failures, remove unreachable ValidationError by [@sergeykad](https://github.com/sergeykad) in [#4029](https://github.com/PrefectHQ/fastmcp/pull/4029)
+* fix: cli option --no-banner is NOT passed to cli but server-spec in-correctly when cli --reload option is specified. by [@itaru2622](https://github.com/itaru2622) in [#4083](https://github.com/PrefectHQ/fastmcp/pull/4083)
+* Fix None backend_* span attributes on un-renamed proxy components by [@ringerc](https://github.com/ringerc) in [#4109](https://github.com/PrefectHQ/fastmcp/pull/4109)
+* Fix OCI Provider issue in 3.x version. Add OCI auth provider example … by [@kiranthakkar](https://github.com/kiranthakkar) in [#4116](https://github.com/PrefectHQ/fastmcp/pull/4116)
+* fix(http): terminate active streamable-HTTP transports before lifespan shutdown by [@SarthakB11](https://github.com/SarthakB11) in [#4118](https://github.com/PrefectHQ/fastmcp/pull/4118)
+### Docs 📚
+* Restructure docs navigation by [@jlowin](https://github.com/jlowin) in [#3951](https://github.com/PrefectHQ/fastmcp/pull/3951)
+* docs: standardize ToolAnnotations examples by [@gnanirahulnutakki](https://github.com/gnanirahulnutakki) in [#3952](https://github.com/PrefectHQ/fastmcp/pull/3952)
+* Be constructively skeptical of bot reviews on own PRs by [@jlowin](https://github.com/jlowin) in [#3971](https://github.com/PrefectHQ/fastmcp/pull/3971)
+* Add UTM params to Horizon docs links by [@aaazzam](https://github.com/aaazzam) in [#4018](https://github.com/PrefectHQ/fastmcp/pull/4018)
+* Add a sandboxed-agents deployment guide by [@strawgate](https://github.com/strawgate) in [#4027](https://github.com/PrefectHQ/fastmcp/pull/4027)
+* docs: add best practices for custom telemetry spans by [@MukundaKatta](https://github.com/MukundaKatta) in [#4001](https://github.com/PrefectHQ/fastmcp/pull/4001)
+* Refresh landing page copy by [@jlowin](https://github.com/jlowin) in [#4043](https://github.com/PrefectHQ/fastmcp/pull/4043)
+* Refresh landing page copy by [@jlowin](https://github.com/jlowin) in [#4047](https://github.com/PrefectHQ/fastmcp/pull/4047)
+* Add UTM tracking to Horizon links by [@jlowin](https://github.com/jlowin) in [#4064](https://github.com/PrefectHQ/fastmcp/pull/4064)
+* docs(integrations): add Pydantic AI FastMCP toolset guide by [@MukundaKatta](https://github.com/MukundaKatta) in [#4070](https://github.com/PrefectHQ/fastmcp/pull/4070)
+* docs: fix broken links in Pydantic AI guide by [@jlowin](https://github.com/jlowin) in [#4094](https://github.com/PrefectHQ/fastmcp/pull/4094)
+### Dependencies 📦
+* chore(deps-dev): bump pydantic-monty from 0.0.11 to 0.0.12 by [@dependabot](https://github.com/dependabot) in [#3940](https://github.com/PrefectHQ/fastmcp/pull/3940)
+* chore(deps-dev): bump pydantic-monty from 0.0.14 to 0.0.16 by [@dependabot](https://github.com/dependabot) in [#3984](https://github.com/PrefectHQ/fastmcp/pull/3984)
+### Other Changes 🦾
+* fix: Don't completely hide plain mcp.tool app-only tools by [@owtaylor](https://github.com/owtaylor) in [#4112](https://github.com/PrefectHQ/fastmcp/pull/4112)
+
+## New Contributors
+* @gnanirahulnutakki made their first contribution in [#3946](https://github.com/PrefectHQ/fastmcp/pull/3946)
+* @lawrence3699 made their first contribution in [#3896](https://github.com/PrefectHQ/fastmcp/pull/3896)
+* @carlos-rian made their first contribution in [#3995](https://github.com/PrefectHQ/fastmcp/pull/3995)
+* @ZLeventer made their first contribution in [#4026](https://github.com/PrefectHQ/fastmcp/pull/4026)
+* @MukundaKatta made their first contribution in [#4001](https://github.com/PrefectHQ/fastmcp/pull/4001)
+* @daniel-tsiang made their first contribution in [#4036](https://github.com/PrefectHQ/fastmcp/pull/4036)
+* @ashwin153 made their first contribution in [#4087](https://github.com/PrefectHQ/fastmcp/pull/4087)
+* @cuyua9 made their first contribution in [#4068](https://github.com/PrefectHQ/fastmcp/pull/4068)
+* @taylorwilsdon made their first contribution in [#4091](https://github.com/PrefectHQ/fastmcp/pull/4091)
+* @SarthakB11 made their first contribution in [#4101](https://github.com/PrefectHQ/fastmcp/pull/4101)
+* @doneman536 made their first contribution in [#4092](https://github.com/PrefectHQ/fastmcp/pull/4092)
+* @sergeykad made their first contribution in [#4029](https://github.com/PrefectHQ/fastmcp/pull/4029)
+* @ringerc made their first contribution in [#4109](https://github.com/PrefectHQ/fastmcp/pull/4109)
+
+**Full Changelog**: [v3.2.4...v3.3.0](https://github.com/PrefectHQ/fastmcp/compare/v3.2.4...v3.3.0)
+
+
+
+
+
+**[v3.2.4: Patch Me If You Can](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.2.4)**
+
+A grab bag of fixes, hardening, and polish. The headline behavior change: background tasks are now scoped to the authorization context rather than the MCP session, so a task survives session churn and stays tied to who started it — a breaking change for anyone relying on the old session-scoped semantics. Plus actual-size validation in `FileUpload`, a Keycloak OAuth provider, automatic parameter descriptions from docstrings, and dozens of schema and sampling fixes.
+
+### Breaking Changes ⚠️
+* Scope tasks to authorization context, not session by [@chrisguidry](https://github.com/chrisguidry) in [#3800](https://github.com/PrefectHQ/fastmcp/pull/3800)
+### Enhancements ✨
+* Bump pydocket>=0.19.0, drop fakeredis pin by [@chrisguidry](https://github.com/chrisguidry) in [#3822](https://github.com/PrefectHQ/fastmcp/pull/3822)
+* Add real-world schema crash test (232K schemas from APIs.guru) by [@strawgate](https://github.com/strawgate) in [#3826](https://github.com/PrefectHQ/fastmcp/pull/3826)
+* Enable 7 zero-violation ruff rules by [@strawgate](https://github.com/strawgate) in [#3841](https://github.com/PrefectHQ/fastmcp/pull/3841)
+* Promote 7 ty rules from ignore to warn by [@strawgate](https://github.com/strawgate) in [#3852](https://github.com/PrefectHQ/fastmcp/pull/3852)
+* Replace ___ with hash-based backend tool routing and per-tool prefab resources by [@jlowin](https://github.com/jlowin) in [#3824](https://github.com/PrefectHQ/fastmcp/pull/3824)
+* Enable 4 ruff rules (DTZ, ERA, ISC, INP) and fix 9 violations by [@strawgate](https://github.com/strawgate) in [#3842](https://github.com/PrefectHQ/fastmcp/pull/3842)
+* Extract parameter descriptions from docstrings by [@jlowin](https://github.com/jlowin) in [#3872](https://github.com/PrefectHQ/fastmcp/pull/3872)
+* ci: speed up schema crash test (CSafeLoader + xdist-safe aggregation) by [@jlowin](https://github.com/jlowin) in [#3873](https://github.com/PrefectHQ/fastmcp/pull/3873)
+* test: bump OpenAPI init perf threshold to 200ms for Windows CI by [@jlowin](https://github.com/jlowin) in [#3879](https://github.com/PrefectHQ/fastmcp/pull/3879)
+* refactor: unify object-schema conversion through _object_schema_to_type by [@jlowin](https://github.com/jlowin) in [#3884](https://github.com/PrefectHQ/fastmcp/pull/3884)
+* Add Keycloak OAuth Provider for Enterprise Authentication and local dev by [@stephaneberle9](https://github.com/stephaneberle9) in [#1937](https://github.com/PrefectHQ/fastmcp/pull/1937)
+* Allow auth providers to override protected resource base URLs by [@aaazzam](https://github.com/aaazzam) in [#3900](https://github.com/PrefectHQ/fastmcp/pull/3900)
+* Enable PERF and T20 ruff rules by [@strawgate](https://github.com/strawgate) in [#3845](https://github.com/PrefectHQ/fastmcp/pull/3845)
+* Add response_title and response_description to ctx.elicit() by [@jlowin](https://github.com/jlowin) in [#3912](https://github.com/PrefectHQ/fastmcp/pull/3912)
+* Deprecate ctx.elicit() without response_type by [@jlowin](https://github.com/jlowin) in [#3916](https://github.com/PrefectHQ/fastmcp/pull/3916)
+### Security 🔒
+* Validate actual base64 data size in FileUpload, not client-reported size by [@strawgate](https://github.com/strawgate) in [#3816](https://github.com/PrefectHQ/fastmcp/pull/3816)
+* Stop forwarding inbound HTTP headers to unrelated remote servers by [@jlowin](https://github.com/jlowin) in [#3837](https://github.com/PrefectHQ/fastmcp/pull/3837)
+* AuthKit: auto-bind token audience to resource URL (RFC 8707) by [@jlowin](https://github.com/jlowin) in [#3905](https://github.com/PrefectHQ/fastmcp/pull/3905)
+### Fixes 🐞
+* Version-check is_docket_available() to avoid transitive pydocket crash by [@jlowin](https://github.com/jlowin) in [#3807](https://github.com/PrefectHQ/fastmcp/pull/3807)
+* fix: materialize generators before result conversion, handle bytes gracefully by [@strawgate](https://github.com/strawgate) in [#3830](https://github.com/PrefectHQ/fastmcp/pull/3830)
+* Fix json_schema_to_type crashes on keywords, boolean schemas, empty enums, and name collisions by [@strawgate](https://github.com/strawgate) in [#3818](https://github.com/PrefectHQ/fastmcp/pull/3818)
+* fix: replace `or` with `is not None` checks for config/override merging by [@strawgate](https://github.com/strawgate) in [#3833](https://github.com/PrefectHQ/fastmcp/pull/3833)
+* fix: TransformedTool sync fn crash and schema mutation by [@strawgate](https://github.com/strawgate) in [#3823](https://github.com/PrefectHQ/fastmcp/pull/3823)
+* fix: cross-provider duplicate detection, error visibility, mask propagation by [@strawgate](https://github.com/strawgate) in [#3827](https://github.com/PrefectHQ/fastmcp/pull/3827)
+* fix: don't pass HTTP kwargs when transport is unspecified by [@strawgate](https://github.com/strawgate) in [#3838](https://github.com/PrefectHQ/fastmcp/pull/3838)
+* fix: strip title fields from tool schemas for Gemini 2.5 Flash compatibility by [@strawgate](https://github.com/strawgate) in [#3861](https://github.com/PrefectHQ/fastmcp/pull/3861)
+* fix: retry when LLM returns text instead of calling final_response by [@strawgate](https://github.com/strawgate) in [#3850](https://github.com/PrefectHQ/fastmcp/pull/3850)
+* Raise on unhandled content types in sampling handler dispatch chains by [@strawgate](https://github.com/strawgate) in [#3857](https://github.com/PrefectHQ/fastmcp/pull/3857)
+* Fix broken code examples in docs by [@strawgate](https://github.com/strawgate) in [#3869](https://github.com/PrefectHQ/fastmcp/pull/3869)
+* fix: GoogleGenaiSamplingHandler leaks thought parts and gives unhelpful errors on empty responses by [@strawgate](https://github.com/strawgate) in [#3849](https://github.com/PrefectHQ/fastmcp/pull/3849)
+* fix: cap consecutive final_response validation retries by [@strawgate](https://github.com/strawgate) in [#3851](https://github.com/PrefectHQ/fastmcp/pull/3851)
+* Fix test quality issues by [@strawgate](https://github.com/strawgate) in [#3854](https://github.com/PrefectHQ/fastmcp/pull/3854)
+* Fix MCP tool on docs welcome page by [@lkiesow](https://github.com/lkiesow) in [#3874](https://github.com/PrefectHQ/fastmcp/pull/3874)
+* Fix CIMD clients getting required_scopes instead of valid_scopes by [@jlowin](https://github.com/jlowin) in [#3836](https://github.com/PrefectHQ/fastmcp/pull/3836)
+* Rename filesystem-provider example dir to avoid mcp/ collision by [@jlowin](https://github.com/jlowin) in [#3878](https://github.com/PrefectHQ/fastmcp/pull/3878)
+* fix: drop configurable dedupe from AggregateProvider, always warn by [@jlowin](https://github.com/jlowin) in [#3877](https://github.com/PrefectHQ/fastmcp/pull/3877)
+* fix: resolve list[dict] return type producing Root() instead of dicts by [@KeWang0622](https://github.com/KeWang0622) in [#3880](https://github.com/PrefectHQ/fastmcp/pull/3880)
+* fix: strip titles from bare-metadata nodes (Gemini 2.5 Flash) by [@jlowin](https://github.com/jlowin) in [#3881](https://github.com/PrefectHQ/fastmcp/pull/3881)
+* Fix wildcard resource template params in mounted servers by [@jlowin](https://github.com/jlowin) in [#3899](https://github.com/PrefectHQ/fastmcp/pull/3899)
+* Harden forced client disconnect cleanup by [@vonbai](https://github.com/vonbai) in [#3885](https://github.com/PrefectHQ/fastmcp/pull/3885)
+* fix: elicitation scalar return, resource auto-serialization, Client.new() state, prompt errors by [@strawgate](https://github.com/strawgate) in [#3859](https://github.com/PrefectHQ/fastmcp/pull/3859)
+* fix: task.wait() hangs indefinitely when task enters input_required by [@mrishav](https://github.com/mrishav) in [#3798](https://github.com/PrefectHQ/fastmcp/pull/3798)
+* Fix RetryMiddleware not retrying tool errors by [@strawgate](https://github.com/strawgate) in [#3858](https://github.com/PrefectHQ/fastmcp/pull/3858)
+* Stop pydantic 2.13 from leaking _WrappedResult docstring into tool output schemas by [@jlowin](https://github.com/jlowin) in [#3918](https://github.com/PrefectHQ/fastmcp/pull/3918)
+### Docs 📚
+* Note generate-notes API in release workflow docs by [@jlowin](https://github.com/jlowin) in [#3806](https://github.com/PrefectHQ/fastmcp/pull/3806)
+* docs: require agents to respect DNM markers on PRs by [@jlowin](https://github.com/jlowin) in [#3871](https://github.com/PrefectHQ/fastmcp/pull/3871)
+* docs: add uv-managed dependencies and uvx examples to mcp-json configuration by [@vincent067](https://github.com/vincent067) in [#3843](https://github.com/PrefectHQ/fastmcp/pull/3843)
+* docs: link fastmcp-keycloak-local companion project from Keycloak integration page by [@stephaneberle9](https://github.com/stephaneberle9) in [#3904](https://github.com/PrefectHQ/fastmcp/pull/3904)
+* Overhaul apps docs by [@jlowin](https://github.com/jlowin) in [#3915](https://github.com/PrefectHQ/fastmcp/pull/3915)
+### Dependencies 📦
+* chore(deps): bump extractions/setup-just from 3 to 4 by [@dependabot](https://github.com/dependabot) in [#3863](https://github.com/PrefectHQ/fastmcp/pull/3863)
+* chore(deps): bump astral-sh/setup-uv from 6 to 7 by [@dependabot](https://github.com/dependabot) in [#3865](https://github.com/PrefectHQ/fastmcp/pull/3865)
+* chore(deps): bump actions/checkout from 4 to 6 by [@dependabot](https://github.com/dependabot) in [#3864](https://github.com/PrefectHQ/fastmcp/pull/3864)
+* chore(deps-dev): bump pydantic-monty from 0.0.9 to 0.0.10 by [@dependabot](https://github.com/dependabot) in [#3809](https://github.com/PrefectHQ/fastmcp/pull/3809)
+* chore(deps): bump the uv group across 2 directories with 1 update by [@dependabot](https://github.com/dependabot) in [#3913](https://github.com/PrefectHQ/fastmcp/pull/3913)
+
+## New Contributors
+* @lkiesow made their first contribution in [#3874](https://github.com/PrefectHQ/fastmcp/pull/3874)
+* @KeWang0622 made their first contribution in [#3880](https://github.com/PrefectHQ/fastmcp/pull/3880)
+* @vonbai made their first contribution in [#3885](https://github.com/PrefectHQ/fastmcp/pull/3885)
+
+**Full Changelog**: [v3.2.3...v3.2.4](https://github.com/PrefectHQ/fastmcp/compare/v3.2.3...v3.2.4)
+
+
+
+
+
+**[v3.2.3: Redis or Not](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.2.3)**
+
+A stopgap pin: fakeredis 2.35.0 shipped an undocumented rename that broke pydocket's `memory://` backend, causing `fastmcp[tasks]` installs to fail at startup with an `ImportError`. This pins `fakeredis<2.35.0` in the `tasks` extra until a fixed pydocket ships.
+
+### Fixes 🐞
+* Pin `fakeredis<2.35.0` in tasks extra by [@jlowin](https://github.com/jlowin) in [#3804](https://github.com/PrefectHQ/fastmcp/pull/3804)
+### Docs 📚
+* Document session state isolation across mount boundaries by [@jlowin](https://github.com/jlowin) in [#3801](https://github.com/PrefectHQ/fastmcp/pull/3801)
+
+
+**Full Changelog**: [v3.2.2...v3.2.3](https://github.com/PrefectHQ/fastmcp/compare/v3.2.2...v3.2.3)
+
+
+
+
+
+**[v3.2.2: Audience Appreciation](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.2.2)**
+
+Fixes the Azure audience regression from 3.2.1: validation switched from `client_id` to `identifier_uri`, which fixed custom Application ID URIs but broke the default case where Azure AD v2 tokens set `aud` to the bare client ID GUID. Both formats are now accepted.
+
+### Fixes 🐞
+* fix: accept both client_id and identifier_uri as Azure audience by [@jlowin](https://github.com/jlowin) in [#3797](https://github.com/PrefectHQ/fastmcp/pull/3797)
+### Dependencies 📦
+* chore(deps): bump the uv group across 2 directories with 1 update by [@dependabot](https://github.com/dependabot) in [#3795](https://github.com/PrefectHQ/fastmcp/pull/3795)
+
+
+**Full Changelog**: [v3.2.1...v3.2.2](https://github.com/PrefectHQ/fastmcp/compare/v3.2.1...v3.2.2)
+
+
+
+
+
+**[v3.2.1: Audience Participation](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.2.1)**
+
+A patch focused on auth-provider audience validation. Cognito tokens now validate on `client_id` (they carry no `aud`), Azure honors the `identifier_uri` parameter for Entra v2.0 tokens, and consent cookies are LRU-capped to prevent unbounded growth past reverse proxy header limits. Also fixes OpenAPI 3.0 `nullable` fields leaking into tool input schemas and server-variable substitution in base URLs.
+
+### Breaking Changes ⚠️
+* fix(google): use sub (user ID) for client_id instead of aud (app ID) by [@shigechika](https://github.com/shigechika) in [#3722](https://github.com/PrefectHQ/fastmcp/pull/3722)
+* fix: remove CSP from tool metadata, keep on resource only by [@jlowin](https://github.com/jlowin) in [#3754](https://github.com/PrefectHQ/fastmcp/pull/3754)
+### Enhancements ✨
+* [codex] Add FastMCP docs telemetry by [@aaazzam](https://github.com/aaazzam) in [#3727](https://github.com/PrefectHQ/fastmcp/pull/3727)
+* chore: split SDK navigation into standalone $ref file by [@jlowin](https://github.com/jlowin) in [#3773](https://github.com/PrefectHQ/fastmcp/pull/3773)
+* fix: bump ty to >=0.0.29 and suppress new false positives by [@jlowin](https://github.com/jlowin) in [#3790](https://github.com/PrefectHQ/fastmcp/pull/3790)
+### Fixes 🐞
+* fix: use explicit None checks for JWT exp validation by [@jlowin](https://github.com/jlowin) in [#3724](https://github.com/PrefectHQ/fastmcp/pull/3724)
+* Unify background task context forwarding, fix concurrent dependency bugs by [@chrisguidry](https://github.com/chrisguidry) in [#3710](https://github.com/PrefectHQ/fastmcp/pull/3710)
+* fix: add proxy timeouts and modernize networking in apps dev by [@mateeaaa](https://github.com/mateeaaa) in [#3741](https://github.com/PrefectHQ/fastmcp/pull/3741)
+* fix: ResponseLimitingMiddleware no longer breaks outputSchema tools by [@jlowin](https://github.com/jlowin) in [#3756](https://github.com/PrefectHQ/fastmcp/pull/3756)
+* fix: substitute server variable defaults when building base URL from OpenAPI spec by [@mrishav](https://github.com/mrishav) in [#3770](https://github.com/PrefectHQ/fastmcp/pull/3770)
+* fix: FastAPI TestClient compatibility and lifespan re-initialization by [@kvdhanush06](https://github.com/kvdhanush06) in [#3736](https://github.com/PrefectHQ/fastmcp/pull/3736)
+* fix: propagate upstream_claims in load_access_token by [@kvdhanush06](https://github.com/kvdhanush06) in [#3750](https://github.com/PrefectHQ/fastmcp/pull/3750)
+* Remove deprecated asyncio.iscoroutinefunction fallback by [@kaiisfree](https://github.com/kaiisfree) in [#3767](https://github.com/PrefectHQ/fastmcp/pull/3767)
+* fix: changeable allowed_client_redirect_uris on OAuthProxy by [@fengarix](https://github.com/fengarix) in [#3772](https://github.com/PrefectHQ/fastmcp/pull/3772)
+* fix: broken link in changelog by [@jlowin](https://github.com/jlowin) in [#3775](https://github.com/PrefectHQ/fastmcp/pull/3775)
+* fix(docs): correct FastMCP tool name in welcome docs by [@buyua9](https://github.com/buyua9) in [#3781](https://github.com/PrefectHQ/fastmcp/pull/3781)
+* fix: cap consent cookie size to prevent header overflow by [@jlowin](https://github.com/jlowin) in [#3784](https://github.com/PrefectHQ/fastmcp/pull/3784)
+* Fix boolean property schemas in JSON Schema parsing by [@jlowin](https://github.com/jlowin) in [#3785](https://github.com/PrefectHQ/fastmcp/pull/3785)
+* Fix OpenAPI 3.0 nullable fields in tool input schemas by [@kvdhanush06](https://github.com/kvdhanush06) in [#3768](https://github.com/PrefectHQ/fastmcp/pull/3768)
+* fix: Cognito token verification checks client_id instead of aud by [@jlowin](https://github.com/jlowin) in [#3786](https://github.com/PrefectHQ/fastmcp/pull/3786)
+* fix: use identifier_uri as audience for Azure token validation by [@jlowin](https://github.com/jlowin) in [#3787](https://github.com/PrefectHQ/fastmcp/pull/3787)
+* Harden client tool result error handling by [@aimable100](https://github.com/aimable100) in [#3778](https://github.com/PrefectHQ/fastmcp/pull/3778)
+### Docs 📚
+* Github integraiton documentation fix: use result.data otherwise CallToolResult not scriptable by [@c4jquick](https://github.com/c4jquick) in [#3753](https://github.com/PrefectHQ/fastmcp/pull/3753)
+* chore: split v2 docs navigation into separate file by [@jlowin](https://github.com/jlowin) in [#3762](https://github.com/PrefectHQ/fastmcp/pull/3762)
+* docs: document forward_resource parameter on OAuthProxy by [@jlowin](https://github.com/jlowin) in [#3788](https://github.com/PrefectHQ/fastmcp/pull/3788)
+### Examples & Contrib 💡
+* fix: boolean false values dropped in form submissions by [@jlowin](https://github.com/jlowin) in [#3776](https://github.com/PrefectHQ/fastmcp/pull/3776)
+### Dependencies 📦
+* chore(deps): bump fastmcp from 3.1.1 to 3.2.0 in /examples/testing_demo in the uv group across 1 directory by [@dependabot](https://github.com/dependabot) in [#3728](https://github.com/PrefectHQ/fastmcp/pull/3728)
+* chore(deps): bump anthropic from 0.86.0 to 0.87.0 in the uv group across 1 directory by [@dependabot](https://github.com/dependabot) in [#3742](https://github.com/PrefectHQ/fastmcp/pull/3742)
+
+## New Contributors
+* @c4jquick made their first contribution in [#3753](https://github.com/PrefectHQ/fastmcp/pull/3753)
+* @mateeaaa made their first contribution in [#3741](https://github.com/PrefectHQ/fastmcp/pull/3741)
+* @mrishav made their first contribution in [#3770](https://github.com/PrefectHQ/fastmcp/pull/3770)
+* @kvdhanush06 made their first contribution in [#3736](https://github.com/PrefectHQ/fastmcp/pull/3736)
+* @kaiisfree made their first contribution in [#3767](https://github.com/PrefectHQ/fastmcp/pull/3767)
+* @fengarix made their first contribution in [#3772](https://github.com/PrefectHQ/fastmcp/pull/3772)
+* @buyua9 made their first contribution in [#3781](https://github.com/PrefectHQ/fastmcp/pull/3781)
+* @aimable100 made their first contribution in [#3778](https://github.com/PrefectHQ/fastmcp/pull/3778)
+
+**Full Changelog**: [v3.2.0...v3.2.1](https://github.com/PrefectHQ/fastmcp/compare/v3.2.0...v3.2.1)
+
+
+
+
+
+**[v3.2.0: Show Don't Tool](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.2.0)**
+
+FastMCP 3.2 is the Apps release: your tools can now return interactive UIs — charts, dashboards, forms, maps — rendered right inside the conversation. `FastMCPApp` separates the tools the LLM sees from the backend tools the UI calls, five built-in providers (FileUpload, Approval, Choice, FormInput, GenerativeUI) cover common interaction patterns, and `fastmcp dev apps` gives you a browser preview. The release also lands a significant security hardening pass across SSRF/path-traversal, JWT algorithm restrictions, OAuth scope enforcement, and CSRF.
+
+### New Features 🎉
+* Add FastMCPApp — a Provider for composable MCP applications by [@jlowin](https://github.com/jlowin) in [#3385](https://github.com/PrefectHQ/fastmcp/pull/3385)
+* Add fastmcp dev apps command with browser UI preview by [@jlowin](https://github.com/jlowin) in [#3489](https://github.com/PrefectHQ/fastmcp/pull/3489)
+* Add GenerativeUI provider, bump prefab-ui 0.14.0 by [@jlowin](https://github.com/jlowin) in [#3647](https://github.com/PrefectHQ/fastmcp/pull/3647)
+* Add FileUpload provider by [@jlowin](https://github.com/jlowin) in [#3669](https://github.com/PrefectHQ/fastmcp/pull/3669)
+* Add Approval and Choice providers by [@jlowin](https://github.com/jlowin) in [#3686](https://github.com/PrefectHQ/fastmcp/pull/3686)
+* Add FormInput provider, bump prefab-ui to 0.15.0 by [@jlowin](https://github.com/jlowin) in [#3687](https://github.com/PrefectHQ/fastmcp/pull/3687)
+### Breaking Changes ⚠️
+* Route app tool calls via ___-prefixed names by [@jlowin](https://github.com/jlowin) in [#3667](https://github.com/PrefectHQ/fastmcp/pull/3667)
+### Enhancements ✨
+* feat: add `--config-path` flag to claude-desktop install command by [@Sumanshu-Nankana](https://github.com/Sumanshu-Nankana) in [#3380](https://github.com/PrefectHQ/fastmcp/pull/3380)
+* Support ImageContent and AudioContent in Message class by [@ericrobinson-indeed](https://github.com/ericrobinson-indeed) in [#3396](https://github.com/PrefectHQ/fastmcp/pull/3396)
+* Deprecate PromptToolMiddleware and ResourceToolMiddleware by [@jlowin](https://github.com/jlowin) in [#3389](https://github.com/PrefectHQ/fastmcp/pull/3389)
+* Block HS* algorithms when JWTVerifier is configured with JWKS by [@jlowin](https://github.com/jlowin) in [#3419](https://github.com/PrefectHQ/fastmcp/pull/3419)
+* Remove prek from Marvin workflows by [@jlowin](https://github.com/jlowin) in [#3444](https://github.com/PrefectHQ/fastmcp/pull/3444)
+* Add dependency version compatibility guidance to code-review skill by [@jlowin](https://github.com/jlowin) in [#3475](https://github.com/PrefectHQ/fastmcp/pull/3475)
+* Remove "good first issue" label by [@jlowin](https://github.com/jlowin) in [#3482](https://github.com/PrefectHQ/fastmcp/pull/3482)
+* Cache component lists in ProxyProvider by [@jlowin](https://github.com/jlowin) in [#3479](https://github.com/PrefectHQ/fastmcp/pull/3479)
+* Support logging/setLevel and add client_log_level by [@jlowin](https://github.com/jlowin) in [#3491](https://github.com/PrefectHQ/fastmcp/pull/3491)
+* Propagate x-fastmcp-wrap-result in tool result _meta by [@jlowin](https://github.com/jlowin) in [#3490](https://github.com/PrefectHQ/fastmcp/pull/3490)
+* feat(auth): add external_consent param to suppress misleading warning by [@mtthidoteu](https://github.com/mtthidoteu) in [#3473](https://github.com/PrefectHQ/fastmcp/pull/3473)
+* Add `verify` parameter for SSL certificate configuration by [@jlowin](https://github.com/jlowin) in [#3487](https://github.com/PrefectHQ/fastmcp/pull/3487)
+* Expose minimum_check_interval, reduce task pickup latency by [@jlowin](https://github.com/jlowin) in [#3500](https://github.com/PrefectHQ/fastmcp/pull/3500)
+* Fix test timeouts, suppress deprecation warnings, speed up auth tests by [@jlowin](https://github.com/jlowin) in [#3504](https://github.com/PrefectHQ/fastmcp/pull/3504)
+* Auto-close upgrade check issue when build passes by [@jlowin](https://github.com/jlowin) in [#3505](https://github.com/PrefectHQ/fastmcp/pull/3505)
+* feat: make upstream_client_secret optional in OAuthProxy by [@jlowin](https://github.com/jlowin) in [#3486](https://github.com/PrefectHQ/fastmcp/pull/3486)
+* Add security label to triage workflow and release notes by [@jlowin](https://github.com/jlowin) in [#3516](https://github.com/PrefectHQ/fastmcp/pull/3516)
+* Claude/review contributor guidelines by [@jlowin](https://github.com/jlowin) in [#3517](https://github.com/PrefectHQ/fastmcp/pull/3517)
+* pin pydantic-monty to 0.0.8 by [@jlowin](https://github.com/jlowin) in [#3539](https://github.com/PrefectHQ/fastmcp/pull/3539)
+* Support ImageContent and AudioContent in sampling handlers by [@jlowin](https://github.com/jlowin) in [#3550](https://github.com/PrefectHQ/fastmcp/pull/3550)
+* Graceful degradation for multi-server proxy setup by [@jlowin](https://github.com/jlowin) in [#3546](https://github.com/PrefectHQ/fastmcp/pull/3546)
+* Extract TokenCache utility, add caching to GitHubTokenVerifier by [@jlowin](https://github.com/jlowin) in [#3547](https://github.com/PrefectHQ/fastmcp/pull/3547)
+* Add review-pr skill for Codex bot workflow by [@jlowin](https://github.com/jlowin) in [#3552](https://github.com/PrefectHQ/fastmcp/pull/3552)
+* Add MCP message inspector to dev apps UI by [@jlowin](https://github.com/jlowin) in [#3570](https://github.com/PrefectHQ/fastmcp/pull/3570)
+* Comprehensive MCP Apps docs, string CallTool resolution by [@jlowin](https://github.com/jlowin) in [#3575](https://github.com/PrefectHQ/fastmcp/pull/3575)
+* Replace UUID global keys with (app_name, tool_name) registry by [@jlowin](https://github.com/jlowin) in [#3585](https://github.com/PrefectHQ/fastmcp/pull/3585)
+* Route app tool calls through provider chain by [@jlowin](https://github.com/jlowin) in [#3587](https://github.com/PrefectHQ/fastmcp/pull/3587)
+* Dev apps: show more/less for long tool descriptions by [@jlowin](https://github.com/jlowin) in [#3600](https://github.com/PrefectHQ/fastmcp/pull/3600)
+* Apps Phase 1: docs, examples, app-only tool filtering by [@jlowin](https://github.com/jlowin) in [#3593](https://github.com/PrefectHQ/fastmcp/pull/3593)
+* Forward enable_cimd to OAuthProxy in all provider subclasses by [@jlowin](https://github.com/jlowin) in [#3608](https://github.com/PrefectHQ/fastmcp/pull/3608)
+* Tune too-long triage heuristic by [@jlowin](https://github.com/jlowin) in [#3610](https://github.com/PrefectHQ/fastmcp/pull/3610)
+* Update ty ignore comments for 0.0.25 compatibility by [@jlowin](https://github.com/jlowin) in [#3614](https://github.com/PrefectHQ/fastmcp/pull/3614)
+* Move app modules to fastmcp.apps package by [@jlowin](https://github.com/jlowin) in [#3616](https://github.com/PrefectHQ/fastmcp/pull/3616)
+* Tighten too-long heuristic for design-document issues by [@jlowin](https://github.com/jlowin) in [#3620](https://github.com/PrefectHQ/fastmcp/pull/3620)
+* Run MCP conformance tests by [@strawgate](https://github.com/strawgate) in [#3628](https://github.com/PrefectHQ/fastmcp/pull/3628)
+* Add PrefabAppConfig for customizable Prefab tool setup by [@jlowin](https://github.com/jlowin) in [#3648](https://github.com/PrefectHQ/fastmcp/pull/3648)
+* Clean error when dev apps ports are in use by [@jlowin](https://github.com/jlowin) in [#3658](https://github.com/PrefectHQ/fastmcp/pull/3658)
+* Add Clerk OAuth provider by [@mostafa6765](https://github.com/mostafa6765) in [#3677](https://github.com/PrefectHQ/fastmcp/pull/3677)
+* Add interactive map example with geocoding by [@jlowin](https://github.com/jlowin) in [#3702](https://github.com/PrefectHQ/fastmcp/pull/3702)
+* Bump pydantic-monty to 0.0.9 by [@jlowin](https://github.com/jlowin) in [#3707](https://github.com/PrefectHQ/fastmcp/pull/3707)
+* Add forward_resource flag to OAuthProxy by [@jlowin](https://github.com/jlowin) in [#3711](https://github.com/PrefectHQ/fastmcp/pull/3711)
+### Security 🔒
+* fix: enforce per-tool auth checks in sampling tool wrapper by [@jlowin](https://github.com/jlowin) in [#3494](https://github.com/PrefectHQ/fastmcp/pull/3494)
+* fix: handle re.error from malformed URI templates by [@jlowin](https://github.com/jlowin) in [#3501](https://github.com/PrefectHQ/fastmcp/pull/3501)
+* fix: reject empty/OIDC-only required_scopes in AzureProvider by [@jlowin](https://github.com/jlowin) in [#3503](https://github.com/PrefectHQ/fastmcp/pull/3503)
+* fix: restrict $ref resolution to local refs only (SSRF/LFI) by [@jlowin](https://github.com/jlowin) in [#3502](https://github.com/PrefectHQ/fastmcp/pull/3502)
+* fix: URL-encode path params to prevent SSRF/path traversal (GHSA-vv7q-7jx5-f767) by [@jlowin](https://github.com/jlowin) in [#3507](https://github.com/PrefectHQ/fastmcp/pull/3507)
+* fix: prevent path traversal in skill download by [@jlowin](https://github.com/jlowin) in [#3493](https://github.com/PrefectHQ/fastmcp/pull/3493)
+* fix: prefer IdP-granted scopes over client-requested scopes in OAuthProxy by [@jlowin](https://github.com/jlowin) in [#3492](https://github.com/PrefectHQ/fastmcp/pull/3492)
+* fix: remove forced follow_redirects from httpx_client_factory calls by [@jlowin](https://github.com/jlowin) in [#3496](https://github.com/PrefectHQ/fastmcp/pull/3496)
+* Bump PyJWT >= 2.12.0 (CVE-2026-32597) by [@jlowin](https://github.com/jlowin) in [#3515](https://github.com/PrefectHQ/fastmcp/pull/3515)
+* Drop diskcache from examples/testing_demo lockfile (CVE-2025-69872) by [@jlowin](https://github.com/jlowin) in [#3518](https://github.com/PrefectHQ/fastmcp/pull/3518)
+* fix: CSRF double-submit cookie check in consent flow by [@jlowin](https://github.com/jlowin) in [#3519](https://github.com/PrefectHQ/fastmcp/pull/3519)
+* fix: validate server names in install commands by [@jlowin](https://github.com/jlowin) in [#3522](https://github.com/PrefectHQ/fastmcp/pull/3522)
+* fix: reject refresh tokens used as Bearer access tokens by [@jlowin](https://github.com/jlowin) in [#3524](https://github.com/PrefectHQ/fastmcp/pull/3524)
+* fix: route ResourcesAsTools/PromptsAsTools through server middleware by [@jlowin](https://github.com/jlowin) in [#3495](https://github.com/PrefectHQ/fastmcp/pull/3495)
+### Fixes 🐞
+* Update docs banner and fix mobile layout by [@jlowin](https://github.com/jlowin) in [#3370](https://github.com/PrefectHQ/fastmcp/pull/3370)
+* Remove form-action from consent CSP, forward consent_csp_policy in providers by [@jlowin](https://github.com/jlowin) in [#3372](https://github.com/PrefectHQ/fastmcp/pull/3372)
+* Fix resource templates with query params on mounted servers by [@jlowin](https://github.com/jlowin) in [#3373](https://github.com/PrefectHQ/fastmcp/pull/3373)
+* Increase uv transport test timeout for CI cold starts by [@jlowin](https://github.com/jlowin) in [#3376](https://github.com/PrefectHQ/fastmcp/pull/3376)
+* Fix stale catalog in CodeMode execute by [@jlowin](https://github.com/jlowin) in [#3375](https://github.com/PrefectHQ/fastmcp/pull/3375)
+* Deduplicate versioned tools in CatalogTransform catalog by [@jlowin](https://github.com/jlowin) in [#3374](https://github.com/PrefectHQ/fastmcp/pull/3374)
+* Fix ty 0.0.20 compatibility by [@jlowin](https://github.com/jlowin) in [#3377](https://github.com/PrefectHQ/fastmcp/pull/3377)
+* Forward scopes_supported through RemoteAuthProvider subclasses by [@jlowin](https://github.com/jlowin) in [#3388](https://github.com/PrefectHQ/fastmcp/pull/3388)
+* Enforce token scopes in WorkOS verifier to prevent scope bypass by [@jlowin](https://github.com/jlowin) in [#3407](https://github.com/PrefectHQ/fastmcp/pull/3407)
+* Bind Discord token verification to configured client_id by [@jlowin](https://github.com/jlowin) in [#3405](https://github.com/PrefectHQ/fastmcp/pull/3405)
+* Return after `McpError` in initialization middleware to prevent fallthrough by [@jlowin](https://github.com/jlowin) in [#3413](https://github.com/PrefectHQ/fastmcp/pull/3413)
+* Escape client_id in OAuth consent advanced details by [@jlowin](https://github.com/jlowin) in [#3418](https://github.com/PrefectHQ/fastmcp/pull/3418)
+* Bound client auto-pagination loops to prevent unbounded list fetches by [@jlowin](https://github.com/jlowin) in [#3411](https://github.com/PrefectHQ/fastmcp/pull/3411)
+* Raise ValueError for invalid boolean query params in resource templates by [@jlowin](https://github.com/jlowin) in [#3434](https://github.com/PrefectHQ/fastmcp/pull/3434)
+* Validate workspace path is a directory in cursor install by [@jlowin](https://github.com/jlowin) in [#3435](https://github.com/PrefectHQ/fastmcp/pull/3435)
+* Validate version metadata to reject non-scalar types by [@jlowin](https://github.com/jlowin) in [#3437](https://github.com/PrefectHQ/fastmcp/pull/3437)
+* Bind AWS Cognito token verification to configured app client by [@jlowin](https://github.com/jlowin) in [#3406](https://github.com/PrefectHQ/fastmcp/pull/3406)
+* Avoid stale context leakage when proxying with an already‑connected ProxyClient by [@jlowin](https://github.com/jlowin) in [#3408](https://github.com/PrefectHQ/fastmcp/pull/3408)
+* Prevent skills manifests from hashing files outside the skill directory by [@jlowin](https://github.com/jlowin) in [#3410](https://github.com/PrefectHQ/fastmcp/pull/3410)
+* Harden fastmcp metadata parsing in proxy paths by [@jlowin](https://github.com/jlowin) in [#3412](https://github.com/PrefectHQ/fastmcp/pull/3412)
+* Re-hash response caching keys to avoid persisting raw request input by [@jlowin](https://github.com/jlowin) in [#3414](https://github.com/PrefectHQ/fastmcp/pull/3414)
+* Handle Windows npx detection when npx.cmd is missing by [@jlowin](https://github.com/jlowin) in [#3416](https://github.com/PrefectHQ/fastmcp/pull/3416)
+* Guard OAuth callback result from post-completion overwrites by [@jlowin](https://github.com/jlowin) in [#3417](https://github.com/PrefectHQ/fastmcp/pull/3417)
+* Fix tool argument rename collisions with passthrough params by [@jlowin](https://github.com/jlowin) in [#3431](https://github.com/PrefectHQ/fastmcp/pull/3431)
+* Guard default progress handler against total=0 notifications by [@jlowin](https://github.com/jlowin) in [#3432](https://github.com/PrefectHQ/fastmcp/pull/3432)
+* Fix get_* returning None when latest version is disabled by [@jlowin](https://github.com/jlowin) in [#3439](https://github.com/PrefectHQ/fastmcp/pull/3439)
+* Fix server lifespan overlap teardown by [@jlowin](https://github.com/jlowin) in [#3415](https://github.com/PrefectHQ/fastmcp/pull/3415)
+* Fix $ref output schema object detection regression by [@jlowin](https://github.com/jlowin) in [#3420](https://github.com/PrefectHQ/fastmcp/pull/3420)
+* Preserve kw-only defaults when rebuilding functions for resolved annotations by [@jlowin](https://github.com/jlowin) in [#3429](https://github.com/PrefectHQ/fastmcp/pull/3429)
+* Redact sensitive headers in OpenAPI provider debug logging by [@jlowin](https://github.com/jlowin) in [#3436](https://github.com/PrefectHQ/fastmcp/pull/3436)
+* Fix async partial callables rejected by iscoroutinefunction by [@jlowin](https://github.com/jlowin) in [#3438](https://github.com/PrefectHQ/fastmcp/pull/3438)
+* Block insecure HS* JWT verification with JWKS/public keys by [@jlowin](https://github.com/jlowin) in [#3430](https://github.com/PrefectHQ/fastmcp/pull/3430)
+* Sanitize untrusted output in `fastmcp list` and `fastmcp call` by [@jlowin](https://github.com/jlowin) in [#3409](https://github.com/PrefectHQ/fastmcp/pull/3409)
+* fix: propagate `version` to components in FileSystemProvider by [@martimfasantos](https://github.com/martimfasantos) in [#3458](https://github.com/PrefectHQ/fastmcp/pull/3458)
+* fix: use intent-based flag for OIDC scope patch in load_access_token by [@voidborne-d](https://github.com/voidborne-d) in [#3465](https://github.com/PrefectHQ/fastmcp/pull/3465)
+* Set readOnlyHint=True on ResourcesAsTools generated tools by [@jlowin](https://github.com/jlowin) in [#3476](https://github.com/PrefectHQ/fastmcp/pull/3476)
+* fix: normalize Google scope shorthands and surface valid_scopes by [@jlowin](https://github.com/jlowin) in [#3477](https://github.com/PrefectHQ/fastmcp/pull/3477)
+* fix: resolve ty 0.0.23 type-checking errors by [@jlowin](https://github.com/jlowin) in [#3481](https://github.com/PrefectHQ/fastmcp/pull/3481)
+* fix: shield lifespan teardown from cancellation by [@jlowin](https://github.com/jlowin) in [#3480](https://github.com/PrefectHQ/fastmcp/pull/3480)
+* fix: forward custom_route endpoints from mounted servers by [@voidborne-d](https://github.com/voidborne-d) in [#3462](https://github.com/PrefectHQ/fastmcp/pull/3462)
+* fix: use dynamic version in CLI help text instead of hardcoded 2.0 by [@saschabuehrle](https://github.com/saschabuehrle) in [#3456](https://github.com/PrefectHQ/fastmcp/pull/3456)
+* Fix Monty 0.0.8 compatibility by [@hkc5](https://github.com/hkc5) in [#3468](https://github.com/PrefectHQ/fastmcp/pull/3468)
+* Fix task test teardown hanging 5s per test by [@jlowin](https://github.com/jlowin) in [#3499](https://github.com/PrefectHQ/fastmcp/pull/3499)
+* fix: validate workspace path is a directory before cursor install by [@nightcityblade](https://github.com/nightcityblade) in [#3440](https://github.com/PrefectHQ/fastmcp/pull/3440)
+* Treat `refresh_expires_in=0` as missing, fall back to 30-day default by [@jlowin](https://github.com/jlowin) in [#3514](https://github.com/PrefectHQ/fastmcp/pull/3514)
+* fix: use raw strings for regex in pytest.raises match by [@jlowin](https://github.com/jlowin) in [#3523](https://github.com/PrefectHQ/fastmcp/pull/3523)
+* fix: resolve Pyright "Module is not callable" on @tool, @resource, @prompt decorators by [@jlowin](https://github.com/jlowin) in [#3540](https://github.com/PrefectHQ/fastmcp/pull/3540)
+* fix: flaky KEY_PREFIX warning test in lowest-direct deps by [@jlowin](https://github.com/jlowin) in [#3549](https://github.com/PrefectHQ/fastmcp/pull/3549)
+* fix: suppress output schema for ToolResult subclass annotations by [@jlowin](https://github.com/jlowin) in [#3548](https://github.com/PrefectHQ/fastmcp/pull/3548)
+* Bump anthropic minimum to 0.48.0 by [@jlowin](https://github.com/jlowin) in [#3553](https://github.com/PrefectHQ/fastmcp/pull/3553)
+* Update startup banner deploy URL to Prefect Horizon by [@zzstoatzz](https://github.com/zzstoatzz) in [#3557](https://github.com/PrefectHQ/fastmcp/pull/3557)
+* fix: increase sleep duration in proxy cache tests by [@strawgate](https://github.com/strawgate) in [#3567](https://github.com/PrefectHQ/fastmcp/pull/3567)
+* fix: store absolute token expiry to prevent stale expires_in on reload by [@jlowin](https://github.com/jlowin) in [#3572](https://github.com/PrefectHQ/fastmcp/pull/3572)
+* fix: preserve tool properties named 'title' during schema compression by [@jlowin](https://github.com/jlowin) in [#3582](https://github.com/PrefectHQ/fastmcp/pull/3582)
+* Add `encoding` parameter to `FileResource` by [@shulkx](https://github.com/shulkx) in [#3580](https://github.com/PrefectHQ/fastmcp/pull/3580)
+* Transparently refresh upstream token in OAuthProxy.load_access_token() by [@jlowin](https://github.com/jlowin) in [#3584](https://github.com/PrefectHQ/fastmcp/pull/3584)
+* Fix loopback redirect URI port matching per RFC 8252 §7.3 by [@radoshi](https://github.com/radoshi) in [#3589](https://github.com/PrefectHQ/fastmcp/pull/3589)
+* Fix app tool routing: visibility check and middleware propagation by [@jlowin](https://github.com/jlowin) in [#3591](https://github.com/PrefectHQ/fastmcp/pull/3591)
+* Fix query parameter serialization to respect OpenAPI explode setting by [@jlowin](https://github.com/jlowin) in [#3595](https://github.com/PrefectHQ/fastmcp/pull/3595)
+* Fix dev apps form: union types, textarea support, JSON parsing by [@jlowin](https://github.com/jlowin) in [#3597](https://github.com/PrefectHQ/fastmcp/pull/3597)
+* Respect OpenAPI content type in request body serialization by [@jlowin](https://github.com/jlowin) in [#3611](https://github.com/PrefectHQ/fastmcp/pull/3611)
+* fix(google): replace deprecated /oauth2/v1/tokeninfo with /oauth2/v3/userinfo by [@shigechika](https://github.com/shigechika) in [#3603](https://github.com/PrefectHQ/fastmcp/pull/3603)
+* fix: resolve EntraOBOToken dependency injection through MultiAuth by [@jer805](https://github.com/jer805) in [#3609](https://github.com/PrefectHQ/fastmcp/pull/3609)
+* fix: filesystem provider import machinery by [@strawgate](https://github.com/strawgate) in [#3626](https://github.com/PrefectHQ/fastmcp/pull/3626)
+* fix: recover StdioTransport after subprocess exits by [@strawgate](https://github.com/strawgate) in [#3630](https://github.com/PrefectHQ/fastmcp/pull/3630)
+* fix(server): preserve mounted tool task metadata by [@pandego](https://github.com/pandego) in [#3632](https://github.com/PrefectHQ/fastmcp/pull/3632)
+* fix: scope deprecation warning filter to FastMCPDeprecationWarning by [@jlowin](https://github.com/jlowin) in [#3649](https://github.com/PrefectHQ/fastmcp/pull/3649)
+* fix: resolve CurrentFastMCP/ctx.fastmcp to child server in mounted background tasks by [@jlowin](https://github.com/jlowin) in [#3651](https://github.com/PrefectHQ/fastmcp/pull/3651)
+* Fix blocking docs issues: chart imports, Select API, Rx consistency by [@jlowin](https://github.com/jlowin) in [#3652](https://github.com/PrefectHQ/fastmcp/pull/3652)
+* Fix prompt caching round-trip on cache miss by [@strawgate](https://github.com/strawgate) in [#3666](https://github.com/PrefectHQ/fastmcp/pull/3666)
+* fix: serialize object query params per OpenAPI style/explode rules by [@4444J99](https://github.com/4444J99) in [#3662](https://github.com/PrefectHQ/fastmcp/pull/3662)
+* fix: HTTP request headers not accessible in background task workers by [@pandego](https://github.com/pandego) in [#3631](https://github.com/PrefectHQ/fastmcp/pull/3631)
+* fix: restore HTTP headers in worker execution path for background tasks by [@jlowin](https://github.com/jlowin) in [#3681](https://github.com/PrefectHQ/fastmcp/pull/3681)
+* fix: strip discriminator after dereferencing schemas by [@jlowin](https://github.com/jlowin) in [#3682](https://github.com/PrefectHQ/fastmcp/pull/3682)
+* fix: remove stale ty:ignore directives for ty 0.0.26 by [@jlowin](https://github.com/jlowin) in [#3684](https://github.com/PrefectHQ/fastmcp/pull/3684)
+* fix: dev apps log panel UX improvements by [@jlowin](https://github.com/jlowin) in [#3698](https://github.com/PrefectHQ/fastmcp/pull/3698)
+* Add quiz example app, fix dev server empty string args by [@jlowin](https://github.com/jlowin) in [#3700](https://github.com/PrefectHQ/fastmcp/pull/3700)
+### Docs 📚
+* Add early-development warning to Prefab docs by [@jlowin](https://github.com/jlowin) in [#3362](https://github.com/PrefectHQ/fastmcp/pull/3362)
+* Add tag to docs by [@jlowin](https://github.com/jlowin) in [#3382](https://github.com/PrefectHQ/fastmcp/pull/3382)
+* Add settings and environment variables reference by [@jlowin](https://github.com/jlowin) in [#3384](https://github.com/PrefectHQ/fastmcp/pull/3384)
+* Add contributing guidelines and update issue/PR templates by [@jlowin](https://github.com/jlowin) in [#3485](https://github.com/PrefectHQ/fastmcp/pull/3485)
+* [Documentation] Move stateless_http transport kwarg to http_app as FastMCP constructor… by [@mhallo](https://github.com/mhallo) in [#3510](https://github.com/PrefectHQ/fastmcp/pull/3510)
+* Update security policy by [@jlowin](https://github.com/jlowin) in [#3521](https://github.com/PrefectHQ/fastmcp/pull/3521)
+* Add release instructions to CLAUDE.md by [@jlowin](https://github.com/jlowin) in [#3583](https://github.com/PrefectHQ/fastmcp/pull/3583)
+* fix(docs): correct misleading stateless_http header by [@jlowin](https://github.com/jlowin) in [#3622](https://github.com/PrefectHQ/fastmcp/pull/3622)
+* Add tag to deployment pages by [@jlowin](https://github.com/jlowin) in [#3624](https://github.com/PrefectHQ/fastmcp/pull/3624)
+* Docs: generative UI page, fix imports, add PrefabAppConfig by [@jlowin](https://github.com/jlowin) in [#3650](https://github.com/PrefectHQ/fastmcp/pull/3650)
+* docs: improve contributor guidelines for framework contributions by [@jlowin](https://github.com/jlowin) in [#3653](https://github.com/PrefectHQ/fastmcp/pull/3653)
+* Add release notes for v3.1.0, v3.1.1, and v2.14.6 by [@jlowin](https://github.com/jlowin) in [#3659](https://github.com/PrefectHQ/fastmcp/pull/3659)
+* Docs: showcase hero, narrative improvements, panel closed by default by [@jlowin](https://github.com/jlowin) in [#3657](https://github.com/PrefectHQ/fastmcp/pull/3657)
+* Docs: add FileTreeStore sanitization warnings and update examples by [@strawgate](https://github.com/strawgate) in [#3661](https://github.com/PrefectHQ/fastmcp/pull/3661)
+* Add prefab-ui version pinning warning to docs by [@jlowin](https://github.com/jlowin) in [#3688](https://github.com/PrefectHQ/fastmcp/pull/3688)
+* Reorganize apps overview TOC by [@jlowin](https://github.com/jlowin) in [#3689](https://github.com/PrefectHQ/fastmcp/pull/3689)
+* Fix docs gaps in app provider pages by [@jlowin](https://github.com/jlowin) in [#3690](https://github.com/PrefectHQ/fastmcp/pull/3690)
+* Polish apps docs for 3.2 release by [@jlowin](https://github.com/jlowin) in [#3693](https://github.com/PrefectHQ/fastmcp/pull/3693)
+* Add apps quickstart tutorial by [@jlowin](https://github.com/jlowin) in [#3695](https://github.com/PrefectHQ/fastmcp/pull/3695)
+* Improve quickstart: pie chart, interactive row selection, screenshots by [@jlowin](https://github.com/jlowin) in [#3699](https://github.com/PrefectHQ/fastmcp/pull/3699)
+* Add sales dashboard and live system monitor examples, bump prefab-ui to 0.17 by [@jlowin](https://github.com/jlowin) in [#3696](https://github.com/PrefectHQ/fastmcp/pull/3696)
+* Add examples gallery page by [@jlowin](https://github.com/jlowin) in [#3705](https://github.com/PrefectHQ/fastmcp/pull/3705)
+* docs: note that custom routes are unauthenticated by [@jlowin](https://github.com/jlowin) in [#3706](https://github.com/PrefectHQ/fastmcp/pull/3706)
+* Remove hardcoded prefab-ui version from pinning warnings by [@jlowin](https://github.com/jlowin) in [#3708](https://github.com/PrefectHQ/fastmcp/pull/3708)
+### Examples & Contrib 💡
+* Block recursive self-invocation in BulkToolCaller by [@jlowin](https://github.com/jlowin) in [#3433](https://github.com/PrefectHQ/fastmcp/pull/3433)
+### Dependencies 📦
+* Bump authlib from 1.6.6 to 1.6.7 in /examples/testing_demo in the uv group across 1 directory by [@dependabot](https://github.com/dependabot) in [#3390](https://github.com/PrefectHQ/fastmcp/pull/3390)
+* Bump actions/create-github-app-token from 2 to 3 by [@dependabot](https://github.com/dependabot) in [#3511](https://github.com/PrefectHQ/fastmcp/pull/3511)
+* chore(deps): bump pyasn1 from 0.6.2 to 0.6.3 in the uv group across 1 directory by [@dependabot](https://github.com/dependabot) in [#3538](https://github.com/PrefectHQ/fastmcp/pull/3538)
+* chore(deps): bump j178/prek-action from 1 to 2 by [@dependabot](https://github.com/dependabot) in [#3578](https://github.com/PrefectHQ/fastmcp/pull/3578)
+* chore(deps): bump requests from 2.32.5 to 2.33.0 in the uv group across 1 directory by [@dependabot](https://github.com/dependabot) in [#3638](https://github.com/PrefectHQ/fastmcp/pull/3638)
+* chore(deps): bump cryptography from 46.0.5 to 46.0.6 in /examples/testing_demo in the uv group across 1 directory by [@dependabot](https://github.com/dependabot) in [#3685](https://github.com/PrefectHQ/fastmcp/pull/3685)
+* chore(deps): bump actions/setup-node from 4 to 6 by [@dependabot](https://github.com/dependabot) in [#3691](https://github.com/PrefectHQ/fastmcp/pull/3691)
+
+## New Contributors
+* @Sumanshu-Nankana made their first contribution in [#3380](https://github.com/PrefectHQ/fastmcp/pull/3380)
+* @ericrobinson-indeed made their first contribution in [#3396](https://github.com/PrefectHQ/fastmcp/pull/3396)
+* @voidborne-d made their first contribution in [#3465](https://github.com/PrefectHQ/fastmcp/pull/3465)
+* @mtthidoteu made their first contribution in [#3473](https://github.com/PrefectHQ/fastmcp/pull/3473)
+* @saschabuehrle made their first contribution in [#3456](https://github.com/PrefectHQ/fastmcp/pull/3456)
+* @hkc5 made their first contribution in [#3468](https://github.com/PrefectHQ/fastmcp/pull/3468)
+* @nightcityblade made their first contribution in [#3440](https://github.com/PrefectHQ/fastmcp/pull/3440)
+* @mhallo made their first contribution in [#3510](https://github.com/PrefectHQ/fastmcp/pull/3510)
+* @radoshi made their first contribution in [#3589](https://github.com/PrefectHQ/fastmcp/pull/3589)
+* @shigechika made their first contribution in [#3603](https://github.com/PrefectHQ/fastmcp/pull/3603)
+* @pandego made their first contribution in [#3632](https://github.com/PrefectHQ/fastmcp/pull/3632)
+* @4444J99 made their first contribution in [#3662](https://github.com/PrefectHQ/fastmcp/pull/3662)
+* @mostafa6765 made their first contribution in [#3677](https://github.com/PrefectHQ/fastmcp/pull/3677)
+
+**Full Changelog**: [v3.1.0...v3.2.0](https://github.com/PrefectHQ/fastmcp/compare/v3.1.0...v3.2.0)
+
+
+
+
+
+**[v3.1.1: 'Tis But a Patch](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.1.1)**
+
+Pins `pydantic-monty` below 0.0.8 to fix a breaking change in Monty that affects code mode. Monty 0.0.8 removed the `external_functions` constructor parameter, causing `MontySandboxProvider` to fail. This patch caps the version so existing installs work correctly.
+
+### Fixes 🐞
+* Pin pydantic-monty below 0.0.8 to fix code mode by [@jlowin](https://github.com/jlowin) in [#3497](https://github.com/PrefectHQ/fastmcp/pull/3497)
+
+**Full Changelog**: [v3.1.0...v3.1.1](https://github.com/PrefectHQ/fastmcp/compare/v3.1.0...v3.1.1)
+
+
+
+
+
+**[v3.1.0: Code to Joy](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.1.0)**
+
+FastMCP 3.1 is the Code Mode release. The 3.0 architecture introduced providers and transforms as the extensibility layer — 3.1 puts that architecture to work, shipping the most requested capability since launch: servers that can find and execute code on behalf of agents, without requiring clients to know what tools exist.
+
+### New Features 🎉
+* feat: Search transforms for tool discovery by [@jlowin](https://github.com/jlowin) in [#3154](https://github.com/PrefectHQ/fastmcp/pull/3154)
+* Add experimental CodeMode transform by [@aaazzam](https://github.com/aaazzam) in [#3297](https://github.com/PrefectHQ/fastmcp/pull/3297)
+* Add Prefab Apps integration for MCP tool UIs by [@jlowin](https://github.com/jlowin) in [#3316](https://github.com/PrefectHQ/fastmcp/pull/3316)
+### Enhancements 🔧
+* Lazy-load heavy imports to reduce import time by [@jlowin](https://github.com/jlowin) in [#3295](https://github.com/PrefectHQ/fastmcp/pull/3295)
+* Add http_client parameter to all token verifiers for connection pooling by [@jlowin](https://github.com/jlowin) in [#3300](https://github.com/PrefectHQ/fastmcp/pull/3300)
+* Add in-memory caching for token introspection results by [@jlowin](https://github.com/jlowin) in [#3298](https://github.com/PrefectHQ/fastmcp/pull/3298)
+* Add SessionStart hook to install gh CLI in cloud sessions by [@jlowin](https://github.com/jlowin) in [#3308](https://github.com/PrefectHQ/fastmcp/pull/3308)
+* Fix ty 0.0.19 type errors by [@jlowin](https://github.com/jlowin) in [#3310](https://github.com/PrefectHQ/fastmcp/pull/3310)
+* Code Mode: Add resource limits to MontySandboxProvider by [@jlowin](https://github.com/jlowin) in [#3326](https://github.com/PrefectHQ/fastmcp/pull/3326)
+* Accept transforms as FastMCP init kwarg by [@jlowin](https://github.com/jlowin) in [#3324](https://github.com/PrefectHQ/fastmcp/pull/3324)
+* Split large test files to comply with loq line limit by [@jlowin](https://github.com/jlowin) in [#3328](https://github.com/PrefectHQ/fastmcp/pull/3328)
+* Add -m/--module flag to `fastmcp run` and `dev inspector` by [@dgenio](https://github.com/dgenio) in [#3331](https://github.com/PrefectHQ/fastmcp/pull/3331)
+* Add search_result_serializer hook and serialize_tools_for_output_markdown by [@MagnusS0](https://github.com/MagnusS0) in [#3337](https://github.com/PrefectHQ/fastmcp/pull/3337)
+* Add MultiAuth for composing multiple token verification sources by [@jlowin](https://github.com/jlowin) in [#3335](https://github.com/PrefectHQ/fastmcp/pull/3335)
+* Adds PropelAuth as an AuthProvider by [@andrew-propelauth](https://github.com/andrew-propelauth) in [#3358](https://github.com/PrefectHQ/fastmcp/pull/3358)
+* Replace vendored DI with uncalled-for by [@chrisguidry](https://github.com/chrisguidry) in [#3301](https://github.com/PrefectHQ/fastmcp/pull/3301)
+* Decompose CodeMode into composable discovery tools by [@jlowin](https://github.com/jlowin) in [#3354](https://github.com/PrefectHQ/fastmcp/pull/3354)
+* feat(contrib): auto-sync MCPMixin decorators with from_function signatures by [@AnkeshThakur](https://github.com/AnkeshThakur) in [#3323](https://github.com/PrefectHQ/fastmcp/pull/3323)
+* Add Google GenAI Sampling Handler by [@strawgate](https://github.com/strawgate) in [#2977](https://github.com/PrefectHQ/fastmcp/pull/2977)
+* Add ListTools, search limit, and catalog size annotation to CodeMode by [@jlowin](https://github.com/jlowin) in [#3359](https://github.com/PrefectHQ/fastmcp/pull/3359)
+* Allow configuring FastMCP transport setting in the same way as other configuration by [@jvdmr](https://github.com/jvdmr) in [#1796](https://github.com/PrefectHQ/fastmcp/pull/1796)
+* Add include_unversioned option to VersionFilter by [@yangbaechu](https://github.com/yangbaechu) in [#3349](https://github.com/PrefectHQ/fastmcp/pull/3349)
+### Fixes 🐞
+* Fix docs banner pushing nav down by [@jlowin](https://github.com/jlowin) in [#3282](https://github.com/PrefectHQ/fastmcp/pull/3282)
+* fix: Replace hardcoded TTL with DEFAULT_TTL_MS - issue #3279 by [@cedric57](https://github.com/cedric57) in [#3280](https://github.com/PrefectHQ/fastmcp/pull/3280)
+* fix: stop suppressing server stderr in fastmcp call by [@jlowin](https://github.com/jlowin) in [#3283](https://github.com/PrefectHQ/fastmcp/pull/3283)
+* fix: skip max_completion_tokens when maxTokens is None by [@eon01](https://github.com/eon01) in [#3284](https://github.com/PrefectHQ/fastmcp/pull/3284)
+* OpenAPI: rewrite $ref under propertyNames and patternProperties in _replace_ref_with_defs; add regression test for dict[StrEnum, Model] by [@manojPal23234](https://github.com/manojPal23234) in [#3306](https://github.com/PrefectHQ/fastmcp/pull/3306)
+* Remove stale add_resource() key parameter from docs by [@jlowin](https://github.com/jlowin) in [#3309](https://github.com/PrefectHQ/fastmcp/pull/3309)
+* Handle AuthorizationError as exclusion in AuthMiddleware list hooks by [@yangbaechu](https://github.com/yangbaechu) in [#3338](https://github.com/PrefectHQ/fastmcp/pull/3338)
+* Fix flaky OpenAPI performance test threshold by [@jlowin](https://github.com/jlowin) in [#3355](https://github.com/PrefectHQ/fastmcp/pull/3355)
+* Fix flaky SSE timeout test by [@jlowin](https://github.com/jlowin) in [#3343](https://github.com/PrefectHQ/fastmcp/pull/3343)
+* Remove system role references from docs by [@jlowin](https://github.com/jlowin) in [#3356](https://github.com/PrefectHQ/fastmcp/pull/3356)
+* Fix session persistence across tool calls in multi-server MCPConfigTransport by [@jer805](https://github.com/jer805) in [#3330](https://github.com/PrefectHQ/fastmcp/pull/3330)
+### Docs 📚
+* Add v3.0.2 release notes by [@jlowin](https://github.com/jlowin) in [#3276](https://github.com/PrefectHQ/fastmcp/pull/3276)
+* Fix "FastMCP Constructor Parameters" in documentation server.mdx (Remove old parameters & Add new parameter) by [@wangyy04](https://github.com/wangyy04) in [#3317](https://github.com/PrefectHQ/fastmcp/pull/3317)
+* Fix stale docs: tag filtering API and missing output_schema param by [@jlowin](https://github.com/jlowin) in [#3322](https://github.com/PrefectHQ/fastmcp/pull/3322)
+* Narrate search example clients by [@jlowin](https://github.com/jlowin) in [#3321](https://github.com/PrefectHQ/fastmcp/pull/3321)
+* Code Mode: Document resource limits and fix docs formatting by [@jlowin](https://github.com/jlowin) in [#3327](https://github.com/PrefectHQ/fastmcp/pull/3327)
+* Add reverse proxy (nginx) section to HTTP deployment docs by [@dgenio](https://github.com/dgenio) in [#3344](https://github.com/PrefectHQ/fastmcp/pull/3344)
+* Restructure docs navigation: CLI section, Composition, More by [@jlowin](https://github.com/jlowin) in [#3361](https://github.com/PrefectHQ/fastmcp/pull/3361)
+### Other Changes 🦾
+* Don't advertise sampling.tools capability by default by [@jlowin](https://github.com/jlowin) in [#3334](https://github.com/PrefectHQ/fastmcp/pull/3334)
+
+## New Contributors
+* @cedric57 made their first contribution in [#3280](https://github.com/PrefectHQ/fastmcp/pull/3280)
+* @eon01 made their first contribution in [#3284](https://github.com/PrefectHQ/fastmcp/pull/3284)
+* @manojPal23234 made their first contribution in [#3306](https://github.com/PrefectHQ/fastmcp/pull/3306)
+* @wangyy04 made their first contribution in [#3317](https://github.com/PrefectHQ/fastmcp/pull/3317)
+* @yangbaechu made their first contribution in [#3338](https://github.com/PrefectHQ/fastmcp/pull/3338)
+* @andrew-propelauth made their first contribution in [#3358](https://github.com/PrefectHQ/fastmcp/pull/3358)
+* @jer805 made their first contribution in [#3330](https://github.com/PrefectHQ/fastmcp/pull/3330)
+* @jvdmr made their first contribution in [#1796](https://github.com/PrefectHQ/fastmcp/pull/1796)
+
+**Full Changelog**: [v3.0.2...v3.1.0](https://github.com/PrefectHQ/fastmcp/compare/v3.0.2...v3.1.0)
+
+
+
**[v3.0.2: Threecovery Mode II](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.2)**
@@ -663,6 +1635,34 @@ Breaking changes are minimal: for most servers, updating the import statement is
+
+
+**[v2.14.7: Fake It Till You Break It](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.7)**
+
+A 2.x backport of the fakeredis pin: fakeredis 2.35.0 renamed a connection class that pydocket's `memory://` backend depended on, crashing `fastmcp[tasks]` installs at startup. This caps `fakeredis<2.35.0` on the 2.x line.
+
+### Fixes 🐞
+* fix(deps): cap fakeredis to `<2.35.0` to prevent startup crash on 2.x by [@vincent067](https://github.com/vincent067) in [#3883](https://github.com/PrefectHQ/fastmcp/pull/3883)
+
+**Full Changelog**: [v2.14.6...v2.14.7](https://github.com/PrefectHQ/fastmcp/compare/v2.14.6...v2.14.7)
+
+
+
+
+
+**[v2.14.6: $Ref Dead Redemption](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.6)**
+
+v2.14.4 backported `dereference_refs()` but never wired it into the tool schema pipeline — `$ref` and `$defs` were still sent to MCP clients. Now fixed: `compress_schema()` dereferences at both tool schema creation sites, so schemas are fully inlined before reaching clients.
+
+### Fixes 🐞
+* Updated deprecation URL for V2 by [@SrzStephen](https://github.com/SrzStephen) in [#3109](https://github.com/PrefectHQ/fastmcp/pull/3109)
+* Use MemoryStore for OAuth proxy tests by [@SrzStephen](https://github.com/SrzStephen) in [#3111](https://github.com/PrefectHQ/fastmcp/pull/3111)
+* fix: wire up dereference_refs() in tool schema pipeline by [@jlowin](https://github.com/jlowin) in [#3170](https://github.com/PrefectHQ/fastmcp/pull/3170)
+
+**Full Changelog**: [v2.14.5...v2.14.6](https://github.com/PrefectHQ/fastmcp/compare/v2.14.5...v2.14.6)
+
+
+
**[v2.14.5: Sealed Docket](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.5)**
@@ -1179,7 +2179,7 @@ Thank you to our new contributors and everyone who tested preview builds. Your f
* Add configurable redirect URI validation for OAuth providers by [@jlowin](https://github.com/jlowin) in [#1582](https://github.com/PrefectHQ/fastmcp/pull/1582)
* Remove invalid-argument-type ignore and fix type errors by [@jlowin](https://github.com/jlowin) in [#1588](https://github.com/PrefectHQ/fastmcp/pull/1588)
* Remove generate-schema from public CLI by [@jlowin](https://github.com/jlowin) in [#1591](https://github.com/PrefectHQ/fastmcp/pull/1591)
-* Skip flaky windows test / mulit-client garbage collection by [@jlowin](https://github.com/jlowin) in [#1592](https://github.com/PrefectHQ/fastmcp/pull/1592)
+* Skip flaky windows test / multi-client garbage collection by [@jlowin](https://github.com/jlowin) in [#1592](https://github.com/PrefectHQ/fastmcp/pull/1592)
* Add setting to disable logging configuration by [@isra17](https://github.com/isra17) in [#1575](https://github.com/PrefectHQ/fastmcp/pull/1575)
* Improve debug logging for nested Servers / Clients by [@strawgate](https://github.com/strawgate) in [#1604](https://github.com/PrefectHQ/fastmcp/pull/1604)
* Add GitHub pull request template by [@strawgate](https://github.com/strawgate) in [#1581](https://github.com/PrefectHQ/fastmcp/pull/1581)
@@ -1852,7 +2852,7 @@ FastMCP 2.8.0 introduces powerful new ways to customize and control your MCP ser
### Tool Transformation
-The highlight of this release is first-class [**Tool Transformation**](/patterns/tool-transformation), a new feature that lets you create enhanced variations of existing tools. You can now easily rename arguments, hide parameters, modify descriptions, and even wrap tools with custom validation or post-processing logic—all without rewriting the original code. This makes it easier than ever to adapt generic tools for specific LLM use cases or to simplify complex APIs. Huge thanks to [@strawgate](https://github.com/strawgate) for partnering on this, starting with [#591](https://github.com/PrefectHQ/fastmcp/discussions/591) and [#599](https://github.com/PrefectHQ/fastmcp/pull/599) and continuing offline.
+The highlight of this release is first-class [**Tool Transformation**](/servers/transforms/tool-transformation), a new feature that lets you create enhanced variations of existing tools. You can now easily rename arguments, hide parameters, modify descriptions, and even wrap tools with custom validation or post-processing logic—all without rewriting the original code. This makes it easier than ever to adapt generic tools for specific LLM use cases or to simplify complex APIs. Huge thanks to [@strawgate](https://github.com/strawgate) for partnering on this, starting with [#591](https://github.com/PrefectHQ/fastmcp/discussions/591) and [#599](https://github.com/PrefectHQ/fastmcp/pull/599) and continuing offline.
### Component Control
This release also gives you more granular control over which components are exposed to clients. With new [**tag-based filtering**](/servers/server#tag-based-filtering), you can selectively enable or disable tools, resources, and prompts based on tags, perfect for managing different environments or user permissions. Complementing this, every component now supports being [programmatically enabled or disabled](/servers/tools#disabling-tools), offering dynamic control over your server's capabilities.
@@ -2968,4 +3968,4 @@ This release is highlighted by the ability to handle complex JSON objects as MCP
The very first release of FastMCP! 🎉
**Full Changelog**: [Initial commits](https://github.com/PrefectHQ/fastmcp/commits/v0.1.0)
-
\ No newline at end of file
+
diff --git a/docs/cli/auth.mdx b/docs/cli/auth.mdx
index 71b89e08a..5a0a4314c 100644
--- a/docs/cli/auth.mdx
+++ b/docs/cli/auth.mdx
@@ -23,21 +23,24 @@ fastmcp auth cimd create \
```json
{
- "client_id": "https://your-domain.com/oauth/client.json",
+ "client_id": "https://YOUR-DOMAIN.com/path/to/client.json",
"client_name": "My App",
"redirect_uris": ["http://localhost:*/callback"],
- "token_endpoint_auth_method": "none"
+ "token_endpoint_auth_method": "none",
+ "grant_types": ["authorization_code"],
+ "response_types": ["code"]
}
```
-The generated document includes a placeholder `client_id` — update it to match the URL where you'll host the document before deploying.
+By default, the generated document includes a placeholder `client_id`. Update it to match the URL where you'll host the document before deploying, or pass `--client-id` when generating the file.
### Options
| Option | Flag | Description |
| ------ | ---- | ----------- |
| Name | `--name` | **Required.** Human-readable client name |
-| Redirect URI | `--redirect-uri` | **Required.** Allowed redirect URIs (repeatable) |
+| Redirect URI | `--redirect-uri`, `-r` | **Required.** Allowed redirect URIs (repeatable) |
+| Client ID | `--client-id` | URL where this document will be hosted; defaults to a placeholder |
| Client URI | `--client-uri` | Client's home page URL |
| Logo URI | `--logo-uri` | Client's logo URL |
| Scope | `--scope` | Space-separated list of scopes |
@@ -51,6 +54,7 @@ fastmcp auth cimd create \
--name "My Production App" \
--redirect-uri "http://localhost:*/callback" \
--redirect-uri "https://myapp.example.com/callback" \
+ --client-id "https://myapp.example.com/oauth/client.json" \
--client-uri "https://myapp.example.com" \
--scope "read write" \
--output client.json
diff --git a/docs/cli/client.mdx b/docs/cli/client.mdx
index bd72b163d..7dac8456d 100644
--- a/docs/cli/client.mdx
+++ b/docs/cli/client.mdx
@@ -104,11 +104,28 @@ Some tools request additional input during execution through MCP's elicitation m
| ------ | ---- | ----------- |
| Command | `--command` | Connect via stdio |
| Transport | `--transport`, `-t` | Force `http` or `sse` |
+| Prompt | `--prompt` | Treat the target as a prompt name instead of a tool/resource |
| Input JSON | `--input-json` | Base arguments as JSON (merged with `key=value`) |
| JSON | `--json` | Raw JSON output |
| Timeout | `--timeout` | Connection timeout in seconds |
| Auth | `--auth` | `oauth`, a bearer token, or `none` |
+## Reading Resources and Getting Prompts
+
+`fastmcp call` can also read resources and render prompts. If the target contains `://`, the CLI treats it as a resource URI and calls `read_resource`:
+
+```bash
+fastmcp call server.py resource://docs/readme
+fastmcp call server.py file:///tmp/example.txt --json
+```
+
+To get a prompt, pass `--prompt`; prompt arguments use the same `key=value` and `--input-json` forms as tool calls:
+
+```bash
+fastmcp call server.py summarize --prompt topic=weather
+fastmcp call server.py summarize --prompt --input-json '{"topic": "weather"}'
+```
+
## Discovering Configured Servers
`fastmcp discover` scans your machine for MCP servers configured in editors and tools. It checks:
@@ -138,3 +155,7 @@ Any server that appears here can be used by name with `list`, `call`, and other
For LLM agents that can execute shell commands but don't have native MCP support, the CLI provides a clean bridge. The agent calls `fastmcp list --json` to discover available tools with full schemas, then `fastmcp call --json` to invoke them with structured results.
Because the CLI handles connection management, transport selection, and type coercion internally, the agent doesn't need to understand MCP protocol details — it just reads JSON and constructs shell commands.
+
+## Remote Stdio Bridges
+
+For MCP hosts that expect a local stdio command but need to connect to a remote HTTP server, use [`fastmcp-remote`](/clients/fastmcp-remote). It provides a small standalone bridge for host configuration, while `fastmcp list` and `fastmcp call` remain focused on direct inspection and invocation from the terminal.
diff --git a/docs/cli/inspecting.mdx b/docs/cli/inspecting.mdx
index 657921357..4039e45f8 100644
--- a/docs/cli/inspecting.mdx
+++ b/docs/cli/inspecting.mdx
@@ -55,6 +55,11 @@ fastmcp inspect server.py --format mcp -o manifest.json
| ------ | ---- | ----------- |
| Format | `--format`, `-f` | `fastmcp` or `mcp` (required when using `-o`) |
| Output File | `--output`, `-o` | Save to file instead of stdout |
+| Python | `--python` | Python version to use when running via `uv` |
+| Extra Packages | `--with` | Additional packages to install (repeatable) |
+| Project | `--project` | Run within a specific uv project directory |
+| Requirements | `--with-requirements` | Install from a requirements file |
+| Skip Env | `--skip-env` | Do not set up a uv environment |
## Entrypoints
diff --git a/docs/cli/install-mcp.mdx b/docs/cli/install-mcp.mdx
index bf1b60b36..7d015592e 100644
--- a/docs/cli/install-mcp.mdx
+++ b/docs/cli/install-mcp.mdx
@@ -14,7 +14,7 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
```bash
fastmcp install claude-desktop server.py
fastmcp install claude-code server.py --with pandas --with matplotlib
-fastmcp install cursor server.py -e .
+fastmcp install cursor server.py --with-editable .
```
@@ -41,14 +41,13 @@ Because MCP clients run servers in isolation, you need to tell the install comma
```bash
fastmcp install claude-desktop server.py --with pandas --with "sqlalchemy>=2.0"
-fastmcp install cursor server.py -e . --with-requirements requirements.txt
+fastmcp install cursor server.py --with-editable . --with-requirements requirements.txt
```
-**`fastmcp.json`** configuration files declare dependencies alongside the server definition. When you install from a config file, dependencies are picked up automatically:
+**`fastmcp.json`** configuration files declare dependencies alongside the server definition. When you install from a config file explicitly, dependencies are picked up automatically:
```bash
fastmcp install claude-desktop fastmcp.json
-fastmcp install claude-desktop # auto-detects fastmcp.json in current directory
```
See [Server Configuration](/deployment/server-configuration) for the full config format.
@@ -57,14 +56,19 @@ See [Server Configuration](/deployment/server-configuration) for the full config
| Option | Flag | Description |
| ------ | ---- | ----------- |
-| Server Name | `--server-name`, `-n` | Custom name for the server |
-| Editable Package | `--with-editable`, `-e` | Install a directory in editable mode |
+| Server Name | `--name`, `-n` | Custom name for the server |
+| Editable Package | `--with-editable` | Install a directory in editable mode |
| Extra Packages | `--with` | Additional packages (repeatable) |
| Environment Variables | `--env` | `KEY=VALUE` pairs (repeatable) |
-| Environment File | `--env-file`, `-f` | Load env vars from a `.env` file |
+| Environment File | `--env-file` | Load env vars from a `.env` file |
| Python | `--python` | Python version (e.g., `3.11`) |
| Project | `--project` | Run within a uv project directory |
| Requirements | `--with-requirements` | Install from a requirements file |
+| Config Path | `--config-path` | Custom path to Claude Desktop config directory (`claude-desktop` only) |
+| Workspace | `--workspace` | Install to the workspace directory instead of globally (`cursor` only) |
+| Copy | `--copy` | Copy the generated output to the clipboard (`mcp-json` and `stdio` only) |
+
+`goose` installs through a deeplink that runs your server with `uvx`, so it accepts only `--name`, `--with`, and `--python`. Options that depend on a local uv project — `--with-editable`, `--project`, and `--with-requirements` — are unavailable there. Deeplinks also cannot carry environment variables: passing `--env` or `--env-file` exits with an error directing you to `fastmcp install mcp-json`, which generates a config you can add to Goose by hand with the variables included.
## Examples
@@ -72,12 +76,12 @@ See [Server Configuration](/deployment/server-configuration) for the full config
# Basic install with auto-detected server instance
fastmcp install claude-desktop server.py
-# Install from fastmcp.json with auto-detection
-fastmcp install claude-desktop
+# Install from fastmcp.json
+fastmcp install claude-desktop fastmcp.json
# Explicit entrypoint with dependencies
fastmcp install claude-desktop server.py:my_server \
- --server-name "My Analysis Server" \
+ --name "My Analysis Server" \
--with pandas
# With environment variables
@@ -92,6 +96,10 @@ fastmcp install cursor server.py --env-file .env
fastmcp install claude-desktop server.py \
--python 3.11 \
--with-requirements requirements.txt
+
+# With custom config path (claude-desktop only)
+fastmcp install claude-desktop server.py \
+ --config-path "C:\Users\username\AppData\Local\Packages\Claude_xyz\LocalCache\Roaming\Claude"
```
## Generating MCP JSON
diff --git a/docs/cli/overview.mdx b/docs/cli/overview.mdx
index 2cd4e145d..9085daaa8 100644
--- a/docs/cli/overview.mdx
+++ b/docs/cli/overview.mdx
@@ -18,11 +18,12 @@ fastmcp --help
| Command | What it does |
| ------- | ------------ |
| [`run`](/cli/running) | Run a server (local file, factory function, remote URL, or config file) |
+| [`dev apps`](/cli/running#previewing-apps) | Launch a browser-based preview UI for Prefab App tools |
| [`dev inspector`](/cli/running#development-with-the-inspector) | Launch a server inside the MCP Inspector for interactive testing |
| [`install`](/cli/install-mcp) | Install a server into Claude Code, Claude Desktop, Cursor, Gemini CLI, or Goose |
| [`inspect`](/cli/inspecting) | Print a server's tools, resources, and prompts as a summary or JSON report |
| [`list`](/cli/client) | List a server's tools (and optionally resources and prompts) |
-| [`call`](/cli/client#calling-tools) | Call a single tool with arguments |
+| [`call`](/cli/client#calling-tools) | Call a tool, read a resource, or get a prompt |
| [`discover`](/cli/client#discovering-configured-servers) | Find MCP servers configured in your editors and tools |
| [`generate-cli`](/cli/generate-cli) | Scaffold a standalone typed CLI from a server's tool schemas |
| [`project prepare`](/cli/running#pre-building-environments) | Pre-install dependencies into a reusable uv project |
@@ -88,10 +89,10 @@ To skip authentication entirely — useful for local development servers — pas
fastmcp call http://localhost:8000/mcp my_tool --auth none
```
-You can also pass a bearer token directly:
+You can also pass a bearer token directly. Give the token value on its own; FastMCP adds the `Bearer` prefix when it builds the `Authorization` header.
```bash
-fastmcp list http://localhost:8000/mcp --auth "Bearer sk-..."
+fastmcp list http://localhost:8000/mcp --auth "sk-..."
```
## Transport Override
diff --git a/docs/cli/running.mdx b/docs/cli/running.mdx
index dd976d561..92cf764ff 100644
--- a/docs/cli/running.mdx
+++ b/docs/cli/running.mdx
@@ -69,19 +69,22 @@ fastmcp run mcp.json
```
-`fastmcp run` completely ignores the `if __name__ == "__main__"` block. Any setup code in that block won't execute. If you need initialization logic to run, use a [factory function](/cli/overview#factory-functions).
+`fastmcp run` completely ignores the `if __name__ == "__main__"` block. Any setup code in that block won't execute. If you need initialization logic to run, use a [factory function](#entrypoints).
### Options
| Option | Flag | Description |
| ------ | ---- | ----------- |
-| Transport | `--transport`, `-t` | `stdio` (default), `http`, or `sse` |
+| Transport | `--transport`, `-t` | `stdio` (default), `http` / `streamable-http`, or `sse` |
| Host | `--host` | Bind address for HTTP (default: `127.0.0.1`) |
| Port | `--port`, `-p` | Bind port for HTTP (default: `8000`) |
-| Path | `--path` | URL path for HTTP (default: `/mcp/`) |
+| Path | `--path` | URL path for HTTP (default: `/mcp` for `http`, `/sse` for `sse`) |
| Log Level | `--log-level`, `-l` | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
| No Banner | `--no-banner` | Suppress the startup banner |
+| Stateless | `--stateless` | Run without sessions, for serverless and multi-worker deployments |
+| Module Mode | `--module`, `-m` | Run a Python module via `python -m` instead of a file path |
+| Skip Source | `--skip-source` | Skip source preparation (use when the source is already prepared) |
| Auto-Reload | `--reload` / `--no-reload` | Watch for file changes and restart automatically |
| Reload Dirs | `--reload-dir` | Directories to watch (repeatable) |
| Skip Env | `--skip-env` | Don't set up a uv environment (use when already in one) |
@@ -96,13 +99,38 @@ By default, `fastmcp run` uses your current Python environment directly. When yo
The `--skip-env` flag is useful when you're already inside an activated venv, a Docker container with pre-installed dependencies, or a uv-managed project — it prevents uv from trying to set up another environment layer.
+## Previewing Apps
+
+
+
+`fastmcp dev apps` launches a browser-based preview UI for servers with [Prefab App tools](/apps/prefab). It starts your MCP server on one port and a local dev UI on another — giving you a live, interactive picker where you can call app tools and see their rendered output without needing a full MCP host client.
+
+```bash
+fastmcp dev apps server.py
+fastmcp dev apps server.py:mcp --mcp-port 9000 --dev-port 9090
+```
+
+The picker auto-generates a form from each tool's input schema. Submit the form and the result opens in a new tab as a rendered Prefab UI.
+
+Auto-reload is on by default — save a file and the MCP server restarts automatically.
+
+
+`fastmcp dev apps` requires `fastmcp[apps]` — install with `pip install "fastmcp[apps]"`.
+
+
+| Option | Flag | Description |
+| ------ | ---- | ----------- |
+| MCP Port | `--mcp-port` | Port for the MCP server (default: `8000`) |
+| Dev Port | `--dev-port` | Port for the dev UI (default: `8080`) |
+| Auto-Reload | `--reload` / `--no-reload` | Watch for file changes (default: on) |
+
## Development with the Inspector
`fastmcp dev inspector` launches your server inside the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), a browser-based tool for interactively testing MCP servers. Auto-reload is on by default, so your server restarts when you save changes.
```bash
fastmcp dev inspector server.py
-fastmcp dev inspector server.py -e . --with pandas
+fastmcp dev inspector server.py --with-editable . --with pandas
```
@@ -115,7 +143,7 @@ The Inspector connects over **stdio only**. When it launches, you may need to se
| Option | Flag | Description |
| ------ | ---- | ----------- |
-| Editable Package | `--with-editable`, `-e` | Install a directory in editable mode |
+| Editable Package | `--with-editable` | Install a directory in editable mode |
| Extra Packages | `--with` | Additional packages (repeatable) |
| Inspector Version | `--inspector-version` | MCP Inspector version to use |
| UI Port | `--ui-port` | Port for the Inspector UI |
diff --git a/docs/clients/auth/bearer.mdx b/docs/clients/auth/bearer.mdx
index 2e12fbc13..109f54df6 100644
--- a/docs/clients/auth/bearer.mdx
+++ b/docs/clients/auth/bearer.mdx
@@ -37,7 +37,7 @@ async with Client(
"https://your-server.fastmcp.app/mcp",
auth="",
) as client:
- await client.ping()
+ await client.list_tools()
```
You can also supply a Bearer token to a transport instance, such as `StreamableHttpTransport` or `SSETransport`:
@@ -52,12 +52,12 @@ transport = StreamableHttpTransport(
)
async with Client(transport) as client:
- await client.ping()
+ await client.list_tools()
```
## `BearerAuth` Helper
-If you prefer to be more explicit and not rely on FastMCP to transform your string token, you can use the `BearerAuth` class yourself, which implements the `httpx.Auth` interface.
+If you prefer to be more explicit and not rely on FastMCP to transform your string token, you can use the `BearerAuth` class yourself, which implements the `httpx2.Auth` interface.
```python {6}
from fastmcp import Client
@@ -67,7 +67,7 @@ async with Client(
"https://your-server.fastmcp.app/mcp",
auth=BearerAuth(token=""),
) as client:
- await client.ping()
+ await client.list_tools()
```
## Custom Headers
@@ -84,5 +84,5 @@ async with Client(
headers={"X-API-Key": ""},
),
) as client:
- await client.ping()
+ await client.list_tools()
```
diff --git a/docs/clients/auth/cimd.mdx b/docs/clients/auth/cimd.mdx
index c1f92d1c4..5b1a6b934 100644
--- a/docs/clients/auth/cimd.mdx
+++ b/docs/clients/auth/cimd.mdx
@@ -32,7 +32,7 @@ async with Client(
client_metadata_url="https://myapp.example.com/oauth/client.json",
),
) as client:
- await client.ping()
+ await client.list_tools()
```
When the server supports CIMD, the client uses your metadata URL as its `client_id` instead of performing Dynamic Client Registration. The server fetches your document, validates it, and proceeds with the standard OAuth authorization flow.
diff --git a/docs/clients/auth/client-credentials.mdx b/docs/clients/auth/client-credentials.mdx
new file mode 100644
index 000000000..382a5d8e9
--- /dev/null
+++ b/docs/clients/auth/client-credentials.mdx
@@ -0,0 +1,89 @@
+---
+title: Machine-to-Machine Authentication
+sidebarTitle: Client Credentials
+description: Authenticate your FastMCP client to a protected server without a browser.
+icon: robot
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+
+Machine-to-machine authentication is only relevant for HTTP-based transports.
+
+
+When a FastMCP client runs without a human present — a backend service, a scheduled job, a CI pipeline, one MCP server calling another — it cannot complete the browser-based [OAuth](/clients/auth/oauth) flow. Instead it authenticates as itself using the OAuth 2.0 **client credentials** grant: the client presents its own credentials directly to the authorization server, receives an access token, and attaches that token to every request. There is no redirect, no consent screen, and no user.
+
+FastMCP provides two providers for this, both implementing the `httpx2.Auth` interface so they drop into the same `auth=` parameter as every other client auth option. You pass the **MCP server URL**, not a token endpoint — the token endpoint is discovered from the server's OAuth metadata, exactly as the interactive `OAuth` helper does. As with `OAuth`, you can omit the URL entirely and let the transport supply it.
+
+## Client ID and Secret
+
+The common case is a pre-registered client with an ID and a secret. Use `ClientCredentialsOAuthProvider` and pass it to the `auth` parameter of your `Client` or transport:
+
+```python {2, 4-8, 10}
+from fastmcp import Client
+from fastmcp.client.auth import ClientCredentialsOAuthProvider
+
+auth = ClientCredentialsOAuthProvider(
+ client_id="my-client-id",
+ client_secret="my-client-secret",
+ scopes=["read", "write"],
+)
+
+async with Client("https://example.com/mcp", auth=auth) as client:
+ await client.list_tools()
+```
+
+The provider discovers the authorization server, exchanges the credentials for an access token, and caches the token in memory for the life of the client. When the token expires it is re-acquired automatically on the next request. Because re-acquiring a token is a single non-interactive request, tokens are held in memory by default with no warning — unlike the interactive `OAuth` flow, losing the cache on restart costs nothing.
+
+### `ClientCredentialsOAuthProvider` Parameters
+
+- **`mcp_url`** (`str`, optional): Full URL to the MCP endpoint. Omit it when passing the provider to `Client(auth=...)` — the transport supplies the URL automatically.
+- **`client_id`** (`str`, required): The pre-registered OAuth client ID.
+- **`client_secret`** (`str`, required): The OAuth client secret.
+- **`scopes`** (`str | list[str]`, optional): Scopes to request, as a space-separated string or a list.
+- **`token_endpoint_auth_method`** (`"client_secret_basic" | "client_secret_post"`, optional): How the credentials are presented to the token endpoint. Defaults to `"client_secret_basic"` (an HTTP Basic `Authorization` header); use `"client_secret_post"` to send them in the request body instead.
+- **`token_storage`** (`AsyncKeyValue`, optional): A key-value store for the acquired token. Defaults to in-memory storage.
+
+## Private Key JWT
+
+Some authorization servers require the client to prove its identity with a signed JWT assertion (RFC 7523 `private_key_jwt`) instead of a shared secret. This is common with workload identity federation, where the assertion comes from a cloud identity provider. Use `PrivateKeyJWTOAuthProvider` and supply an `assertion_provider` — an async callback that receives the authorization server's issuer identifier (the required JWT audience) and returns the assertion.
+
+For a locally signed assertion, build the callback with `SignedJWTParameters`:
+
+```python {4-7, 9, 11-15, 17-20, 22}
+from pathlib import Path
+
+from fastmcp import Client
+from fastmcp.client.auth import (
+ PrivateKeyJWTOAuthProvider,
+ SignedJWTParameters,
+)
+
+private_key_pem = Path("client-signing-key.pem").read_text()
+
+jwt_params = SignedJWTParameters(
+ issuer="my-client-id",
+ subject="my-client-id",
+ signing_key=private_key_pem,
+)
+
+auth = PrivateKeyJWTOAuthProvider(
+ client_id="my-client-id",
+ assertion_provider=jwt_params.create_assertion_provider(),
+)
+
+async with Client("https://example.com/mcp", auth=auth) as client:
+ await client.list_tools()
+```
+
+If you already have a JWT from an identity provider, wrap it with `static_assertion_provider`, or pass your own `async def provider(audience: str) -> str` callback to fetch one on demand.
+
+### `PrivateKeyJWTOAuthProvider` Parameters
+
+- **`mcp_url`** (`str`, optional): Full URL to the MCP endpoint. Omit it when passing the provider to `Client(auth=...)`.
+- **`client_id`** (`str`, required): The OAuth client ID.
+- **`assertion_provider`** (`Callable[[str], Awaitable[str]]`, required): Async callback that receives the authorization server's issuer identifier and returns a signed JWT assertion.
+- **`scopes`** (`str | list[str]`, optional): Scopes to request, as a space-separated string or a list.
+- **`token_storage`** (`AsyncKeyValue`, optional): A key-value store for the acquired token. Defaults to in-memory storage.
diff --git a/docs/clients/auth/oauth.mdx b/docs/clients/auth/oauth.mdx
index 84fbe2164..21236406d 100644
--- a/docs/clients/auth/oauth.mdx
+++ b/docs/clients/auth/oauth.mdx
@@ -29,13 +29,13 @@ from fastmcp import Client
# Uses default OAuth settings
async with Client("https://your-server.fastmcp.app/mcp", auth="oauth") as client:
- await client.ping()
+ await client.list_tools()
```
### `OAuth` Helper
-To fully configure the OAuth flow, use the `OAuth` helper and pass it to the `auth` parameter of the `Client` or transport instance. `OAuth` manages the complexities of the OAuth 2.1 Authorization Code Grant with PKCE (Proof Key for Code Exchange) for enhanced security, and implements the full `httpx.Auth` interface.
+To fully configure the OAuth flow, use the `OAuth` helper and pass it to the `auth` parameter of the `Client` or transport instance. `OAuth` manages the complexities of the OAuth 2.1 Authorization Code Grant with PKCE (Proof Key for Code Exchange) for enhanced security, and implements the full `httpx2.Auth` interface.
```python {2, 4, 6}
from fastmcp import Client
@@ -44,7 +44,7 @@ from fastmcp.client.auth import OAuth
oauth = OAuth(scopes=["user"])
async with Client("https://your-server.fastmcp.app/mcp", auth=oauth) as client:
- await client.ping()
+ await client.list_tools()
```
@@ -61,7 +61,7 @@ You don't need to pass `mcp_url` when using `OAuth` with `Client(auth=...)` —
- **`token_storage`** (`AsyncKeyValue`, optional): Storage backend for persisting OAuth tokens. Defaults to in-memory storage (tokens lost on restart). See [Token Storage](#token-storage) for encrypted storage options
- **`additional_client_metadata`** (`dict[str, Any]`, optional): Extra metadata for client registration
- **`callback_port`** (`int`, optional): Fixed port for OAuth callback server. If not specified, uses a random available port
-- **`httpx_client_factory`** (`McpHttpClientFactory`, optional): Factory for creating httpx clients
+- **`httpx_client_factory`** (`McpHttpClientFactory`, optional): Factory for creating httpx2 clients
## OAuth Flow
@@ -125,7 +125,7 @@ encrypted_storage = FernetEncryptionWrapper(
oauth = OAuth(token_storage=encrypted_storage)
async with Client("https://your-server.fastmcp.app/mcp", auth=oauth) as client:
- await client.ping()
+ await client.list_tools()
```
You can use any `AsyncKeyValue`-compatible backend from the [key-value library](https://github.com/strawgate/py-key-value) including Redis, DynamoDB, and more. Wrap your storage in `FernetEncryptionWrapper` for encryption.
@@ -150,7 +150,7 @@ async with Client(
client_metadata_url="https://myapp.example.com/oauth/client.json",
),
) as client:
- await client.ping()
+ await client.list_tools()
```
See the [CIMD Authentication](/clients/auth/cimd) page for complete documentation on creating, hosting, and validating CIMD documents.
@@ -172,7 +172,7 @@ async with Client(
client_secret="my-client-secret",
),
) as client:
- await client.ping()
+ await client.list_tools()
```
Public clients that rely on PKCE for security can omit `client_secret`:
diff --git a/docs/clients/client-only-package.mdx b/docs/clients/client-only-package.mdx
new file mode 100644
index 000000000..020b2f077
--- /dev/null
+++ b/docs/clients/client-only-package.mdx
@@ -0,0 +1,89 @@
+---
+title: Client-Only Package
+description: Use FastMCP's client without installing the full server framework.
+icon: box
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+FastMCP's full `fastmcp` package includes everything needed to build and run MCP servers, apps, proxies, and clients. If you are only embedding an MCP client in another framework, building your own LLM host, or testing MCP servers, you can install the smaller client-only package instead.
+
+```bash
+pip install "fastmcp-slim[client]"
+```
+
+The client-only package uses the `fastmcp` import namespace:
+
+```python
+from fastmcp import Client
+
+client = Client("https://example.com/mcp")
+```
+
+Use `fastmcp-slim[client]` when your code connects to MCP servers but does not define or run FastMCP servers itself. For example, framework authors can depend on `fastmcp-slim[client]` to provide MCP connectivity without requiring users to install the full FastMCP server stack.
+
+## Supported Usage
+
+Client-only installs support remote and subprocess transports:
+
+```python
+from fastmcp import Client
+
+# Remote MCP server
+http_client = Client("https://example.com/mcp")
+
+# Local MCP server over stdio
+stdio_client = Client("my_server.py")
+```
+
+Single-server MCP configuration works as well:
+
+```python
+from fastmcp import Client
+
+config = {
+ "mcpServers": {
+ "weather": {
+ "url": "https://weather.example.com/mcp"
+ }
+ }
+}
+
+client = Client(config)
+```
+
+Optional sampling handlers are available through the same extras as the full package:
+
+```bash
+pip install "fastmcp-slim[client,openai]"
+pip install "fastmcp-slim[client,anthropic]"
+pip install "fastmcp-slim[client,gemini]"
+```
+
+## When to Use the Full Package
+
+Install `fastmcp` when you need server-side FastMCP features:
+
+```bash
+pip install fastmcp
+```
+
+The full package remains the default for most users and continues to support the existing import style:
+
+```python
+from fastmcp import Client, FastMCP
+
+server = FastMCP("Example")
+client = Client(server)
+```
+
+Use the full package for:
+
+- defining or running FastMCP servers
+- in-memory clients connected directly to `FastMCP` server objects
+- multi-server MCP configurations
+- FastMCP apps, proxies, server auth, middleware, and other server-side features
+
+The `fastmcp-slim` package is intentionally narrower: it is for client-only consumers who want FastMCP's MCP client behavior without depending on the full framework.
diff --git a/docs/clients/client.mdx b/docs/clients/client.mdx
index fc5ddc263..fb910777f 100644
--- a/docs/clients/client.mdx
+++ b/docs/clients/client.mdx
@@ -37,9 +37,6 @@ client = Client("my_mcp_server.py")
async def main():
async with client:
- # Basic server interaction
- await client.ping()
-
# List available operations
tools = await client.list_tools()
resources = await client.list_resources()
@@ -67,16 +64,21 @@ server = FastMCP("TestServer")
client = Client(server) # In-memory, no network or subprocess
```
-**STDIO transport** launches a server as a subprocess and communicates through stdin/stdout pipes. This is the standard mechanism used by desktop clients like Claude Desktop. The subprocess runs in an isolated environment, so you must explicitly pass any environment variables the server needs.
+**STDIO transport** launches a server as a subprocess and communicates through stdin/stdout pipes. This is the standard mechanism used by desktop clients like Claude Desktop. By default, the subprocess receives the MCP SDK's default environment; pass an explicit transport when you need to add environment variables, set a working directory, or control process reuse.
```python
from fastmcp import Client
+from fastmcp.client.transports import PythonStdioTransport
# Simple inference from file path
client = Client("my_server.py")
# With explicit environment configuration
-client = Client("my_server.py", env={"API_KEY": "secret"})
+transport = PythonStdioTransport(
+ "my_server.py",
+ env={"API_KEY": "secret"},
+)
+client = Client(transport)
```
**HTTP transport** connects to servers running as web services. Use this for production deployments where the server runs independently and manages its own lifecycle.
@@ -121,7 +123,7 @@ async with client:
## Connection Lifecycle
-The client uses context managers for connection management. When you enter the context, the client establishes a connection and performs an MCP initialization handshake with the server. This handshake exchanges capabilities, server metadata, and instructions.
+The client uses context managers for connection management. When you enter the context, the client establishes a connection and negotiates the protocol era with the server. Metadata returned by either legacy initialization or modern discovery is exposed through the same client properties.
```python
from fastmcp import Client, FastMCP
@@ -134,18 +136,20 @@ def greet(name: str) -> str:
return f"Hello, {name}!"
async with Client(mcp) as client:
- # Initialization already happened automatically
- print(f"Server: {client.initialize_result.serverInfo.name}")
- print(f"Instructions: {client.initialize_result.instructions}")
- print(f"Capabilities: {client.initialize_result.capabilities.tools}")
+ # Protocol negotiation already happened automatically
+ assert client.server_info is not None
+ assert client.server_capabilities is not None
+ print(f"Server: {client.server_info.name}")
+ print(f"Instructions: {client.instructions}")
+ print(f"Capabilities: {client.server_capabilities.tools}")
```
-For advanced scenarios where you need precise control over when initialization happens, disable automatic initialization and call `initialize()` manually:
+For advanced scenarios where you need precise control over when initialization happens, disable automatic initialization and call `initialize()` manually. `initialize()` is a handshake-era operation, so pin the connection with `mode="legacy"`: the modern protocol has no `initialize` round trip, and calling it on a modern connection raises.
```python
from fastmcp import Client
-client = Client("my_mcp_server.py", auto_initialize=False)
+client = Client("my_mcp_server.py", auto_initialize=False, mode="legacy")
async with client:
# Connection established, but not initialized yet
@@ -154,12 +158,144 @@ async with client:
# Initialize manually with custom timeout
result = await client.initialize(timeout=10.0)
- print(f"Server: {result.serverInfo.name}")
+ print(f"Server: {result.server_info.name}")
# Now ready for operations
tools = await client.list_tools()
```
+## Protocol negotiation
+
+
+
+MCP has two protocol eras: the original *legacy* era, which begins every connection with an `initialize` handshake, and the *modern* era (protocol version `2026-07-28` and later), which a client discovers by probing the server's `server/discover` endpoint. The `mode` parameter controls which era the client negotiates when it connects.
+
+By default, `mode="auto"`. The client probes `server/discover` and adopts the modern protocol when the server responds; for any server that is not positive evidence of modern support, it falls back to the legacy handshake. This makes the default safe against a mixed fleet of legacy and modern servers.
+
+```python
+from fastmcp import Client
+
+# Negotiate the newest era the server supports (the default)
+client = Client("https://example.com/mcp", mode="auto")
+```
+
+Set `mode="legacy"` to force the initialize handshake. This behaves identically to earlier FastMCP versions and is the opt-out if a server misbehaves under discovery or you need the legacy `initialize` result object.
+
+```python
+client = Client("https://example.com/mcp", mode="legacy")
+```
+
+Legacy mode is also what carries the *pushed* form of a server's requests. The handshake opens a persistent back-channel down which a server can send a sampling, roots, or elicitation request mid-call, and the modern era removed it. Your handlers are unaffected by that: a [sampling](/clients/sampling), [roots](/clients/roots), or [elicitation](/clients/elicitation) handler you register answers a modern server's [input-required rounds](/clients/elicitation#input-required-rounds) from the same registration. Pin `mode="legacy"` when you connect to a server that pushes, or when your code calls `client.ping()` or `transport.get_session_id()`, which need the session the modern era does not open.
+
+Conversely, [background tasks](/clients/tasks) are **modern-only**: the tasks capability is negotiated over `2026-07-28` connections, so `mode="legacy"` never triggers one and a task-enabled tool just runs synchronously.
+
+A FastMCP server serves both eras, so a default client negotiates the modern one and the session-dependent calls raise an era-specific error there. Pinning the handshake restores them.
+
+You can also pin a specific modern protocol version to adopt it directly, without a discovery probe:
+
+```python
+client = Client("https://example.com/mcp", mode="2026-07-28")
+```
+
+Once connected, the negotiated version, server identity, capabilities, and instructions are available as properties. They are populated from either the legacy `InitializeResult` or modern `DiscoverResult`, and reset to `None` when the client disconnects. `instructions` is also `None` when the server does not provide any.
+
+When you pin a modern version directly, the client skips discovery and adopts that version with minimal synthesized metadata. In that mode, `server_info` has an empty name and `instructions` is `None`.
+
+```python
+async with Client("https://example.com/mcp", mode="auto") as client:
+ print(client.protocol_version) # e.g. "2026-07-28"
+ print(client.server_info) # Implementation | None
+ print(client.server_capabilities) # ServerCapabilities | None
+ print(client.instructions) # str | None
+```
+
+
+`mode="auto"` is the default as of FastMCP 4.0. Earlier versions defaulted to `"legacy"`. If a server behaves unexpectedly under discovery, or you depend on the legacy `initialize` result, pin the old behavior with `Client(..., mode="legacy")`.
+
+The SSE transport is legacy-only — it cannot carry the sessionless modern era — so a client connecting over SSE always negotiates the legacy handshake, even under `mode="auto"`. A multi-server config (`MCPConfigTransport` with more than one server) is likewise legacy-only, because it mounts each backend behind a legacy-era proxy; a single-server config mirrors its one backend transport's era.
+
+
+## Response caching
+
+
+
+The client can cache the results of `list_tools`, `list_resources`, and `list_prompts` so that repeated calls avoid a network round-trip. Caching is opt-in and honors the server's own cache hints, so it only takes effect against modern-era servers that advertise them — a cache is inert on a legacy connection.
+
+Enable the default in-memory cache by passing `cache=True`. It respects the `ttlMs` and `cacheScope` hints the server attaches to each response.
+
+```python
+from fastmcp import Client
+
+client = Client("https://example.com/mcp", mode="auto", cache=True)
+
+async with client:
+ tools = await client.list_tools() # fetched from the server
+ tools = await client.list_tools() # served from the cache
+```
+
+The default (`cache=None`) and `cache=False` both disable caching. For control over the store, TTL, or partitioning, pass a `CacheConfig`. A custom config requires a `target_id`, since in-memory FastMCP transports expose no server URL to derive a shared-store identity from.
+
+```python
+from fastmcp import Client
+from mcp.client.caching import CacheConfig
+
+config = CacheConfig(target_id="weather-api", default_ttl_ms=60_000)
+client = Client("https://example.com/mcp", mode="auto", cache=config)
+```
+
+The high-level `list_tools`, `list_resources`, and `list_prompts` methods always use the cache when one is configured. To override the behavior for a single call, use the lower-level `list_tools_mcp`, `list_resources_mcp`, `list_resource_templates_mcp`, and `list_prompts_mcp` variants, which accept a `cache_mode` argument: `"use"` (the default) serves and stores, `"refresh"` stores a fresh result without serving a cached one, and `"bypass"` skips the cache entirely.
+
+```python
+async with client:
+ fresh = await client.list_tools_mcp(cache_mode="refresh")
+```
+
+### Sharing a cache across clients
+
+The default cache lives in each client's process. To share cached responses across a fleet — a set of proxy replicas backed by one Redis, for example — pass a `KeyValueResponseCacheStore`, FastMCP's adapter over the same `AsyncKeyValue` key-value abstraction the event store and OAuth proxy use. It accepts any compatible backend (memory, Redis, and more).
+
+A shared store mingles responses from different principals, so it requires an explicit `partition` that isolates them. Derive the partition from a verified credential — never from request data or the server URL — and construct a new client when the principal changes. Only responses the server marks `"public"` are ever served across partitions.
+
+```python
+from fastmcp import Client
+from fastmcp.client.caching import KeyValueResponseCacheStore
+from mcp.client.caching import CacheConfig
+from key_value.aio.stores.redis import RedisStore
+
+backend = RedisStore(url="redis://localhost")
+store = KeyValueResponseCacheStore(storage=backend)
+
+config = CacheConfig(store=store, partition="tenant-a", target_id="weather-api")
+client = Client("https://example.com/mcp", mode="auto", cache=config)
+```
+
+The adapter serializes each result through a type-tagged envelope validated against an allowlist of cacheable result models, so a value naming an unknown type is treated as a cache miss rather than deserialized blindly. Each store instance owns its own collection namespace; `clear()` affects only that namespace, never another tenant's entries.
+
+## Client extensions
+
+
+
+Client extensions (SEP-2133) are the advanced mechanism a client uses to opt into vendor capabilities that live outside the core protocol. An extension is a `ClientExtension` instance that bundles three things: a capability *advertisement* the server can read, one or more *result claims* that let the client parse extra `tools/call` result shapes, and *notification bindings* that observe server notifications the core protocol doesn't define. Pass a sequence of them to `extensions=`.
+
+```python
+from fastmcp import Client
+from myproject.extensions import AppsExtension
+
+client = Client("https://example.com/mcp", extensions=[AppsExtension()])
+```
+
+Each extension's contributions are threaded into the underlying session. FastMCP folds in its own internal extension for [background tasks](/clients/tasks) automatically, and your own extensions *compose* with it rather than replacing it — pass your own tasks extension with the same identifier if you need to override it. When a tool returns a shape an extension claims, `client.call_tool()` resolves it transparently through the owning claim's resolver and hands you back an ordinary result. Result claims and their advertisements are honored only on modern-era connections, so they are inert on a legacy handshake.
+
+For the rare case where you need to register additional result claims against an extension that is already advertised, pass them through `result_claims=`, keyed by the extension's identifier. Prefer declaring claims on the extension itself; this parameter merges extra claims with an extension's own.
+
+```python
+client = Client(
+ "https://example.com/mcp",
+ extensions=[AppsExtension()],
+ result_claims={"example.com/apps": [extra_claim]},
+)
+```
+
## Operations
FastMCP clients interact with three types of server components.
@@ -201,6 +337,8 @@ See [Prompts](/clients/prompts) for detailed documentation including argument se
The client supports callback handlers for advanced server interactions. These let you respond to server-initiated requests and receive notifications.
+Sampling, elicitation, and roots are the requests a server makes of the client. A server reaches your handler by whichever route its [era](#protocol-negotiation) allows — pushed down the open session on the handshake, returned as an input-required result on the modern protocol — and both routes dispatch to the same handler, so one registration covers both. Logging and progress arrive as notifications on the response stream and work in either era.
+
```python
from fastmcp import Client
from fastmcp.client.logging import LogMessage
diff --git a/docs/clients/elicitation.mdx b/docs/clients/elicitation.mdx
index 33adbb6d6..73cc6fdb7 100644
--- a/docs/clients/elicitation.mdx
+++ b/docs/clients/elicitation.mdx
@@ -13,6 +13,10 @@ Use this when you need to respond to server requests for user input during tool
Elicitation allows MCP servers to request structured input from users during operations. Instead of requiring all inputs upfront, servers can interactively ask for missing parameters, request clarification, or gather additional context.
+
+**These sections show the server-initiated flow, which the handshake-era protocol uses.** On `2026-07-28` the server asks by returning a request instead — see [input-required rounds](#input-required-rounds). One `elicitation_handler` serves both, so the examples below pin `mode="legacy"` only to exercise the pushed form.
+
+
## Handler Template
```python
@@ -30,8 +34,8 @@ async def elicitation_handler(
Args:
message: The prompt to display to the user
- response_type: Python dataclass type for the response (None if no data expected)
- params: Original MCP elicitation parameters including raw JSON schema
+ response_type: Python dataclass type for form responses (None for URL requests or empty schemas)
+ params: Original MCP elicitation parameters
context: Request context with metadata
Returns:
@@ -44,18 +48,24 @@ async def elicitation_handler(
if not user_input:
return ElicitResult(action="decline")
+ # URL requests and empty-object schemas have no response type to construct,
+ # so accepting is the whole response.
+ if response_type is None:
+ return ElicitResult(action="accept")
+
# Create response using the provided dataclass type
return response_type(value=user_input)
client = Client(
"my_mcp_server.py",
+ mode="legacy",
elicitation_handler=elicitation_handler,
)
```
## How It Works
-When a server needs user input, it sends an elicitation request with a message prompt and a JSON schema describing the expected response structure. FastMCP automatically converts this schema into a Python dataclass type, making it easy to construct properly typed responses without manually parsing JSON schemas.
+When a server needs user input, it sends an elicitation request with a message prompt. Form elicitation requests include a JSON schema describing the expected response structure, and FastMCP automatically converts that schema into a Python dataclass type. URL elicitation requests and empty-object schemas use `response_type=None`.
The handler receives four parameters:
@@ -65,11 +75,11 @@ The handler receives four parameters:
- A Python dataclass type that FastMCP created from the server's JSON schema. Use this to construct your response with proper typing. If the server requests an empty object, this will be `None`.
+ A Python dataclass type that FastMCP created from a form request's JSON schema. Use this to construct your response with proper typing. For URL requests or empty-object schemas, this will be `None`.
- The original MCP elicitation parameters, including the raw JSON schema in `params.requestedSchema`
+ The original MCP elicitation parameters. Form requests carry the raw JSON schema on `params.requested_schema`; URL requests carry `params.url` instead and have no schema.
@@ -133,6 +143,24 @@ async def elicitation_handler(message, response_type, params, context):
client = Client(
"my_mcp_server.py",
+ mode="legacy",
elicitation_handler=elicitation_handler
)
```
+
+## Input-required rounds
+
+
+
+On protocol version `2026-07-28` and later, a server can ask for input before it returns a final result. Nothing is held open: the tool *returns* a description of what it needs, which completes that round as an ordinary response, and the client answers by issuing a **new** `call_tool`, `get_prompt`, or `read_resource` request carrying the answer. `fastmcp.Client` drives that loop for you — it fulfils each round's requests using the callbacks you already configured (your `elicitation_handler`, `sampling_handler`, and roots) and repeats until the call reaches a terminal result. No extra wiring is needed beyond the handlers described above.
+
+The `input_required_max_rounds` parameter caps how many rounds the client will answer before giving up, guarding against a server that never terminates. It defaults to `10`.
+
+```python
+client = Client(
+ "https://example.com/mcp",
+ mode="auto",
+ elicitation_handler=elicitation_handler,
+ input_required_max_rounds=5,
+)
+```
diff --git a/docs/clients/fastmcp-remote.mdx b/docs/clients/fastmcp-remote.mdx
new file mode 100644
index 000000000..dfc36a82b
--- /dev/null
+++ b/docs/clients/fastmcp-remote.mdx
@@ -0,0 +1,169 @@
+---
+title: fastmcp-remote
+description: Bridge remote MCP servers into stdio-only MCP hosts with uvx fastmcp-remote.
+icon: bridge
+---
+
+`fastmcp-remote` is FastMCP's standalone stdio bridge for remote MCP servers. Use it when an MCP host expects to launch a local command, but the server you want to use is hosted over Streamable HTTP or SSE.
+
+```json
+{
+ "mcpServers": {
+ "linear": {
+ "command": "uvx",
+ "args": ["fastmcp-remote", "https://mcp.linear.app/mcp"]
+ }
+ }
+}
+```
+
+The package is powered by FastMCP. It builds one FastMCP client for the remote URL, exposes that client as a local stdio proxy, and keeps the executable focused on that bridge. For running Python server files, local project environments, FastMCP config files, and development reload loops, use [`fastmcp run`](/cli/running).
+
+The command shape follows the original [`mcp-remote`](https://github.com/geelen/mcp-remote) npm project, which established this stdio-to-remote bridge pattern for MCP hosts.
+
+## Installation
+
+Most MCP hosts can run `fastmcp-remote` directly through `uvx`, so you usually do not need to install it yourself:
+
+```bash
+uvx fastmcp-remote https://example.com/mcp
+```
+
+If your host requires an already-installed command, install the package with your Python package manager:
+
+```bash
+uv tool install fastmcp-remote
+```
+
+## Host Configuration
+
+For hosts that use `mcpServers` JSON configuration, set the command to `uvx` and pass `fastmcp-remote` plus the remote server URL as arguments:
+
+```json
+{
+ "mcpServers": {
+ "remote-api": {
+ "command": "uvx",
+ "args": ["fastmcp-remote", "https://example.com/mcp"]
+ }
+ }
+}
+```
+
+## Endpoint URLs and Connection Status
+
+Pass the full MCP endpoint URL for the remote server. Many FastMCP HTTP servers expose MCP at `/mcp`, so a local development server may need `http://localhost:8000/mcp` rather than `http://localhost:8000`.
+
+`fastmcp-remote` starts a local stdio bridge, then connects to the upstream server when the MCP host initializes that bridge. If the upstream server is unavailable, the URL does not point to an MCP endpoint, or authentication cannot complete, initialization fails and the host should report the remote server as failed. After initialization succeeds, later tool, resource, prompt, and ping requests continue to proxy through the same remote server configuration.
+
+OAuth is enabled automatically unless you provide an `Authorization` header or pass `--auth none`. The first connection opens the browser-based OAuth flow when the server requires authentication, then stores tokens locally for future runs.
+
+To pass a bearer token or another custom header directly, provide `--header` in `Name: Value` form. The header name ends at the first colon, so values can contain additional colons. Quote the header when the value contains spaces, just like any other shell argument. An `Authorization` header disables OAuth by default:
+
+```json
+{
+ "mcpServers": {
+ "private-api": {
+ "command": "uvx",
+ "args": [
+ "fastmcp-remote",
+ "https://example.com/mcp",
+ "--header",
+ "Authorization: Bearer "
+ ]
+ }
+ }
+}
+```
+
+Repeat `--header` to send multiple headers:
+
+```bash
+uvx fastmcp-remote https://example.com/mcp \
+ --header "Authorization: Bearer " \
+ --header "X-Workspace: production" \
+ --header "X-Client-Name: My MCP Host" \
+ --header "X-Callback-Url: https://example.com/oauth/callback"
+```
+
+Some MCP hosts on Windows have trouble preserving spaces inside command arguments. Put the spaced value in an environment variable and reference it from the header value:
+
+```json
+{
+ "mcpServers": {
+ "remote-api": {
+ "command": "uvx",
+ "args": [
+ "fastmcp-remote",
+ "https://example.com/mcp",
+ "--header",
+ "Authorization:${AUTH_HEADER}"
+ ],
+ "env": {
+ "AUTH_HEADER": "Bearer "
+ }
+ }
+ }
+}
+```
+
+For local development servers over plain HTTP, disable OAuth when the server is unauthenticated:
+
+```bash
+uvx fastmcp-remote http://localhost:8000/mcp --auth none
+```
+
+## Self-Signed Certificates
+
+For servers behind a self-signed certificate, point `--verify` at a CA bundle that trusts the certificate:
+
+```bash
+uvx fastmcp-remote https://internal.example.com/mcp --verify /path/to/ca-bundle.pem
+```
+
+To disable certificate verification entirely, pass `--verify false`. This is insecure and should only be used for trusted servers on private networks:
+
+```bash
+uvx fastmcp-remote https://internal.example.com/mcp --verify false
+```
+
+To trust a CA bundle without a flag, set the standard `SSL_CERT_FILE` environment variable, which OpenSSL reads automatically:
+
+```bash
+SSL_CERT_FILE=/path/to/ca-bundle.pem uvx fastmcp-remote https://internal.example.com/mcp
+```
+
+## OAuth Storage
+
+OAuth tokens are stored under `~/.fastmcp/remote` by default. Set `FASTMCP_REMOTE_CONFIG_DIR` to use another directory:
+
+```bash
+FASTMCP_REMOTE_CONFIG_DIR=~/.config/fastmcp-remote uvx fastmcp-remote https://example.com/mcp
+```
+
+Use `--resource` to isolate tokens for a particular remote server identity:
+
+```bash
+uvx fastmcp-remote https://example.com/mcp --resource example-prod
+```
+
+If the remote authorization server requires a fixed callback port or hostname, pass them after the URL:
+
+```bash
+uvx fastmcp-remote https://example.com/mcp 3334 --host 127.0.0.1
+```
+
+## Options
+
+| Option | Description |
+| ------ | ----------- |
+| `--transport` | Choose `http` or `sse`. Defaults to `http`. |
+| `--header` | Add a header to upstream requests, for example `--header "Authorization: Bearer "`. Values may contain colons. Quote headers whose values contain spaces. Use `${VAR}` to expand environment variables inside values. Repeat for multiple headers. |
+| `--auth` | Choose `oauth` or `none`. The default uses OAuth unless an `Authorization` header is provided. |
+| `--verify` | Control TLS certificate verification. Pass a path to a CA bundle to trust a self-signed certificate, or `false` to disable verification (insecure). Defaults to verification enabled. |
+| `--resource` | Isolate OAuth token storage for a named remote resource. |
+| `--host` | Set the OAuth callback hostname. Defaults to `localhost`. |
+| `--auth-timeout` | Set how long to wait for the OAuth callback. Defaults to 300 seconds. |
+| `--ignore-tool` | Hide tools whose names match a glob pattern. Repeat for multiple patterns. |
+| `--debug` | Enable debug logging. |
+| `--silent` | Suppress non-critical logs. |
diff --git a/docs/clients/logging.mdx b/docs/clients/logging.mdx
index eea9ff322..407b1ebd5 100644
--- a/docs/clients/logging.mdx
+++ b/docs/clients/logging.mdx
@@ -28,12 +28,32 @@ logging.basicConfig(
)
logger = logging.getLogger(__name__)
-LOGGING_LEVEL_MAP = logging.getLevelNamesMapping()
+LOGGING_LEVEL_MAP = {
+ "DEBUG": logging.DEBUG,
+ "INFO": logging.INFO,
+ "NOTICE": logging.INFO,
+ "WARNING": logging.WARNING,
+ "ERROR": logging.ERROR,
+ "CRITICAL": logging.CRITICAL,
+ "ALERT": logging.CRITICAL,
+ "EMERGENCY": logging.CRITICAL,
+}
async def log_handler(message: LogMessage):
"""Forward MCP server logs to Python's logging system."""
- msg = message.data.get('msg')
- extra = message.data.get('extra')
+ data = message.data
+ if isinstance(data, dict):
+ msg = data.get('msg', data)
+ extra = data.get('extra')
+ else:
+ msg = data
+ extra = None
+
+ # Python's logging requires `extra` to be a mapping, but a server can send
+ # any JSON value, so fold anything else into the message instead.
+ if extra is not None and not isinstance(extra, dict):
+ msg = f"{msg} ({extra})"
+ extra = None
level = LOGGING_LEVEL_MAP.get(message.level.upper(), logging.INFO)
logger.log(level, msg, extra=extra)
@@ -55,19 +75,20 @@ The handler receives a `LogMessage` object:
The logger name (may be None)
-
- The log payload, containing `msg` and `extra` keys
+
+ The JSON-serializable log payload sent by the server. FastMCP's structured logger uses a dictionary with `msg` and `extra` keys, but other MCP servers may send any JSON value.
## Structured Logs
-The `message.data` attribute is a dictionary containing the log payload. This enables structured logging with rich contextual information.
+The `message.data` attribute contains the server's JSON-serializable log payload. FastMCP servers commonly send a dictionary with `msg` and `extra` keys, which enables structured logging with rich contextual information.
```python
async def detailed_log_handler(message: LogMessage):
- msg = message.data.get('msg')
- extra = message.data.get('extra')
+ data = message.data
+ msg = data.get('msg', data) if isinstance(data, dict) else data
+ extra = data.get('extra') if isinstance(data, dict) else None
if message.level == "error":
print(f"ERROR: {msg} | Details: {extra}")
diff --git a/docs/clients/notifications.mdx b/docs/clients/notifications.mdx
index 5e1b447aa..b771c903e 100644
--- a/docs/clients/notifications.mdx
+++ b/docs/clients/notifications.mdx
@@ -22,8 +22,8 @@ from fastmcp import Client
async def message_handler(message):
"""Handle MCP notifications from the server."""
- if hasattr(message, 'root'):
- method = message.root.method
+ if hasattr(message, 'method'):
+ method = message.method
if method == "notifications/tools/list_changed":
print("Tools have changed - refresh tool cache")
@@ -31,6 +31,8 @@ async def message_handler(message):
print("Resources have changed")
elif method == "notifications/prompts/list_changed":
print("Prompts have changed")
+ elif method == "notifications/resources/updated":
+ print("A resource was updated")
client = Client(
"my_mcp_server.py",
@@ -113,6 +115,18 @@ class MyMessageHandler(MessageHandler):
"""Called for progress updates during long-running operations."""
pass
+ async def on_resource_updated(
+ self, notification: mcp.types.ResourceUpdatedNotification
+ ) -> None:
+ """Called when a specific resource changes."""
+ pass
+
+ async def on_cancelled(
+ self, notification: mcp.types.CancelledNotification
+ ) -> None:
+ """Called when a request is cancelled."""
+ pass
+
async def on_logging_message(
self, notification: mcp.types.LoggingMessageNotification
) -> None:
diff --git a/docs/clients/prompts.mdx b/docs/clients/prompts.mdx
index bb50d475f..fcebfe967 100644
--- a/docs/clients/prompts.mdx
+++ b/docs/clients/prompts.mdx
@@ -21,7 +21,7 @@ Request a rendered prompt with `get_prompt()`:
async with client:
# Simple prompt without arguments
result = await client.get_prompt("welcome_message")
- # result -> mcp.types.GetPromptResult
+ # result -> mcp_types.GetPromptResult
# Access the generated messages
for message in result.messages:
@@ -128,12 +128,12 @@ See [Metadata](/servers/versioning#version-discovery) for how to discover availa
## Multi-Server Clients
-When using multi-server clients, prompts are accessible directly without prefixing:
+When using multi-server clients, prompts are mounted with the server name as a prefix, just like tools:
```python
async with client: # Multi-server client
- result1 = await client.get_prompt("weather_prompt", {"city": "London"})
- result2 = await client.get_prompt("assistant_prompt", {"query": "help"})
+ result1 = await client.get_prompt("weather_weather_prompt", {"city": "London"})
+ result2 = await client.get_prompt("assistant_assistant_prompt", {"query": "help"})
```
## Raw Protocol Access
@@ -143,5 +143,5 @@ For complete control, use `get_prompt_mcp()` which returns the full MCP protocol
```python
async with client:
result = await client.get_prompt_mcp("example_prompt", {"arg": "value"})
- # result -> mcp.types.GetPromptResult
+ # result -> mcp_types.GetPromptResult
```
diff --git a/docs/clients/resources.mdx b/docs/clients/resources.mdx
index a3e9300da..041ad0978 100644
--- a/docs/clients/resources.mdx
+++ b/docs/clients/resources.mdx
@@ -53,23 +53,30 @@ async with client:
for item in content:
if hasattr(item, 'text'):
print(f"Text content: {item.text}")
- print(f"MIME type: {item.mimeType}")
+ print(f"MIME type: {item.mime_type}")
```
Binary resources include images, PDFs, and other non-text data:
+Binary resources arrive as `BlobResourceContents`, whose `blob` field is a base64 **string**, so decode it before writing bytes to disk:
+
```python
+import base64
+
+from mcp_types import BlobResourceContents
+
async with client:
content = await client.read_resource("resource://images/logo.png")
for item in content:
- if hasattr(item, 'blob'):
- print(f"Binary content: {len(item.blob)} bytes")
- print(f"MIME type: {item.mimeType}")
+ if isinstance(item, BlobResourceContents):
+ data = base64.b64decode(item.blob)
+ print(f"Binary content: {len(data)} bytes")
+ print(f"MIME type: {item.mime_type}")
# Save to file
with open("downloaded_logo.png", "wb") as f:
- f.write(item.blob)
+ f.write(data)
```
## Multi-Server Clients
@@ -106,5 +113,5 @@ For complete control, use `read_resource_mcp()` which returns the full MCP proto
```python
async with client:
result = await client.read_resource_mcp("resource://example")
- # result -> mcp.types.ReadResourceResult
+ # result -> mcp_types.ReadResourceResult
```
diff --git a/docs/clients/roots.mdx b/docs/clients/roots.mdx
index 0370c119a..08f5c9786 100644
--- a/docs/clients/roots.mdx
+++ b/docs/clients/roots.mdx
@@ -1,7 +1,7 @@
---
title: Client Roots
sidebarTitle: Roots
-description: Provide local context and resource boundaries to MCP servers.
+description: Tell servers which local paths your client can reach.
icon: folder-tree
---
@@ -11,24 +11,26 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
Use this when you need to tell servers what local resources the client has access to.
-Roots inform servers about resources the client can provide. Servers can use this information to adjust behavior or provide more relevant responses.
+A root is a path your client is willing to expose — a project directory, a workspace, a document store. Servers read them to scope their work, so a tool that searches files searches where you pointed it, and a server that gets no roots has to ask the user for paths instead. Roots describe where the client can reach; the server takes them as its working boundary.
+
+Register them once with `roots=`, and the client answers however the server asks. A handshake-era server pushes a `roots/list` request down the open session and reads the reply mid-call; a modern (`2026-07-28`) server has no such channel, so it returns a roots request and `fastmcp.Client` fulfils it from the same registration and re-issues the call with the answer attached. The default `mode="auto"` negotiates whichever era the server speaks, so the examples below work on either — see [protocol negotiation](/clients/client#protocol-negotiation) for how that choice is made, and [the guard pattern](/servers/elicitation#sampling-and-roots) for how a server issues the modern form.
## Static Roots
-Provide a list of roots when creating the client:
+When the paths are known up front, pass them as a list. The client holds them for the life of the connection and hands back the same set every time a server asks.
```python
from fastmcp import Client
client = Client(
"my_mcp_server.py",
- roots=["/path/to/root1", "/path/to/root2"]
+ roots=["file:///path/to/root1", "file:///path/to/root2"]
)
```
## Dynamic Roots
-Use a callback to compute roots dynamically when the server requests them:
+Pass a callback instead when the roots depend on something the client learns at runtime, such as the workspace the user has open. It runs at the moment a server asks, on either route, and receives the request context so you can see which request it is answering:
```python
from fastmcp import Client
@@ -36,7 +38,7 @@ from fastmcp.client.roots import RequestContext
async def roots_callback(context: RequestContext) -> list[str]:
print(f"Server requested roots (Request ID: {context.request_id})")
- return ["/path/to/root1", "/path/to/root2"]
+ return ["file:///path/to/root1", "file:///path/to/root2"]
client = Client(
"my_mcp_server.py",
diff --git a/docs/clients/sampling.mdx b/docs/clients/sampling.mdx
index a2e0e9942..f2dfe82e3 100644
--- a/docs/clients/sampling.mdx
+++ b/docs/clients/sampling.mdx
@@ -1,7 +1,7 @@
---
title: LLM Sampling
sidebarTitle: Sampling
-description: Handle server-initiated LLM completion requests.
+description: Answer a server's request for an LLM completion.
icon: robot
---
@@ -9,52 +9,46 @@ import { VersionBadge } from "/snippets/version-badge.mdx";
-Use this when you need to respond to server requests for LLM completions.
+Use this when a server asks your client to run an LLM completion on its behalf.
-MCP servers can request LLM completions from clients during tool execution. This enables servers to delegate AI reasoning to the client, which controls which LLM is used and how requests are made.
+Sampling is how a server borrows your model. Rather than hold an API key of its own, the server describes the messages it wants completed and asks you to run them — you pick the model, and you pay for the tokens. Your side of that arrangement is one function, a **sampling handler**, registered when you create the client.
+
+The handler receives the conversation the server wants completed, the parameters it asked for, and a request context carrying metadata about the call. Return the generated text as a string and FastMCP wraps it in the protocol's result for you; return a `CreateMessageResult` yourself when you want to report the real model name or hand back content that isn't text. If the handler raises, the client sends the error back in place of a completion and the server's tool decides what to do about it.
## Handler Template
```python
from fastmcp import Client
from fastmcp.client.sampling import SamplingMessage, SamplingParams, RequestContext
+from mcp.types import TextContent
+
async def sampling_handler(
messages: list[SamplingMessage],
params: SamplingParams,
- context: RequestContext
+ context: RequestContext,
) -> str:
- """
- Handle server requests for LLM completions.
+ """Run the server's messages against your LLM and return the completion."""
+ conversation = [
+ f"{message.role}: {message.content.text}"
+ for message in messages
+ if isinstance(message.content, TextContent)
+ ]
+ system_prompt = params.system_prompt or "You are a helpful assistant."
- Args:
- messages: Conversation messages to send to the LLM
- params: Sampling parameters (temperature, max_tokens, etc.)
- context: Request context with metadata
-
- Returns:
- Generated text response from your LLM
- """
- # Extract message content
- conversation = []
- for message in messages:
- content = message.content.text if hasattr(message.content, 'text') else str(message.content)
- conversation.append(f"{message.role}: {content}")
-
- # Use the system prompt if provided
- system_prompt = params.systemPrompt or "You are a helpful assistant."
-
- # Integrate with your LLM service here
+ # Call your LLM here with `conversation` and `system_prompt`.
return "Generated response based on the messages"
-client = Client(
- "my_mcp_server.py",
- sampling_handler=sampling_handler,
-)
+
+client = Client("my_mcp_server.py", sampling_handler=sampling_handler)
```
+The client answers with this handler however the server asks for a completion. The default `mode="auto"` negotiates whichever protocol era the server speaks, and one handler covers both of the routes an era can use — see [Request Routes](#request-routes).
+
## Handler Parameters
+Everything the server sends arrives in the first two arguments. The messages are the conversation to complete; the parameters are how the server would like it completed. You decide how much of that to honor, since the client owns the model — a preference your provider cannot express is yours to ignore.
+
The role of the message
@@ -66,11 +60,11 @@ client = Client(
-
+
Optional system prompt the server wants to use
-
+
Server preferences for model selection (hints, cost/speed/intelligence priorities)
@@ -78,11 +72,11 @@ client = Client(
Sampling temperature
-
+
Maximum tokens to generate
-
+
Stop sequences for sampling
@@ -90,14 +84,14 @@ client = Client(
Tools the LLM can use during sampling
-
+
Tool usage behavior (`auto`, `required`, or `none`)
## Built-in Handlers
-FastMCP provides built-in handlers for OpenAI, Anthropic, and Google Gemini APIs that support the full sampling API including tool use.
+Writing the provider call yourself is rarely worth it. FastMCP ships handlers for OpenAI, Anthropic, and Google Gemini that implement the full sampling API, tool use included, and translate the protocol's parameters into each provider's own. Give one a default model and pass it where your own handler would go. Write a custom handler when you need routing across providers, caching, or a provider FastMCP does not cover.
### OpenAI Handler
@@ -113,9 +107,11 @@ client = Client(
)
```
-For OpenAI-compatible APIs (like local models):
+Point the handler at any OpenAI-compatible API, including a local model server, by passing your own provider client:
```python
+from fastmcp import Client
+from fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler
from openai import AsyncOpenAI
client = Client(
@@ -128,7 +124,7 @@ client = Client(
```
-Install the OpenAI handler with `pip install fastmcp[openai]`.
+Install the OpenAI handler with `pip install 'fastmcp[openai]'`.
### Anthropic Handler
@@ -146,7 +142,7 @@ client = Client(
```
-Install the Anthropic handler with `pip install fastmcp[anthropic]`.
+Install the Anthropic handler with `pip install 'fastmcp[anthropic]'`.
### Google Gemini Handler
@@ -155,36 +151,44 @@ Install the Anthropic handler with `pip install fastmcp[anthropic]`.
```python
from fastmcp import Client
-from fastmcp.client.sampling.handlers.google_genai import GoogleGenAISamplingHandler
+from fastmcp.client.sampling.handlers.google_genai import GoogleGenaiSamplingHandler
client = Client(
"my_mcp_server.py",
- sampling_handler=GoogleGenAISamplingHandler(default_model="gemini-2.0-flash"),
+ sampling_handler=GoogleGenaiSamplingHandler(default_model="gemini-2.0-flash"),
)
```
-Install the Google Gemini handler with `pip install fastmcp[gemini]`.
+Install the Google Gemini handler with `pip install 'fastmcp[gemini]'`.
-## Sampling Capabilities
+The [source of these handlers](https://github.com/PrefectHQ/fastmcp/tree/main/fastmcp_slim/fastmcp/client/sampling/handlers) is the best reference for writing your own.
-When you provide a `sampling_handler`, FastMCP automatically advertises full sampling capabilities to the server, including tool support. To disable tool support for simpler handlers:
+## Tool Use
+
+A sampling request can carry tools. When it does, your handler passes them to the model and returns whatever comes back, tool calls included — the server executes the tools itself and sends a follow-up sampling request with the results if it needs another turn. Your handler never runs a tool.
+
+Registering any `sampling_handler` advertises full sampling support, tools included. A handler that only generates text should say so, so servers know not to send tools it will drop:
```python
+from fastmcp import Client
from mcp.types import SamplingCapability
+
+async def text_only_handler(messages, params, context) -> str:
+ return "Generated response based on the messages"
+
+
client = Client(
"my_mcp_server.py",
- sampling_handler=basic_handler,
- sampling_capabilities=SamplingCapability(), # No tool support
+ sampling_handler=text_only_handler,
+ sampling_capabilities=SamplingCapability(),
)
```
-## Tool Execution
+## Request Routes
-Tool execution happens on the server side. The client's role is to pass tools to the LLM and return the LLM's response (which may include tool use requests). The server then executes the tools and may send follow-up sampling requests with tool results.
+Servers reach your handler by two routes, and which one applies depends on the protocol era the connection negotiated. A handshake-era server pushes a `sampling/createMessage` request down the open session while a tool is running and waits for the reply. A modern (`2026-07-28`) connection has no such channel, so the tool ends its round by returning a request for a completion instead; the client answers from your handler and calls the tool again with the result attached.
-
-To implement a custom sampling handler, see the [handler source code](https://github.com/PrefectHQ/fastmcp/tree/main/src/fastmcp/client/sampling/handlers) as a reference.
-
+One registration covers both, so this is rarely something you configure — it matters only when you pin an era, since `mode="legacy"` is the sole route that carries a pushed request. See [protocol negotiation](/clients/client#protocol-negotiation) for how the era is chosen, and [Sampling](/servers/sampling) under Servers for how a server issues these requests.
diff --git a/docs/clients/tasks.mdx b/docs/clients/tasks.mdx
index ce27520e4..71a1384c9 100644
--- a/docs/clients/tasks.mdx
+++ b/docs/clients/tasks.mdx
@@ -1,180 +1,138 @@
---
title: Background Tasks
sidebarTitle: Tasks
-description: Execute operations asynchronously and track their progress.
+description: Call long-running tools without blocking, and answer questions they ask mid-run.
icon: clock
tag: "NEW"
---
import { VersionBadge } from "/snippets/version-badge.mdx"
-
+
-Use this when you need to run long operations asynchronously while doing other work.
+Some tool calls take a while. The MCP background tasks extension lets a server run one in the background instead of holding the request open, and FastMCP's client drives the whole thing for you — most of the time you don't need to know a call was tasked at all.
-The MCP task protocol lets you request operations to run in the background. The call returns a Task object immediately, letting you track progress, cancel operations, or await results.
+
+**Client task support is opt-in.** Install the `fastmcp-tasks` package (`pip install "fastmcp[tasks]"`) and import it — importing `fastmcp_tasks` anywhere (which you do to use `call_tool_task`) enables task support for every `Client` in the process. Without it, a `Client` never advertises the tasks capability, so the server runs its calls synchronously and background tasks simply don't happen.
-## Requesting Background Execution
+**Tasks also require the modern protocol.** The capability is negotiated over `2026-07-28` connections. `mode="auto"` (the client default) negotiates it automatically; `mode="legacy"` never does. See [protocol negotiation](/clients/client#protocol-negotiation).
+
-Pass `task=True` to run an operation as a background task:
+## Transparent Calls
+
+With task support enabled, just call the tool. If the server runs it as a background task, `call_tool` polls it to completion under the hood and returns the same result you'd get from a synchronous call — the task is invisible.
+
+```python
+import fastmcp_tasks # enables client task support
+from fastmcp import Client
+
+async with Client(server, mode="auto") as client:
+ result = await client.call_tool("slow_computation", {"duration": 10})
+ print(result.data)
+```
+
+This is the right default for most code: it works whether or not the server actually tasks the call, so you can write ordinary tool-calling code without checking server capabilities.
+
+## Driving a Task Explicitly
+
+When you want to do other work while a task runs — or check on it, or cancel it — use `call_tool_task` instead. It returns a `ToolTask` handle immediately rather than waiting for completion.
```python
from fastmcp import Client
+from fastmcp_tasks import call_tool_task
-async with Client(server) as client:
- # Start a background task
- task = await client.call_tool("slow_computation", {"duration": 10}, task=True)
-
+async with Client(server, mode="auto") as client:
+ task = await call_tool_task(client, "slow_computation", {"duration": 10})
print(f"Task started: {task.task_id}")
# Do other work while it runs...
- # Get the result when ready
result = await task.result()
```
-This works with tools, resources, and prompts:
-
-```python
-tool_task = await client.call_tool("my_tool", args, task=True)
-resource_task = await client.read_resource("file://large.txt", task=True)
-prompt_task = await client.get_prompt("my_prompt", args, task=True)
-```
-
-## Task API
-
-All task types share a common interface.
-
-### Getting Results
-
-Call `await task.result()` or simply `await task` to block until the task completes:
-
-```python
-task = await client.call_tool("analyze", {"text": "hello"}, task=True)
-
-# Wait for result (blocking)
-result = await task.result()
-# or: result = await task
-```
+`call_tool_task` requires the server to actually run the call as a task — if the tool isn't `task=True`, or the server doesn't have the tasks extension registered, it raises `ToolError`. Use it when you specifically need the handle; use `call_tool` when you just want the result.
### Checking Status
-Check the current status without blocking:
-
```python
status = await task.status()
-print(f"{status.status}: {status.statusMessage}")
-# status.status is "working", "completed", "failed", or "cancelled"
+print(f"{status.status}: {status.status_message}")
+# status.status is "working", "input_required", "completed", "failed", or "cancelled"
```
### Waiting with Control
-Use `task.wait()` for more control over waiting:
+`task.wait()` polls until a terminal state (or a specific one you name), without answering any input the task asks for — use it when you want to observe an `input_required` pause yourself rather than have it answered automatically.
```python
# Wait up to 30 seconds for completion
status = await task.wait(timeout=30.0)
# Wait for a specific state
-status = await task.wait(state="completed", timeout=30.0)
+status = await task.wait(state="input_required", timeout=30.0)
```
-### Cancellation
+### Getting the Result
-Cancel a running task:
+`task.result()` drives the task the rest of the way — including answering any input it asks for — and returns the finished result, same as `client.call_tool` would. Awaiting the task directly is shorthand for this.
+
+```python
+result = await task.result()
+# or: result = await task
+```
+
+By default a failed or cancelled task raises `ToolError`. Pass `raise_on_error=False` to `call_tool_task` to get an error result back instead.
+
+### Cancellation
```python
await task.cancel()
```
-## Status Updates
+Cancellation is cooperative — the task may still finish before the server notices the request.
-Register callbacks to receive real-time status updates as the server reports progress:
+## Answering Questions Mid-Task
-```python
-def on_status_change(status):
- print(f"Task {status.taskId}: {status.status} - {status.statusMessage}")
-
-task.on_status_change(on_status_change)
-
-# Async callbacks work too
-async def on_status_async(status):
- await log_status(status)
-
-task.on_status_change(on_status_async)
-```
-
-### Handler Template
+A task can pause partway through to ask a question, the same way a foreground [multi-round-trip](/clients/elicitation#input-required-rounds) tool does. Pass an `elicitation_handler` and both `call_tool` and `task.result()` answer it automatically as part of driving the task to completion:
```python
from fastmcp import Client
-def status_handler(status):
- """
- Handle task status updates.
+async def handle_elicitation(message, response_type, params, context):
+ return {"cuisine": "Thai", "vegetarian": True}
- Args:
- status: Task status object with:
- - taskId: Unique task identifier
- - status: "working", "completed", "failed", or "cancelled"
- - statusMessage: Optional progress message from server
- """
- if status.status == "working":
- print(f"Progress: {status.statusMessage}")
- elif status.status == "completed":
- print("Task completed")
- elif status.status == "failed":
- print(f"Task failed: {status.statusMessage}")
-
-task.on_status_change(status_handler)
+async with Client(server, mode="auto", elicitation_handler=handle_elicitation) as client:
+ result = await client.call_tool("plan_dinner", {})
+ print(result.data)
```
-## Graceful Degradation
-
-You can always pass `task=True` regardless of whether the server supports background tasks. Per the MCP specification, servers without task support execute the operation immediately and return the result inline.
-
-```python
-task = await client.call_tool("my_tool", args, task=True)
-
-if task.returned_immediately:
- print("Server executed immediately (no background support)")
-else:
- print("Running in background")
-
-# Either way, this works
-result = await task.result()
-```
-
-This lets you write task-aware client code without worrying about server capabilities.
+Without an `elicitation_handler`, a task that asks for input raises `ToolError` rather than hanging. See [server-side background tasks](/servers/tasks#gathering-input-mid-task) for how a tool asks a question in the first place.
## Example
+Putting it together, here is a client that submits a background task with `call_tool_task` and awaits its result:
+
```python
import asyncio
from fastmcp import Client
+from fastmcp_tasks import call_tool_task
async def main():
- async with Client(server) as client:
- # Start background task
- task = await client.call_tool(
- "slow_computation",
- {"duration": 10},
- task=True,
- )
+ async with Client(server, mode="auto") as client:
+ # Return immediately and drive the task yourself
+ task = await call_tool_task(client, "slow_computation", {"duration": 10})
+ print(f"Task started: {task.task_id}")
- # Subscribe to updates
- def on_update(status):
- print(f"Progress: {status.statusMessage}")
+ # Do other work while the task runs
+ while True:
+ status = await task.status()
+ if status.status in ("completed", "failed", "cancelled"):
+ break
+ print(f"Still working... ({status.status})")
+ await asyncio.sleep(1)
- task.on_status_change(on_update)
-
- # Do other work while task runs
- print("Doing other work...")
- await asyncio.sleep(2)
-
- # Wait for completion and get result
result = await task.result()
- print(f"Result: {result.content}")
+ print(f"Result: {result.data}")
asyncio.run(main())
```
diff --git a/docs/clients/tools.mdx b/docs/clients/tools.mdx
index 1541f593e..77389296e 100644
--- a/docs/clients/tools.mdx
+++ b/docs/clients/tools.mdx
@@ -80,7 +80,7 @@ async with client:
Fully hydrated Python objects with complex type support (datetimes, UUIDs, custom classes). FastMCP exclusive.
-
+
Standard MCP content blocks (`TextContent`, `ImageContent`, `AudioContent`, etc.).
@@ -173,9 +173,9 @@ For complete control, use `call_tool_mcp()` which returns the raw MCP protocol o
```python
async with client:
result = await client.call_tool_mcp("my_tool", {"param": "value"})
- # result -> mcp.types.CallToolResult
+ # result -> mcp_types.CallToolResult
- if result.isError:
+ if result.is_error:
print(f"Tool failed: {result.content}")
else:
print(f"Tool succeeded: {result.content}")
diff --git a/docs/clients/transports.mdx b/docs/clients/transports.mdx
index 7e3eb03db..23acab535 100644
--- a/docs/clients/transports.mdx
+++ b/docs/clients/transports.mdx
@@ -16,7 +16,9 @@ Transports handle the underlying connection between your client and MCP servers.
STDIO transport communicates with MCP servers through subprocess pipes. When using STDIO, your client launches and manages the server process, controlling its lifecycle and environment.
-STDIO servers run in isolated environments by default. They do not inherit your shell's environment variables. You must explicitly pass any configuration the server needs.
+STDIO servers inherit only a small allowlist of environment variables — just enough to locate an interpreter and a home directory. Anything else in your shell, including API keys and other credentials, does not reach the server unless you pass it through `env` explicitly.
+
+The allowlist is platform-specific. On POSIX systems it is `HOME`, `LOGNAME`, `PATH`, `SHELL`, `TERM`, and `USER`; on Windows it is `APPDATA`, `HOMEDRIVE`, `HOMEPATH`, `LOCALAPPDATA`, `PATH`, `PATHEXT`, `PROCESSOR_ARCHITECTURE`, `SYSTEMDRIVE`, `SYSTEMROOT`, `TEMP`, `USERNAME`, and `USERPROFILE`.
```python
@@ -42,7 +44,7 @@ client = Client("my_server.py") # Limited - no configuration options
### Environment Variables
-Since STDIO servers do not inherit your environment, you need strategies for passing configuration.
+Values you pass through `env` are merged on top of the inherited allowlist, so you add configuration rather than replacing the base environment. Anything your server needs beyond those six variables has to be listed explicitly.
**Selective forwarding** passes only the variables your server needs:
@@ -63,7 +65,11 @@ client = Client(transport)
from dotenv import dotenv_values
from fastmcp.client.transports import StdioTransport
-env = dotenv_values(".env")
+env = {
+ key: value
+ for key, value in dotenv_values(".env").items()
+ if value is not None
+}
transport = StdioTransport(command="python", args=["server.py"], env=env)
client = Client(transport)
```
@@ -80,7 +86,7 @@ client = Client(transport)
async def efficient_multiple_operations():
async with client:
- await client.ping()
+ await client.list_tools()
async with client: # Reuses the same subprocess
await client.call_tool("process_data", {"file": "data.csv"})
@@ -124,6 +130,38 @@ client = Client(
)
```
+### SSL Verification
+
+By default, HTTPS connections verify the server's SSL certificate. You can customize this behavior with the `verify` parameter, which accepts the same values as httpx2 (documented in [httpx's SSL guide](https://www.python-httpx.org/advanced/ssl/), which httpx2 follows):
+
+```python
+from fastmcp import Client
+
+# Disable SSL verification (e.g., for self-signed certs in development)
+client = Client("https://dev-server.internal/mcp", verify=False)
+
+# Use a custom CA bundle
+client = Client("https://corp-server.internal/mcp", verify="/path/to/ca-bundle.pem")
+
+# Use a custom SSL context for full control
+import ssl
+ctx = ssl.create_default_context()
+ctx.load_verify_locations("/path/to/internal-ca.pem")
+client = Client("https://corp-server.internal/mcp", verify=ctx)
+```
+
+The `verify` parameter is also available directly on `StreamableHttpTransport` and `SSETransport`:
+
+```python
+from fastmcp.client.transports import StreamableHttpTransport
+
+transport = StreamableHttpTransport(
+ url="https://dev-server.internal/mcp",
+ verify=False,
+)
+client = Client(transport)
+```
+
### SSE Transport
Server-Sent Events transport is maintained for backward compatibility. Use Streamable HTTP for new deployments unless you have specific infrastructure requirements.
diff --git a/docs/css/banner.css b/docs/css/banner.css
index f9c6384b2..036437304 100644
--- a/docs/css/banner.css
+++ b/docs/css/banner.css
@@ -1,7 +1,9 @@
-/* Banner styling -- improve readability with better contrast */
+/* Banner: an animated brand-rainbow wash behind Mintlify's white text.
+ Mintlify always renders banner text white, so every gradient stop is a
+ deep, saturated shade (all >=7:1 on white) — the colors evoke the FastMCP
+ watercolor logo while keeping the announcement legible in both themes.
+ A dark fallback color is configured in docs.json for the no-CSS case. */
#banner {
- background: #f1f5f9 !important;
- color: #1e293b !important;
font-size: 0.95rem !important;
font-weight: 600 !important;
padding-top: 12px !important;
@@ -12,58 +14,41 @@
#banner::before {
content: "";
position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
+ inset: 0;
+ z-index: 0;
background: linear-gradient(
90deg,
- rgba(6, 182, 212, 0.25) 0%,
- rgba(6, 182, 212, 0.05) 25%,
- rgba(6, 182, 212, 0.35) 50%,
- rgba(6, 182, 212, 0.08) 75%,
- rgba(6, 182, 212, 0.28) 100%
+ #1e40af 0%,
+ #5b21b6 22%,
+ #115e59 44%,
+ #9a3412 66%,
+ #9d174d 88%,
+ #1e40af 100%
);
- background-size: 300% 100%;
- animation: colorWave 14s ease-in-out infinite alternate;
+ background-size: 250% 100%;
+ animation: colorWave 18s ease-in-out infinite alternate;
pointer-events: none;
}
-.dark #banner {
- background: #475569 !important;
- color: #f1f5f9 !important;
-}
-
-.dark #banner::before {
- background: linear-gradient(
- 90deg,
- rgba(247, 37, 133, 0.35) 0%,
- rgba(247, 37, 133, 0.08) 25%,
- rgba(247, 37, 133, 0.45) 50%,
- rgba(247, 37, 133, 0.12) 75%,
- rgba(247, 37, 133, 0.38) 100%
- );
- background-size: 300% 100%;
+/* Keep the announcement text above the animated wash. */
+#banner > * {
+ position: relative;
+ z-index: 1;
}
@keyframes colorWave {
0% {
- background-position: 0% 0%;
+ background-position: 0% 50%;
}
100% {
- background-position: 100% 0%;
+ background-position: 100% 50%;
}
}
#banner * {
- color: #1e293b !important;
margin: 0 !important;
}
-.dark #banner * {
- color: #f1f5f9 !important;
-}
-
@media (max-width: 767px) {
#banner {
font-size: 0.8rem !important;
@@ -71,4 +56,3 @@
padding-bottom: 8px !important;
}
}
-
diff --git a/docs/css/language-dropdown.css b/docs/css/language-dropdown.css
new file mode 100644
index 000000000..0eb810545
--- /dev/null
+++ b/docs/css/language-dropdown.css
@@ -0,0 +1,57 @@
+/* Language dropdown: injected by language-dropdown.js into the sidebar
+ footer, to the right of Mintlify's theme selector. Mirrors the almond
+ theme pill's exact metrics (lg:h-7 desktop / 2.375rem mobile, rounded-full,
+ border-gray-200/70, dark:border-white/[0.07]) so the two controls read as
+ one family. */
+#language-switch {
+ margin-left: auto;
+ display: inline-flex;
+ align-items: center;
+}
+
+#language-switch select {
+ appearance: none;
+ -webkit-appearance: none;
+ background-color: transparent;
+ border: 1px solid rgb(229 231 235 / 0.7);
+ border-radius: 9999px;
+ color: rgb(107 114 128);
+ cursor: pointer;
+ font-size: 0.75rem;
+ line-height: 1rem;
+ height: 2.375rem;
+ padding: 0 1.375rem 0 0.75rem;
+ /* Chevron, drawn in the same gray as the label text. */
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right 0.5rem center;
+ background-size: 0.7rem;
+ transition: border-color 0.2s;
+}
+
+@media (min-width: 1024px) {
+ #language-switch select {
+ height: 1.75rem;
+ }
+}
+
+#language-switch select:hover {
+ color: rgb(75 85 99);
+ border-color: rgb(229 231 235);
+}
+
+#language-switch select:focus-visible {
+ outline: 2px solid rgb(45 0 247 / 0.4);
+ outline-offset: 1px;
+}
+
+.dark #language-switch select {
+ border-color: rgb(255 255 255 / 0.07);
+ color: rgb(156 163 175);
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
+}
+
+.dark #language-switch select:hover {
+ color: rgb(209 213 219);
+ border-color: rgb(255 255 255 / 0.1);
+}
diff --git a/docs/css/style.css b/docs/css/style.css
index 99844f692..94d58b4bf 100644
--- a/docs/css/style.css
+++ b/docs/css/style.css
@@ -57,6 +57,42 @@ h6 code:not(pre code) {
background: linear-gradient(135deg, #2d00f7 0%, #4cc9f0 100%);
}
+/* V3 banner - inside content-container, breaks out of padding with negative margins */
+#v3-banner {
+ display: block;
+ background: linear-gradient(135deg, #4cc9f0 0%, #2d00f7 100%);
+ color: white;
+ text-align: center;
+ padding: 10px 16px;
+ font-size: 0.875rem;
+ font-weight: 600;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ margin: -2rem -2rem 1.5rem -2rem;
+ width: calc(100% + 4rem);
+ border-radius: 8px 8px 0 0;
+}
+
+#v3-banner a {
+ color: white;
+ text-decoration: underline;
+ font-weight: 700;
+}
+
+#v3-banner a:hover {
+ opacity: 0.9;
+}
+
+@media (min-width: 1024px) {
+ #v3-banner {
+ margin: -3rem -4rem 1.5rem -4rem;
+ width: calc(100% + 8rem);
+ }
+}
+
+.dark #v3-banner {
+ background: linear-gradient(135deg, #2d00f7 0%, #4cc9f0 100%);
+}
+
diff --git a/docs/deployment/http.mdx b/docs/deployment/http.mdx
index e961de9d1..f057efe4c 100644
--- a/docs/deployment/http.mdx
+++ b/docs/deployment/http.mdx
@@ -79,17 +79,17 @@ The ASGI approach shines in production environments where you need reliability a
### Custom Path
-By default, your MCP server is accessible at `/mcp/` on your domain. You can customize this path to fit your URL structure or avoid conflicts with existing endpoints. This is particularly useful when integrating MCP into an existing application or following specific API conventions.
+By default, your MCP server is accessible at `/mcp` on your domain. You can customize this path to fit your URL structure or avoid conflicts with existing endpoints. This is particularly useful when integrating MCP into an existing application or following specific API conventions.
```python
# Option 1: With mcp.run()
-mcp.run(transport="http", host="0.0.0.0", port=8000, path="/api/mcp/")
+mcp.run(transport="http", host="0.0.0.0", port=8000, path="/api/mcp")
# Option 2: With ASGI app
-app = mcp.http_app(path="/api/mcp/")
+app = mcp.http_app(path="/api/mcp")
```
-Now your server is accessible at `http://localhost:8000/api/mcp/`.
+Now your server is accessible at `http://localhost:8000/api/mcp`.
### Authentication
@@ -101,6 +101,96 @@ FastMCP supports multiple authentication methods to secure your remote server. S
If you're mounting an authenticated server under a path prefix, see [Mounting Authenticated Servers](#mounting-authenticated-servers) below for important routing considerations.
+### Host and Origin Protection
+
+FastMCP can validate `Host` and browser `Origin` headers for Streamable HTTP requests before they reach MCP session handling. This request guard protects localhost-bound servers from DNS rebinding attacks, and it stays opt-in to preserve compatibility with existing ASGI, serverless, and reverse-proxy deployments.
+
+Think of this as a request guard rather than CORS middleware. It decides whether a request can reach MCP session handling. CORS remains a separate browser response-header policy; configure CORS middleware separately when browser JavaScript must read cross-origin responses.
+
+Enable strict validation with `host_origin_protection=True`. When you deploy behind a public hostname, add the hostname clients use to reach your MCP endpoint. If a browser-based MCP client runs on a separate origin, add that origin as well:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("My Server")
+
+app = mcp.http_app(
+ host_origin_protection=True,
+ allowed_hosts=["mcp.example.com"],
+ allowed_origins=["https://app.example.com"],
+)
+```
+
+For the direct server approach, pass the same values to `run()`:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("My Server")
+
+if __name__ == "__main__":
+ mcp.run(
+ transport="http",
+ host="0.0.0.0",
+ port=8000,
+ host_origin_protection=True,
+ allowed_hosts=["mcp.example.com"],
+ allowed_origins=["https://app.example.com"],
+ )
+```
+
+You can also configure these values with environment variables:
+
+```bash
+export FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=true
+export FASTMCP_HTTP_ALLOWED_HOSTS='["mcp.example.com"]'
+export FASTMCP_HTTP_ALLOWED_ORIGINS='["https://app.example.com"]'
+```
+
+Use `host_origin_protection="auto"` to protect localhost-bound direct servers while allowing ASGI, serverless, and reverse-proxy deployments to keep their existing Host handling unless they configure explicit trust rules. Use `host_origin_protection=False` to keep the request guard disabled.
+
+### Gateway Routing Headers
+
+
+
+A gateway, load balancer, or reverse proxy in front of your MCP server often needs to route a request before it reads the JSON-RPC body — the body may be an SSE stream, or the gateway may simply want to avoid parsing it. On a connection that negotiates the modern `2026-07-28` protocol, Streamable HTTP clients built on the MCP Python SDK (including FastMCP's own client) attach routing information to each request as HTTP headers so an intermediary can dispatch on headers alone:
+
+- `Mcp-Method` carries the JSON-RPC method (for example `tools/call`) on every request.
+- `Mcp-Name` carries the target's name on named operations — the tool name for `tools/call`, the prompt name for `prompts/get`, the resource URI for `resources/read`.
+- `Mcp-Param-*` carries selected argument values for a `tools/call`, one header per opted-in parameter.
+
+FastMCP's HTTP transport neither strips nor rewrites these headers, so a gateway sees them exactly as the client sent them. The `Host`/`Origin` request guard inspects only `Host` and `Origin` and leaves the routing headers untouched.
+
+
+These headers are a feature of the modern `2026-07-28` protocol. A client connected over an earlier protocol revision — including one running in legacy mode or one that has fallen back to a legacy server — sends no routing headers at all. Design gateway routing to require the headers rather than assume their presence: if a request arrives without them, fall back to inspecting the body or route it to a default backend, rather than dropping it.
+
+
+To expose an argument as an `Mcp-Param-*` header, annotate the parameter with the `x-mcp-header` JSON Schema extension. FastMCP carries the annotation into the tool's advertised input schema, and a conforming client mirrors the argument into a header named `Mcp-Param-`:
+
+```python
+from typing import Annotated
+
+from pydantic import Field
+
+from fastmcp import FastMCP
+
+mcp = FastMCP("My Server")
+
+@mcp.tool
+def query_tenant(
+ tenant: Annotated[str, Field(json_schema_extra={"x-mcp-header": "Tenant"})],
+ sql: str,
+) -> str:
+ """A call to this tool sends the tenant value as an `Mcp-Param-Tenant` header."""
+ ...
+```
+
+A gateway can now route on `Mcp-Param-Tenant` — for example, pinning each tenant to a dedicated backend — without inspecting the request body. The annotation is only permitted on `string`, `integer`, and `boolean` parameters. These headers advertise routing intent; treat them as untrusted hints, since the server still validates the request body as the source of truth.
+
+
+When you put a FastMCP [proxy](/servers/providers/proxy) in front of another server, the proxy re-advertises each backend tool's `x-mcp-header` annotation, so routing headers work across the proxy hop as well. The headers themselves are regenerated per hop rather than forwarded verbatim, since each describes a single HTTP request.
+
+
### Health Checks
Health check endpoints are essential for monitoring your deployed server and ensuring it's responding correctly. FastMCP allows you to add custom routes alongside your MCP endpoints, making it easy to implement health checks that work with both deployment approaches.
@@ -115,6 +205,10 @@ async def health_check(request):
This health endpoint will be available at `http://localhost:8000/health` and can be used by load balancers, monitoring systems, or deployment platforms to verify your server is running.
+
+Custom routes are never protected by the server's authentication middleware, even when an `AuthProvider` is configured. This is by design — the primary use case for custom routes is unauthenticated operational endpoints like health checks and readiness probes. If you need authenticated HTTP endpoints alongside your MCP server, [mount it in a FastAPI app](/integrations/fastapi) and use FastAPI's `Depends()` for auth on your routes.
+
+
### Custom Middleware
@@ -152,6 +246,8 @@ Most MCP clients, including those that you access through a browser like ChatGPT
CORS (Cross-Origin Resource Sharing) is needed when JavaScript running in a web browser connects directly to your MCP server. This is different from using an LLM through a browser—in that case, the browser connects to the LLM service, and the LLM service connects to your MCP server (no CORS needed).
+Host and Origin protection runs before CORS when it is active for a request. Add browser client origins to `allowed_origins` so trusted browser requests reach the CORS middleware, then configure CORS to let browser JavaScript read the MCP response headers it needs. Setting `allowed_origins` trusts the request; it does not emit `Access-Control-Allow-Origin` or other CORS response headers.
+
Browser-based MCP clients that need CORS include:
- **MCP Inspector** - Browser-based debugging tool for testing MCP servers
@@ -291,7 +387,7 @@ def analyze(data: str) -> dict:
return {"result": f"Analyzed: {data}"}
# Create the ASGI app
-mcp_app = mcp.http_app(path='/mcp')
+mcp_app = mcp.http_app(path="/mcp")
# Create a Starlette app and mount the MCP server
app = Starlette(
@@ -303,7 +399,7 @@ app = Starlette(
)
```
-The MCP endpoint will be available at `/mcp-server/mcp/` of the resulting Starlette app.
+The MCP endpoint will be available at `/mcp-server/mcp` of the resulting Starlette app.
For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the resulting Starlette app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized.
@@ -322,7 +418,7 @@ from starlette.routing import Mount
mcp = FastMCP("MyServer")
# Create the ASGI app
-mcp_app = mcp.http_app(path='/mcp')
+mcp_app = mcp.http_app(path="/mcp")
# Create nested application structure
inner_app = Starlette(routes=[Mount("/inner", app=mcp_app)])
@@ -332,7 +428,7 @@ app = Starlette(
)
```
-In this setup, the MCP server is accessible at the `/outer/inner/mcp/` path.
+In this setup, the MCP server is accessible at the `/outer/inner/mcp` path.
### FastAPI Integration
@@ -450,7 +546,7 @@ base_url="http://localhost:8000/api" # Includes mount prefix
mcp_path="/mcp" # Internal MCP path, NOT the mount prefix
```
-**`issuer_url`** (optional) controls the authorization server identity for OAuth discovery. Defaults to `base_url`.
+**`issuer_url`** (optional) controls the authorization server identity for OAuth discovery. Defaults to `base_url`. It sets the `issuer` advertised in the authorization server metadata and the `iss` on issued tokens, while the endpoints in that metadata continue to point at `base_url`.
```python
# Usually not needed - just set base_url and it works
@@ -580,7 +676,7 @@ if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
```
-For more details on OAuth authentication, see the [Authentication guide](/servers/auth).
+For more details on OAuth authentication, see the [Authentication guide](/servers/auth/authentication).
## Production Deployment
@@ -604,7 +700,7 @@ When deploying FastMCP behind a load balancer or running multiple server instanc
#### Understanding Sessions
-By default, FastMCP's Streamable HTTP transport maintains server-side sessions. Sessions enable stateful MCP features like [elicitation](/servers/elicitation) and [sampling](/servers/sampling), where the server needs to maintain context across multiple requests from the same client.
+By default, FastMCP's Streamable HTTP transport maintains server-side sessions. A session holds the context a server keeps across multiple requests from the same client, and it carries the handshake-era back-channel that server-initiated requests like [elicitation](/servers/elicitation) push down.
This works perfectly for single-instance deployments. However, sessions are stored in memory on each server instance, which creates challenges when scaling horizontally.
@@ -625,18 +721,18 @@ You might expect sticky sessions (session affinity) to solve this, but they don'
For horizontally scaled deployments, enable stateless HTTP mode. In stateless mode, each request creates a fresh transport context, eliminating the need for session affinity entirely.
-**Option 1: Via constructor**
+**Option 1: Via `http_app()`**
```python
from fastmcp import FastMCP
-mcp = FastMCP("My Server", stateless_http=True)
+mcp = FastMCP("My Server")
@mcp.tool
def process(data: str) -> str:
return f"Processed: {data}"
-app = mcp.http_app()
+app = mcp.http_app(stateless_http=True)
```
**Option 2: Via `run()`**
@@ -656,17 +752,17 @@ FASTMCP_STATELESS_HTTP=true uvicorn app:app --host 0.0.0.0 --port 8000 --workers
Production deployments should never hardcode sensitive information like API keys or authentication tokens. Instead, use environment variables to configure your server at runtime. This keeps your code secure and makes it easy to deploy the same code to different environments with different configurations.
-Here's an example using bearer token authentication (though OAuth is recommended for production):
+Here's an example using static token authentication for development (OAuth is recommended for production):
```python
import os
from fastmcp import FastMCP
-from fastmcp.server.auth import BearerTokenAuth
+from fastmcp.server.auth import StaticTokenVerifier
# Read configuration from environment
auth_token = os.environ.get("MCP_AUTH_TOKEN")
if auth_token:
- auth = BearerTokenAuth(token=auth_token)
+ auth = StaticTokenVerifier(tokens={auth_token: {"sub": "admin", "client_id": "cli"}})
mcp = FastMCP("Production Server", auth=auth)
else:
mcp = FastMCP("Production Server")
@@ -687,9 +783,7 @@ If you're using the [OAuth Proxy](/servers/auth/oauth-proxy), FastMCP issues its
**Default Behavior (Development Only):**
-By default, FastMCP automatically manages cryptographic keys:
-- **Mac/Windows**: Keys are generated and stored in your system keyring, surviving server restarts. Suitable **only** for development and local testing.
-- **Linux**: Keys are ephemeral (random salt at startup), so tokens are invalidated on restart.
+By default, FastMCP automatically manages cryptographic keys the same way on every platform: the signing key is deterministically derived from your OAuth client secret, so it survives server restarts as long as the secret doesn't change. Suitable **only** for development and local testing.
This automatic approach is convenient for development but not suitable for production deployments.
diff --git a/docs/deployment/prefect-horizon.mdx b/docs/deployment/prefect-horizon.mdx
index a7d053813..68f157c52 100644
--- a/docs/deployment/prefect-horizon.mdx
+++ b/docs/deployment/prefect-horizon.mdx
@@ -5,7 +5,7 @@ description: The MCP platform from the FastMCP team
icon: cloud
---
-[Prefect Horizon](https://www.prefect.io/horizon) is a platform for deploying and managing MCP servers. Built by the FastMCP team at [Prefect](https://www.prefect.io), Horizon provides managed hosting, authentication, access control, and a registry of MCP capabilities.
+[Prefect Horizon](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_horizon&utm_content=guide_intro) is a platform for deploying and managing MCP servers. Built by the FastMCP team at [Prefect](https://www.prefect.io), Horizon provides managed hosting, authentication, access control, and a registry of MCP capabilities.
Horizon includes a **free personal tier for FastMCP users**, making it the fastest way to get a secure, production-ready server URL with built-in OAuth authentication.
@@ -54,7 +54,7 @@ There are just three steps to deploying a server to Horizon:
### Step 1: Select a Repository
-Visit [horizon.prefect.io](https://horizon.prefect.io) and sign in with your GitHub account. Connect your GitHub account to grant Horizon access to your repositories, then select the repo you want to deploy.
+Visit [horizon.prefect.io](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) and sign in with your GitHub account. Connect your GitHub account to grant Horizon access to your repositories, then select the repo you want to deploy.
diff --git a/docs/deployment/running-server.mdx b/docs/deployment/running-server.mdx
index c10855345..9bb8e544c 100644
--- a/docs/deployment/running-server.mdx
+++ b/docs/deployment/running-server.mdx
@@ -240,7 +240,7 @@ if __name__ == "__main__":
mcp.run(transport="http") # Health check at http://localhost:8000/health
```
-Custom routes are served by the same web server as your MCP endpoint. They're available at the root of your domain while the MCP endpoint is at `/mcp/`. For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/deployment/http#integration-with-web-frameworks).
+Custom routes are served by the same web server as your MCP endpoint. They're available at the root of your domain while the MCP endpoint is at `/mcp`. For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/deployment/http#integration-with-web-frameworks).
## Alternative Initialization Patterns
diff --git a/docs/deployment/sandboxed-agents.mdx b/docs/deployment/sandboxed-agents.mdx
new file mode 100644
index 000000000..16191affb
--- /dev/null
+++ b/docs/deployment/sandboxed-agents.mdx
@@ -0,0 +1,262 @@
+---
+title: Sandboxed Agents
+sidebarTitle: Sandboxed Agents
+description: Expose MCP tools to isolated agents without giving the sandbox long-lived credentials.
+icon: box-open
+---
+
+This guide is for deployments where an agent runs inside an isolated container, subprocess, or remote worker and still needs MCP access. In that setup, the sandbox itself becomes part of your trust boundary.
+
+The core recommendation is simple: use FastMCP as the capability boundary. Run a remote FastMCP server, authenticate the sandbox with short-lived scoped credentials, and keep privileged credentials on the server side.
+
+## When to Use This Pattern
+
+This pattern is useful when:
+
+- your agent runs in an ephemeral container or subprocess
+- you do not want long-lived credentials inside that sandbox
+- you need per-run, per-tenant, or per-job scoping
+- the sandbox must call internal APIs, databases, or upstream MCP servers indirectly
+
+If you are building a local desktop integration, STDIO and normal local configuration may be enough. This guide is for cases where the sandbox is isolated enough that secret distribution, credential lifetimes, and privilege boundaries become part of the design.
+
+## What Changes in a Sandboxed Deployment
+
+A desktop MCP client usually runs on a developer's machine and launches local servers with configuration the developer controls. A sandboxed agent is different:
+
+- It often runs in an ephemeral container or subprocess.
+- Its filesystem may be inspected after the fact.
+- Its environment variables may be broader than you intend.
+- You may launch many sandboxes concurrently for different users, tenants, or jobs.
+
+That means convenience patterns that are acceptable locally become risky in sandboxes. Passing a GitHub token, database password, or cloud credentials directly into the sandbox creates a secret distribution problem you do not need to have.
+
+The safer approach is to make your FastMCP server the only component with privileged access and let the sandbox call it over MCP.
+
+## Recommended Architecture
+
+Use this shape by default:
+
+```mermaid
+flowchart LR
+ A["Sandboxed agent"] -->|"short-lived token"| B["FastMCP server"]
+ B --> C["internal APIs"]
+ B --> D["databases"]
+ B --> E["other MCP servers"]
+```
+
+The sandbox gets:
+
+- the MCP server URL
+- a short-lived token scoped to its job, tenant, or run
+- no long-lived upstream credentials
+
+The FastMCP server does the privileged work:
+
+- verifies the sandbox token
+- authorizes the request from token claims, scopes, or other server-side policy
+- exposes only the tools that sandbox should see
+- talks to internal APIs, databases, or upstream MCP servers on the sandbox's behalf
+
+The key design rule is simple:
+
+
+Give the sandbox capabilities, not credentials.
+
+
+With that boundary in place, the next questions are how the sandbox connects, how the server verifies and authorizes it, and how you design the tools the sandbox is allowed to call.
+
+## Prefer HTTP for Sandboxed Agents
+
+For sandboxes, prefer a remote HTTP server over a local STDIO server.
+
+STDIO is still excellent for local development, but a remote HTTP server is usually the better production boundary for sandboxed agents because:
+
+- authentication is explicit
+- the server lifecycle is independent from the sandbox lifecycle
+- secrets stay on the server
+- one deployment can safely serve many sandboxes
+- auditing and revocation happen in one place
+
+This means the sandbox should connect as a client:
+
+```python
+from fastmcp import Client
+from fastmcp.client.auth import BearerAuth
+
+client = Client(
+ "https://sandbox-tools.example.com/mcp",
+ auth=BearerAuth("short-lived-sandbox-token"),
+)
+```
+
+And your FastMCP server should run remotely:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("Sandbox Tools")
+
+if __name__ == "__main__":
+ mcp.run(transport="http", host="0.0.0.0", port=8000)
+```
+
+For production transport setup, see [HTTP Deployment](/deployment/http).
+
+## Use Short-Lived, Scoped Credentials
+
+For sandboxed agents, it is usually cleaner to issue credentials for the sandbox session than to place long-lived upstream credentials directly inside the container.
+
+In practice, that usually means issuing a short-lived bearer token for each sandbox, run, or tenant and validating it on your FastMCP server with a token verifier.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+auth = JWTVerifier(
+ jwks_uri="https://auth.example.com/.well-known/jwks.json",
+ issuer="https://auth.example.com",
+ audience="sandbox-mcp",
+)
+
+mcp = FastMCP("Sandbox Tools", auth=auth)
+```
+
+The token should identify the sandbox's scope. Depending on your system, it may represent a job, a tenant, a run, or a user-authorized session. Useful claims often include:
+
+- sandbox or run id
+- tenant or installation id
+- user or actor id when applicable
+- expiration
+- optional capability scopes
+
+Avoid shared static tokens across many sandboxes. If one sandbox token leaks, you want the blast radius to be small and the lifetime to be short.
+
+Token verification is only one half of the boundary. Authorization still belongs on the FastMCP server: use scopes, claims, middleware, or custom auth checks to decide which tools and resources that sandbox can actually access.
+
+For example, you can verify the token globally and still require a narrower scope on a specific tool:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import require_scopes
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+auth = JWTVerifier(
+ jwks_uri="https://auth.example.com/.well-known/jwks.json",
+ issuer="https://auth.example.com",
+ audience="sandbox-mcp",
+)
+
+mcp = FastMCP("Sandbox Tools", auth=auth)
+
+@mcp.tool(auth=require_scopes("write:summary"))
+def write_summary(content: str) -> str:
+ return f"Stored summary with {len(content)} characters"
+```
+
+For validation patterns, see [Token Verification](/servers/auth/token-verification). For policy enforcement, see [Authorization](/servers/authorization).
+
+## Expose Capabilities, Not Raw Access
+
+The sandbox should not need:
+
+- GitHub app private keys
+- database passwords
+- upstream OAuth client secrets
+- cloud provider credentials
+
+Instead, expose MCP tools that perform privileged work on the server side.
+
+Good sandbox-facing tools tend to look like this:
+
+- `get_recent_updates`
+- `write_summary`
+- `fetch_repo_context`
+- `publish_review_comment`
+
+These tools describe the capability the sandbox needs, not the low-level credentialed action required to perform it.
+
+That distinction matters. A tool like `write_summary` lets the server decide where and how to persist the summary. A tool like `run_sql` or `call_internal_api` pushes privilege and policy into the sandbox where they are much harder to control.
+
+Sandboxed agents behave best when those tools are narrow and structured:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("Sandbox Tools")
+
+@mcp.tool
+def write_summary(content: str) -> str:
+ """Store the final summary for the current run."""
+ return f"Stored summary with {len(content)} characters"
+
+@mcp.tool
+def publish_review_comment(pr_number: int, body: str) -> str:
+ """Queue a review comment for a specific pull request."""
+ return f"Queued comment for PR #{pr_number}"
+```
+
+These are easier to audit, easier to authorize, and easier for agents to use reliably than a broad catch-all tool like `mutate_state(kind: str, payload: dict)`.
+
+Narrow tools also let you express different policies per tool instead of creating one large privileged escape hatch.
+
+## Use a Proxy When Upstream Systems Are More Privileged
+
+If the sandbox needs access to other MCP servers or internal systems, put FastMCP in front of them instead of forwarding secrets into the sandbox.
+
+This is where proxying becomes useful. Your public-facing FastMCP server can authenticate the sandbox, then forward allowed capabilities to upstream systems with stronger credentials.
+
+Typical examples:
+
+- a sandbox-safe MCP gateway in front of internal MCP servers
+- a FastMCP layer in front of internal HTTP APIs
+- a job-scoped server that fronts a Git provider, issue tracker, or storage system
+
+If the upstream system is itself an MCP server, FastMCP's proxy support is a natural fit. See [MCP Proxy](/servers/providers/proxy).
+
+## mcp.json for Sandboxed Clients
+
+If your sandboxed agent is configured through `mcp.json`, keep that configuration minimal. Point it at the remote FastMCP server and pass only the values the sandbox actually needs.
+
+```json
+{
+ "mcpServers": {
+ "sandbox-tools": {
+ "url": "https://sandbox-tools.example.com/mcp",
+ "transport": "http"
+ }
+ }
+}
+```
+
+In many systems, authentication is injected by the launcher or environment rather than hardcoded in `mcp.json`. That is usually the right tradeoff for sandboxes. Avoid baking long-lived credentials directly into generated config files, and avoid treating `mcp.json` as the place where secret material should live.
+
+That is all this section needs to do: tell the sandbox where the server lives. Keep auth and secret handling elsewhere.
+
+For configuration details, see [MCP.json](/integrations/mcp-json-configuration).
+
+## Common Mistakes
+
+The same few mistakes show up again and again in sandboxed deployments:
+
+- passing long-lived API keys directly into the sandbox
+- treating helper scripts in the sandbox as a security boundary
+- exposing broad mutation tools instead of narrow capabilities
+- using one shared token for every sandbox
+- relying on STDIO inheritance for configuration in production
+
+Each of these works at first. Each becomes painful once you have multiple tenants, multiple jobs, or an incident that requires revoking access quickly.
+
+## Production Checklist
+
+Before shipping a sandbox-facing FastMCP server, check these:
+
+- The sandbox connects over HTTP, not with privileged local credentials.
+- Tokens are short-lived and scoped to a run, tenant, or job.
+- The FastMCP server verifies tokens on every request.
+- Long-lived secrets stay on the server side.
+- Tools are narrow, explicit, and structured.
+- Upstream privileged systems sit behind the FastMCP server or proxy.
+- Revocation and audit live at the server boundary, not inside the sandbox.
+
+If you adopt those defaults, sandbox support stops being a special case and becomes a normal deployment pattern: isolated workers talk to a constrained FastMCP surface, and the server handles the privileged parts centrally.
diff --git a/docs/deployment/server-configuration.mdx b/docs/deployment/server-configuration.mdx
index f9b0e4781..c67d5ef1f 100644
--- a/docs/deployment/server-configuration.mdx
+++ b/docs/deployment/server-configuration.mdx
@@ -39,30 +39,33 @@ The `fastmcp.json` configuration answers three fundamental questions about your
This conceptual model helps you understand the purpose of each configuration section and organize your settings effectively. The configuration file maps directly to these three concerns:
+`source` is the *where*, `environment` the *what*, and `deployment` the *how*:
+
```json
{
"$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
"source": {
- // WHERE: Location of your server code
- "type": "filesystem", // Optional, defaults to "filesystem"
+ "type": "filesystem",
"path": "server.py",
"entrypoint": "mcp"
},
"environment": {
- // WHAT: Environment setup and dependencies
- "type": "uv", // Optional, defaults to "uv"
+ "type": "uv",
"python": ">=3.10",
"dependencies": ["pandas", "numpy"]
},
"deployment": {
- // HOW: Runtime configuration
"transport": "stdio",
"log_level": "INFO"
}
}
```
-Only the `source` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed.
+Only the `source` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed. Both `type` fields shown above are optional too, defaulting to `"filesystem"` and `"uv"` respectively.
+
+
+`fastmcp.json` is parsed as strict JSON, so it accepts no comments or trailing commas.
+
### JSON Schema Support
@@ -229,9 +232,10 @@ Environment variables are included in this section because they're runtime confi
- Protocol for client communication:
+ Protocol for client communication. `"http"` and `"streamable-http"` both select FastMCP's Streamable HTTP transport:
- `"stdio"`: Standard input/output for desktop clients
- - `"http"`: Network-accessible HTTP server
+ - `"http"`: Network-accessible Streamable HTTP server
+ - `"streamable-http"`: Explicit alias for Streamable HTTP
- `"sse"`: Server-sent events
@@ -241,12 +245,12 @@ Environment variables are included in this section because they're runtime confi
- `"0.0.0.0"`: All network interfaces
-
- Port number for HTTP transport.
+
+ Port number for HTTP transport. If omitted, FastMCP uses the server runtime default.
-
- URL path for the MCP endpoint when using HTTP transport.
+
+ URL path for the MCP endpoint when using HTTP transport. The default is `/mcp` for Streamable HTTP and `/sse` for SSE.
@@ -396,20 +400,20 @@ This flag tells FastMCP: "I already have the source code, skip any download/clon
Note: For filesystem sources (local Python files), this flag has no effect since they don't require preparation.
-The configuration file works with all FastMCP commands:
+The configuration file works with server-loading commands that explicitly accept FastMCP config files:
- **`run`** - Start the server in production mode
-- **`dev`** - Launch with the Inspector UI for development
+- **`dev inspector`** - Launch with the Inspector UI for development
- **`inspect`** - View server capabilities and configuration
-- **`install`** - Install to Claude Desktop, Cursor, or other MCP clients
+- **`install`** - Install to Claude Desktop, Cursor, or another MCP client
-When no file argument is provided, FastMCP searches the current directory for `fastmcp.json`. This means you can simply navigate to your project directory and run `fastmcp run` to start your server with all its configured settings.
+`run`, `dev inspector`, and `inspect` search the current directory for a file named exactly `fastmcp.json` when you don't pass a file argument, so you can navigate to your project directory and run `fastmcp run` to start your server with all its configured settings. `install` requires an explicit path to the config file — it never searches.
### CLI Override Behavior
Command-line arguments take precedence over configuration file values, allowing ad-hoc adjustments without modifying the file:
```bash
-# Config specifies port 3000, CLI overrides to 8080
+# Config specifies port 8000, CLI overrides to 8080
fastmcp run fastmcp.json --port 8080
# Config specifies stdio, CLI overrides to HTTP
@@ -434,7 +438,7 @@ You can use different configuration files for different environments:
- `prod.fastmcp.json` - Production settings
- `test_fastmcp.json` - Test configuration
-Any file with "fastmcp.json" in the name is recognized as a configuration file.
+Only a file named exactly `fastmcp.json` is auto-detected when you omit the path. Other FastMCP configuration files can use any `.json` name, but you must pass them explicitly.
## Examples
@@ -471,7 +475,7 @@ A configuration optimized for local development:
"type": "uv",
"python": "3.12",
"dependencies": ["fastmcp[dev]"],
- "editable": "."
+ "editable": ["."]
},
// HOW should it run?
"deployment": {
@@ -510,7 +514,7 @@ A production-ready configuration with full dependency management:
"transport": "http",
"host": "0.0.0.0",
"port": 3000,
- "path": "/api/mcp/",
+ "path": "/api/mcp",
"log_level": "INFO",
"env": {
"ENV": "production",
diff --git a/docs/development/contributing.mdx b/docs/development/contributing.mdx
index c8772765a..e03ec37c7 100644
--- a/docs/development/contributing.mdx
+++ b/docs/development/contributing.mdx
@@ -134,7 +134,7 @@ Tests are documentation that shows how features work. Good tests give reviewers
uv run pytest tests/server/ -v
# Run all tests before submitting PR
-uv run pytest
+uv run pytest -n auto
```
Every new feature needs tests. See the [Testing Guide](/development/tests) for patterns and requirements.
@@ -166,7 +166,7 @@ just api-ref-all
#### Before Submitting
-1. **Run all checks**: `uv run prek run --all-files && uv run pytest`
+1. **Run all checks**: `uv run prek run --all-files && uv run pytest -n auto`
2. **Keep scope small**: One feature or fix per PR
3. **Write clear description**: Your PR description becomes permanent documentation
4. **Update docs**: Include documentation for API changes
diff --git a/docs/development/releases.mdx b/docs/development/releases.mdx
index 346462736..f537703e2 100644
--- a/docs/development/releases.mdx
+++ b/docs/development/releases.mdx
@@ -20,7 +20,7 @@ Major versions represent fundamental shifts. FastMCP 2.x is entirely different f
Unlike traditional semantic versioning, minor versions **may** include [breaking changes](#breaking-changes) when necessary for the ecosystem's evolution. This flexibility is essential in a young ecosystem where perfect backwards compatibility would prevent important improvements.
-FastMCP always targets the most current MCP Protocol version. Breaking changes in the MCP spec or MCP SDK automatically flow through to FastMCP - we prioritize staying current with the latest features and conventions over maintaining compatibility with older protocol versions.
+FastMCP tracks the current MCP Protocol version while serving earlier handshake versions alongside it. Building on MCP SDK v2, a FastMCP server negotiates the protocol era each client speaks — the sessionless `2026-07-28` era and earlier session-based eras are both handled by the same server. New features and conventions from the spec flow through to FastMCP as they land; for the details of which capabilities are available on each era, see [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3#protocol-version-support).
**Patch (2.0.x)**: Bug fixes and refinements
@@ -53,8 +53,8 @@ We expect this exemption to last through at least the 2.12.x and 2.13.x release
Pin to exact versions:
```
-fastmcp==2.11.0 # Good
-fastmcp>=2.11.0 # Bad - will install breaking changes
+fastmcp==4.0.0 # Good
+fastmcp>=4.0.0 # Bad - will install breaking changes
```
## Creating Releases
@@ -65,6 +65,8 @@ Our release process is intentionally simple:
2. Generate release notes automatically, and curate or add additional editorial information as needed
3. GitHub releases automatically trigger PyPI deployments
+Current-major releases target `main`. Maintenance releases target their release branch, such as `release/3.x` for 3.x patches and `release/2.x` for 2.x patches. Stable releases from `main` open a PR that syncs the release commit to `published-docs` after PyPI publishing succeeds; merging that PR publishes the live docs. Prereleases skip the automatic PR and use the same PR-based sync when their docs are ready to publish. Maintenance releases publish packages and GitHub release notes without repointing the live docs branch.
+
This automation lets maintainers focus on code quality rather than release mechanics.
### Release Cadence
diff --git a/docs/development/tests.mdx b/docs/development/tests.mdx
index 6a9973fe8..37760e7aa 100644
--- a/docs/development/tests.mdx
+++ b/docs/development/tests.mdx
@@ -14,7 +14,7 @@ Good tests are the foundation of reliable software. In FastMCP, we treat tests a
```bash
# Run all tests
-uv run pytest
+uv run pytest -n auto
# Run specific test file
uv run pytest tests/server/test_auth.py
@@ -26,14 +26,14 @@ uv run pytest --cov=fastmcp
uv run pytest -m "not integration"
# Skip tests that spawn processes
-uv run pytest -m "not integration and not client_process"
+uv run pytest -m "not integration and not client_process and not subprocess_heavy"
```
Tests should complete in under 1 second unless marked as integration tests. This speed encourages running them frequently, catching issues early.
### Test Organization
-Our test organization mirrors the `src/` directory structure, creating a predictable mapping between code and tests. When you're working on `src/fastmcp/server/auth.py`, you'll find its tests in `tests/server/test_auth.py`. In rare cases tests are split further - for example, the OpenAPI tests are so comprehensive they're split across multiple files.
+Our test organization mirrors the source package structure, creating a predictable mapping between code and tests. When you're working on `fastmcp_slim/fastmcp/server/auth.py`, you'll find its tests in `tests/server/test_auth.py`. In rare cases tests are split further - for example, the OpenAPI tests are so comprehensive they're split across multiple files.
### Test Markers
@@ -61,6 +61,40 @@ async def test_stdio_transport():
assert result.content[0].text == "test"
```
+A third marker, `subprocess_heavy`, exists specifically for Windows CI stability. See [Windows CI and Test Parallelism](#windows-ci-and-test-parallelism) below for when to use it and why it exists.
+
+### Windows CI and Test Parallelism
+
+Windows CI ran the unit suite serially for months. [#2715](https://github.com/PrefectHQ/fastmcp/pull/2715) tried enabling `pytest-xdist` parallelism there in December 2025; [#2726](https://github.com/PrefectHQ/fastmcp/pull/2726) reverted it the next day because "Windows tests continue to fail with intermittent worker crashes." [#4554](https://github.com/PrefectHQ/fastmcp/pull/4554) re-enabled it after removing most of the subprocess pressure that caused those crashes, taking the Windows unit step from roughly 460s to 175s.
+
+That pressure came from three sources, all addressed by #4554: most HTTP tests moved in-process via `asgi_client` instead of binding real sockets, stdio lifecycle tests spawn a minimal stdlib responder (`tests/client/minimal_stdio_server.py`, ~0.03s to start) instead of a subprocess that runs `import fastmcp` (~0.7s), and roughly 80 real `sleep()` calls became deterministic waits on the condition each test actually cared about. Fewer, cheaper subprocesses competing under parallel workers left fewer chances for a worker to die.
+
+#### The `subprocess_heavy` marker
+
+One class of test still spawns a full Python interpreter that imports FastMCP — checking that a bare install doesn't need optional dependencies, or that a decorator works from a fresh process. Each spawn pays a full interpreter's startup and memory footprint, and a 2-core Windows runner already running 2 xdist workers has little headroom left to absorb that. These tests carry `@pytest.mark.subprocess_heavy` and run in the existing serial `client_process` CI step instead of alongside the parallel workers — `.github/actions/run-pytest/action.yml` routes `client_process or subprocess_heavy` to that step (`MAX_PROCS=0`) and excludes both markers from the parallel unit step.
+
+If a test runs `subprocess.run([sys.executable, "-c", ...])`, or otherwise starts a fresh interpreter that imports `fastmcp`, mark it `subprocess_heavy`. A subprocess that runs a minimal stdlib script with no FastMCP import doesn't need the marker — it's the interpreter startup and import that's expensive, not the subprocess itself.
+
+#### This is a mitigation, not a proof
+
+There is no root-cause diagnosis behind this fix, only a plausible one. During validation, one Windows run genuinely crashed a worker on `test_fastmcp_imports_without_legacy_httpx` — a fresh-interpreter test — with pytest-xdist reporting `worker 'gw1' crashed while running '...'` after execnet's channel saw `ConnectionResetError: [WinError 10054]`. Nothing in that log says *why* the worker died: memory exhaustion, handle exhaustion, and some Windows-specific `subprocess`/`execnet` interaction are all still consistent with what was observed. Marking the fresh-interpreter tests `subprocess_heavy` made the crash stop recurring, but "it stopped" is not the same as "we know why."
+
+Treat the next Windows worker crash as a test of this diagnosis. **If it lands on a test that is not a fresh-interpreter spawner, the `subprocess_heavy` theory was wrong** — the real problem is subprocess-under-xdist on Windows more generally, and isolating one marker's worth of tests was never going to fix that. The fallback is one conditional back in `run-pytest/action.yml`, restoring the pre-#4554 behavior:
+
+```bash
+PARALLEL_FLAGS=""
+if [ "$MAX_PROCS" != "0" ] && [ "${{ runner.os }}" != "Windows" ]; then
+ PARALLEL_FLAGS="--numprocesses auto --maxprocesses $MAX_PROCS --dist worksteal"
+fi
+```
+
+#### Two traps that aren't Windows-specific
+
+Two test-authoring bugs surfaced while validating this change. Neither is about Windows or parallelism, but both are worth watching for anywhere a real `sleep()` gets replaced with a wait:
+
+- **Match the wait condition to the assertion.** A test waited for "any callback fired," then asserted that a `completed` callback existed. That races, because an earlier `working` notification satisfies the wait before the `completed` one arrives. A deterministic wait is only as good as the condition it waits on — wait for the thing you actually assert.
+- **Don't assert on incidental timing.** A crash-recovery test asserted "at least one concurrent request fails" while a subprocess restarts, which quietly depended on the restart being slow. Once restart got faster, recovery could beat every in-flight request and the test started failing because the behavior *improved*. Assert the invariant instead: no hang, and no result served by the dead process.
+
## Writing Tests
@@ -228,7 +262,7 @@ async def test_tool_schema_generation():
return {"amount": amount, "tax": amount * rate, "total": amount * (1 + rate)}
tools = mcp.list_tools()
- schema = tools[0].inputSchema
+ schema = tools[0].input_schema
# First run: snapshot() is empty, gets auto-populated
# Subsequent runs: compares against stored snapshot
@@ -299,22 +333,19 @@ async def test_database_tool():
### Testing Network Transports
-While in-memory testing covers most unit testing needs, you'll occasionally need to test actual network transports like HTTP or SSE. FastMCP provides two approaches: in-process async servers (preferred), and separate subprocess servers (for special cases).
+In-memory testing covers most unit testing needs, but some behavior only exists over HTTP: middleware, authentication, session management, header handling, and SSE streaming. To test those, serve your server over HTTP with `asgi_client`.
-#### In-Process Network Testing (Preferred)
+#### Testing Over HTTP
-
+
-For most network transport tests, use `run_server_async` as an async context manager. This runs the server as a task in the same process, providing fast, deterministic tests with full debugger support:
+`asgi_client` builds your server's real Starlette app, starts its lifespan, and hands you a connected `Client` that talks to it over the full HTTP stack. The one thing it skips is the socket: requests are dispatched straight into the ASGI application on the current event loop, so there is no port to bind, no uvicorn to start, and no connection to negotiate. Everything else — middleware, authentication, session management, SSE framing — runs exactly as it does in production.
```python
-import pytest
-from fastmcp import FastMCP, Client
-from fastmcp.client.transports import StreamableHttpTransport
-from fastmcp.utilities.tests import run_server_async
+from fastmcp import FastMCP
+from fastmcp.utilities.tests import asgi_client
def create_test_server() -> FastMCP:
- """Create a test server instance."""
server = FastMCP("TestServer")
@server.tool
@@ -323,26 +354,89 @@ def create_test_server() -> FastMCP:
return server
-@pytest.fixture
-async def http_server() -> str:
- """Start server in-process for testing."""
- server = create_test_server()
- async with run_server_async(server) as url:
- yield url
-
-async def test_http_transport(http_server: str):
- """Test actual HTTP transport behavior."""
- async with Client(
- transport=StreamableHttpTransport(http_server)
- ) as client:
- result = await client.ping()
- assert result is True
-
+async def test_greet_over_http():
+ async with asgi_client(create_test_server()) as client:
greeting = await client.call_tool("greet", {"name": "World"})
assert greeting.data == "Hello, World!"
```
-The `run_server_async` context manager automatically handles server lifecycle and cleanup. This approach is faster than subprocess-based testing and provides better error messages.
+Pass `transport="sse"` to exercise the SSE app instead of streamable HTTP, `path=` to serve on a custom path, `headers=` and `auth=` to configure the client's requests, and any other keyword argument to configure the `Client` itself.
+
+```python
+async def test_tenant_header_is_visible_to_tools():
+ async with asgi_client(
+ create_test_server(),
+ headers={"X-Tenant-ID": "acme"},
+ timeout=5,
+ ) as client:
+ await client.list_tools()
+```
+
+#### Sharing One Server Across Tests
+
+When several tests share a server but each needs its own client, use `asgi_server` in a fixture. It yields an `ASGIServer`, whose `client()` method produces a fresh client — with its own session — on demand.
+
+```python
+import pytest
+from fastmcp import FastMCP
+from fastmcp.utilities.tests import ASGIServer, asgi_server
+
+@pytest.fixture
+async def http_server():
+ server = FastMCP("TestServer")
+
+ @server.tool
+ def greet(name: str) -> str:
+ return f"Hello, {name}!"
+
+ async with asgi_server(server) as running_server:
+ yield running_server
+
+async def test_greet(http_server: ASGIServer):
+ async with http_server.client() as client:
+ greeting = await client.call_tool("greet", {"name": "World"})
+ assert greeting.data == "Hello, World!"
+
+async def test_sessions_are_isolated(http_server: ASGIServer):
+ async with (
+ http_server.client(mode="legacy") as first,
+ http_server.client(mode="legacy") as second,
+ ):
+ assert await first.ping() is True
+ assert await second.ping() is True
+```
+
+Sessions belong to the handshake era of the MCP protocol, and so does `ping`, so a test that is about session behavior pins `mode="legacy"`. Every keyword argument `client()` doesn't consume itself is passed straight to `Client`. See [protocol negotiation](/clients/client#protocol-negotiation).
+
+For assertions about raw HTTP — status codes, response headers, metadata endpoints — `http_client()` returns an `httpx.AsyncClient` bound to the same app. Because nothing is listening on the network, this is the only way to make raw requests; a plain `httpx.AsyncClient()` cannot reach the server.
+
+```python
+async def test_unauthenticated_request_is_rejected(http_server: ASGIServer):
+ async with http_server.http_client() as http:
+ response = await http.post(http_server.url, json={"jsonrpc": "2.0", "id": 1})
+ assert response.status_code in (400, 401)
+```
+
+If you need to build the client transport yourself, `transport()` returns a `StreamableHttpTransport` or `SSETransport` already wired to the in-process app.
+
+#### Testing on a Real Port
+
+
+
+`run_server_async` starts a real uvicorn server on a real TCP port as a task in the current process and yields its URL. Reach for it only when the subject of the test is the network itself — real sockets, TLS, or a server that must be reachable by something other than an in-process client.
+
+```python
+from fastmcp import FastMCP, Client
+from fastmcp.utilities.tests import run_server_async
+
+async def test_server_binds_a_real_port():
+ server = FastMCP("TestServer")
+
+ async with run_server_async(server) as url:
+ assert url.startswith("http://127.0.0.1:")
+ async with Client(url) as client:
+ assert await client.list_tools() == []
+```
#### Subprocess Testing (Special Cases)
@@ -375,8 +469,8 @@ async def test_http_transport(http_server: str):
async with Client(
transport=StreamableHttpTransport(http_server)
) as client:
- result = await client.ping()
- assert result is True
+ tools = await client.list_tools()
+ assert "greet" in [tool.name for tool in tools]
```
The `run_server_in_process` utility handles server lifecycle, port allocation, and cleanup automatically. Use this only when subprocess isolation is truly necessary, as it's slower and harder to debug than in-process testing. FastMCP uses the `client_process` marker to isolate these tests in CI.
@@ -393,4 +487,4 @@ just docs
mintlify dev
```
-The local server watches for changes and automatically refreshes. This preview catches formatting issues and helps you see documentation as users will experience it.
\ No newline at end of file
+The local server watches for changes and automatically refreshes. This preview catches formatting issues and helps you see documentation as users will experience it.
diff --git a/docs/docs.json b/docs/docs.json
index 69b173496..c52daada0 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -12,7 +12,11 @@
"decoration": "gradient"
},
"banner": {
- "content": "Deploy FastMCP servers for free on [Prefect Horizon](https://www.prefect.io/horizon)"
+ "color": {
+ "dark": "#475569",
+ "light": "#1e3a5f"
+ },
+ "content": "FastMCP 4 is in beta — build stateful applications on sessionless MCP. [See what's new](/getting-started/whats-new)."
},
"colors": {
"dark": "#f72585",
@@ -20,15 +24,12 @@
"primary": "#2d00f7"
},
"contextual": {
- "options": [
- "copy",
- "view"
- ]
+ "options": ["copy", "view"]
},
"description": "The fast, Pythonic way to build MCP servers and clients.",
"errors": {
"404": {
- "description": "You\u2019ve wandered outside the context.",
+ "description": "You’ve wandered outside the context.",
"redirect": false,
"title": "Don't panic."
}
@@ -66,7 +67,7 @@
"label": ""
},
{
- "href": "https://prefect.io/horizon",
+ "href": "https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_horizon&utm_content=header",
"icon": "cloud",
"label": "Prefect Horizon"
}
@@ -88,7 +89,8 @@
"pages": [
"getting-started/welcome",
"getting-started/installation",
- "getting-started/quickstart"
+ "getting-started/quickstart",
+ "getting-started/whats-new"
]
},
{
@@ -108,30 +110,23 @@
},
{
"collapsed": true,
- "group": "Features",
- "icon": "stars",
+ "group": "Working with Tools",
+ "icon": "wand-magic-sparkles",
"pages": [
- "servers/tasks",
- "servers/composition",
- "servers/dependency-injection",
- "servers/elicitation",
- "servers/icons",
- "servers/lifespan",
- "servers/logging",
- "servers/middleware",
- "servers/pagination",
- "servers/progress",
- "servers/sampling",
- "servers/storage-backends",
- "servers/telemetry",
- "servers/testing",
- "servers/versioning"
- ],
- "tag": "UPDATED"
+ "servers/transforms/transforms",
+ "servers/transforms/tool-transformation",
+ "servers/transforms/code-mode",
+ "servers/transforms/tool-search",
+ "servers/transforms/namespace",
+ "servers/visibility",
+ "servers/transforms/resources-as-tools",
+ "servers/transforms/prompts-as-tools",
+ "servers/tool-fingerprinting"
+ ]
},
{
"collapsed": true,
- "group": "Providers",
+ "group": "MCP Providers",
"icon": "layer-group",
"pages": [
"servers/providers/overview",
@@ -139,42 +134,61 @@
"servers/providers/filesystem",
"servers/providers/proxy",
"servers/providers/skills",
+ "servers/composition",
"servers/providers/custom"
- ],
- "tag": "NEW"
+ ]
},
{
"collapsed": true,
- "group": "Transforms",
- "icon": "wand-magic-sparkles",
+ "group": "Interactivity",
+ "icon": "comments",
"pages": [
- "servers/transforms/transforms",
- "servers/transforms/namespace",
- "servers/transforms/tool-transformation",
- "servers/visibility",
- "servers/transforms/code-mode",
- "servers/transforms/tool-search",
- "servers/transforms/resources-as-tools",
- "servers/transforms/prompts-as-tools"
- ],
- "tag": "NEW"
+ "servers/elicitation",
+ "servers/sampling",
+ "servers/completions",
+ "servers/progress",
+ "servers/logging",
+ "servers/pagination",
+ "servers/icons"
+ ]
},
{
"collapsed": true,
- "group": "Authentication",
- "icon": "key",
+ "group": "Extensibility",
+ "icon": "puzzle-piece",
"pages": [
- "servers/auth/authentication",
- "servers/auth/token-verification",
- "servers/auth/remote-oauth",
- "servers/auth/oauth-proxy",
- "servers/auth/oidc-proxy",
- "servers/auth/full-oauth-server",
- "servers/auth/multi-auth"
- ],
- "tag": "UPDATED"
+ "servers/middleware",
+ "servers/dependency-injection",
+ "servers/lifespan",
+ "servers/storage-backends",
+ "servers/sessions",
+ "servers/extensions",
+ "servers/tasks",
+ "servers/versioning"
+ ]
+ },
+ {
+ "collapsed": true,
+ "group": "Auth",
+ "icon": "shield-check",
+ "pages": [
+ {
+ "collapsed": true,
+ "group": "Authentication",
+ "icon": "key",
+ "pages": [
+ "servers/auth/authentication",
+ "servers/auth/token-verification",
+ "servers/auth/remote-oauth",
+ "servers/auth/oauth-proxy",
+ "servers/auth/oidc-proxy",
+ "servers/auth/full-oauth-server",
+ "servers/auth/multi-auth"
+ ]
+ },
+ "servers/authorization"
+ ]
},
- "servers/authorization",
{
"collapsed": true,
"group": "Deployment",
@@ -182,8 +196,11 @@
"pages": [
"deployment/running-server",
"deployment/http",
+ "deployment/sandboxed-agents",
"deployment/prefect-horizon",
- "deployment/server-configuration"
+ "deployment/server-configuration",
+ "servers/testing",
+ "servers/telemetry"
]
}
]
@@ -192,38 +209,56 @@
"group": "Apps",
"pages": [
"apps/overview",
+ "apps/quickstart",
+ "apps/fastmcp-app",
"apps/prefab",
- "apps/patterns",
- "apps/low-level"
+ "apps/generative",
+ "apps/low-level",
+ {
+ "collapsed": true,
+ "group": "Reference",
+ "icon": "book",
+ "pages": [
+ {
+ "collapsed": true,
+ "group": "Prefab Providers",
+ "icon": "cube",
+ "pages": [
+ "apps/providers/approval",
+ "apps/providers/choice",
+ "apps/providers/file-upload",
+ "apps/providers/form"
+ ]
+ },
+ "apps/development",
+ "apps/examples",
+ "apps/architecture"
+ ]
+ }
]
},
{
"group": "Clients",
"pages": [
"clients/client",
+ "clients/client-only-package",
"clients/transports",
+ "clients/fastmcp-remote",
{
"collapsed": true,
- "group": "Core Operations",
+ "group": "Operations",
"icon": "toolbox",
"pages": [
"clients/tools",
"clients/resources",
- "clients/prompts"
- ]
- },
- {
- "collapsed": true,
- "group": "Handlers",
- "icon": "hand",
- "pages": [
- "clients/notifications",
+ "clients/prompts",
"clients/sampling",
"clients/elicitation",
"clients/tasks",
"clients/progress",
"clients/logging",
- "clients/roots"
+ "clients/roots",
+ "clients/notifications"
],
"tag": "UPDATED"
},
@@ -233,6 +268,7 @@
"icon": "key",
"pages": [
"clients/auth/oauth",
+ "clients/auth/client-credentials",
"clients/auth/cimd",
"clients/auth/bearer"
],
@@ -257,6 +293,8 @@
"integrations/eunomia-authorization",
"integrations/github",
"integrations/google",
+ "integrations/huggingface",
+ "integrations/keycloak",
"integrations/oci",
"integrations/permit",
"integrations/propelauth",
@@ -269,10 +307,7 @@
"collapsed": true,
"group": "Web Frameworks",
"icon": "code",
- "pages": [
- "integrations/fastapi",
- "integrations/openapi"
- ]
+ "pages": ["integrations/fastapi", "integrations/openapi"]
},
{
"collapsed": true,
@@ -294,35 +329,42 @@
"pages": [
"integrations/anthropic",
"integrations/gemini",
- "integrations/openai"
+ "integrations/openai",
+ "integrations/pydantic-ai"
]
},
"integrations/mcp-json-configuration"
]
},
- {
- "group": "CLI",
- "pages": [
- "cli/overview",
- "cli/running",
- "cli/install-mcp",
- "cli/inspecting",
- "cli/client",
- "cli/generate-cli",
- "cli/auth"
- ]
- },
{
"group": "More",
"pages": [
+ "more/settings",
+ {
+ "collapsed": true,
+ "group": "CLI",
+ "icon": "terminal",
+ "pages": [
+ "cli/overview",
+ "cli/running",
+ "cli/install-mcp",
+ "cli/inspecting",
+ "cli/client",
+ "cli/generate-cli",
+ "cli/auth"
+ ]
+ },
{
"collapsed": true,
"group": "Upgrading",
"icon": "up",
"pages": [
+ "getting-started/upgrading/from-fastmcp-3",
"getting-started/upgrading/from-fastmcp-2",
- "getting-started/upgrading/from-mcp-sdk",
- "getting-started/upgrading/from-low-level-sdk"
+ "getting-started/upgrading/from-mcp-sdk-v1",
+ "getting-started/upgrading/from-mcp-sdk-v2",
+ "getting-started/upgrading/from-low-level-sdk-v1",
+ "getting-started/upgrading/from-low-level-sdk-v2"
]
},
{
@@ -340,11 +382,9 @@
"collapsed": true,
"group": "What's New",
"icon": "sparkles",
- "pages": [
- "updates",
- "changelog"
- ]
- }
+ "pages": ["updates", "changelog"]
+ },
+ "more/faq"
]
}
],
@@ -355,616 +395,62 @@
{
"anchor": "Python SDK",
"icon": "python",
- "pages": [
- "python-sdk/fastmcp-decorators",
- "python-sdk/fastmcp-dependencies",
- "python-sdk/fastmcp-exceptions",
- "python-sdk/fastmcp-mcp_config",
- "python-sdk/fastmcp-settings",
- "python-sdk/fastmcp-telemetry",
- {
- "group": "fastmcp.cli",
- "pages": [
- "python-sdk/fastmcp-cli-__init__",
- "python-sdk/fastmcp-cli-auth",
- "python-sdk/fastmcp-cli-cimd",
- "python-sdk/fastmcp-cli-cli",
- "python-sdk/fastmcp-cli-client",
- "python-sdk/fastmcp-cli-discovery",
- "python-sdk/fastmcp-cli-generate",
- {
- "group": "install",
- "pages": [
- "python-sdk/fastmcp-cli-install-__init__",
- "python-sdk/fastmcp-cli-install-claude_code",
- "python-sdk/fastmcp-cli-install-claude_desktop",
- "python-sdk/fastmcp-cli-install-cursor",
- "python-sdk/fastmcp-cli-install-gemini_cli",
- "python-sdk/fastmcp-cli-install-goose",
- "python-sdk/fastmcp-cli-install-mcp_json",
- "python-sdk/fastmcp-cli-install-shared",
- "python-sdk/fastmcp-cli-install-stdio"
- ]
- },
- "python-sdk/fastmcp-cli-run",
- "python-sdk/fastmcp-cli-tasks"
- ]
- },
- {
- "group": "fastmcp.client",
- "pages": [
- "python-sdk/fastmcp-client-__init__",
- {
- "group": "auth",
- "pages": [
- "python-sdk/fastmcp-client-auth-__init__",
- "python-sdk/fastmcp-client-auth-bearer",
- "python-sdk/fastmcp-client-auth-oauth"
- ]
- },
- "python-sdk/fastmcp-client-client",
- "python-sdk/fastmcp-client-elicitation",
- "python-sdk/fastmcp-client-logging",
- "python-sdk/fastmcp-client-messages",
- {
- "group": "mixins",
- "pages": [
- "python-sdk/fastmcp-client-mixins-__init__",
- "python-sdk/fastmcp-client-mixins-prompts",
- "python-sdk/fastmcp-client-mixins-resources",
- "python-sdk/fastmcp-client-mixins-task_management",
- "python-sdk/fastmcp-client-mixins-tools"
- ]
- },
- "python-sdk/fastmcp-client-oauth_callback",
- "python-sdk/fastmcp-client-progress",
- "python-sdk/fastmcp-client-roots",
- {
- "group": "sampling",
- "pages": [
- "python-sdk/fastmcp-client-sampling-__init__",
- {
- "group": "handlers",
- "pages": [
- "python-sdk/fastmcp-client-sampling-handlers-__init__",
- "python-sdk/fastmcp-client-sampling-handlers-anthropic",
- "python-sdk/fastmcp-client-sampling-handlers-google_genai",
- "python-sdk/fastmcp-client-sampling-handlers-openai"
- ]
- }
- ]
- },
- "python-sdk/fastmcp-client-tasks",
- "python-sdk/fastmcp-client-telemetry",
- {
- "group": "transports",
- "pages": [
- "python-sdk/fastmcp-client-transports-__init__",
- "python-sdk/fastmcp-client-transports-base",
- "python-sdk/fastmcp-client-transports-config",
- "python-sdk/fastmcp-client-transports-http",
- "python-sdk/fastmcp-client-transports-inference",
- "python-sdk/fastmcp-client-transports-memory",
- "python-sdk/fastmcp-client-transports-sse",
- "python-sdk/fastmcp-client-transports-stdio"
- ]
- }
- ]
- },
- {
- "group": "fastmcp.experimental",
- "pages": [
- "python-sdk/fastmcp-experimental-__init__",
- {
- "group": "sampling",
- "pages": [
- "python-sdk/fastmcp-experimental-sampling-__init__",
- "python-sdk/fastmcp-experimental-sampling-handlers"
- ]
- },
- {
- "group": "transforms",
- "pages": [
- "python-sdk/fastmcp-experimental-transforms-__init__",
- "python-sdk/fastmcp-experimental-transforms-code_mode"
- ]
- }
- ]
- },
- {
- "group": "fastmcp.prompts",
- "pages": [
- "python-sdk/fastmcp-prompts-__init__",
- "python-sdk/fastmcp-prompts-function_prompt",
- "python-sdk/fastmcp-prompts-prompt"
- ]
- },
- {
- "group": "fastmcp.resources",
- "pages": [
- "python-sdk/fastmcp-resources-__init__",
- "python-sdk/fastmcp-resources-function_resource",
- "python-sdk/fastmcp-resources-resource",
- "python-sdk/fastmcp-resources-template",
- "python-sdk/fastmcp-resources-types"
- ]
- },
- {
- "group": "fastmcp.server",
- "pages": [
- "python-sdk/fastmcp-server-__init__",
- "python-sdk/fastmcp-server-apps",
- {
- "group": "auth",
- "pages": [
- "python-sdk/fastmcp-server-auth-__init__",
- "python-sdk/fastmcp-server-auth-auth",
- "python-sdk/fastmcp-server-auth-authorization",
- "python-sdk/fastmcp-server-auth-cimd",
- "python-sdk/fastmcp-server-auth-jwt_issuer",
- "python-sdk/fastmcp-server-auth-middleware",
- {
- "group": "oauth_proxy",
- "pages": [
- "python-sdk/fastmcp-server-auth-oauth_proxy-__init__",
- "python-sdk/fastmcp-server-auth-oauth_proxy-consent",
- "python-sdk/fastmcp-server-auth-oauth_proxy-models",
- "python-sdk/fastmcp-server-auth-oauth_proxy-proxy",
- "python-sdk/fastmcp-server-auth-oauth_proxy-ui"
- ]
- },
- "python-sdk/fastmcp-server-auth-oidc_proxy",
- {
- "group": "providers",
- "pages": [
- "python-sdk/fastmcp-server-auth-providers-__init__",
- "python-sdk/fastmcp-server-auth-providers-auth0",
- "python-sdk/fastmcp-server-auth-providers-aws",
- "python-sdk/fastmcp-server-auth-providers-azure",
- "python-sdk/fastmcp-server-auth-providers-debug",
- "python-sdk/fastmcp-server-auth-providers-descope",
- "python-sdk/fastmcp-server-auth-providers-discord",
- "python-sdk/fastmcp-server-auth-providers-github",
- "python-sdk/fastmcp-server-auth-providers-google",
- "python-sdk/fastmcp-server-auth-providers-in_memory",
- "python-sdk/fastmcp-server-auth-providers-introspection",
- "python-sdk/fastmcp-server-auth-providers-jwt",
- "python-sdk/fastmcp-server-auth-providers-oci",
- "python-sdk/fastmcp-server-auth-providers-propelauth",
- "python-sdk/fastmcp-server-auth-providers-scalekit",
- "python-sdk/fastmcp-server-auth-providers-supabase",
- "python-sdk/fastmcp-server-auth-providers-workos"
- ]
- },
- "python-sdk/fastmcp-server-auth-redirect_validation",
- "python-sdk/fastmcp-server-auth-ssrf"
- ]
- },
- "python-sdk/fastmcp-server-context",
- "python-sdk/fastmcp-server-dependencies",
- "python-sdk/fastmcp-server-elicitation",
- "python-sdk/fastmcp-server-event_store",
- "python-sdk/fastmcp-server-http",
- "python-sdk/fastmcp-server-lifespan",
- "python-sdk/fastmcp-server-low_level",
- {
- "group": "middleware",
- "pages": [
- "python-sdk/fastmcp-server-middleware-__init__",
- "python-sdk/fastmcp-server-middleware-authorization",
- "python-sdk/fastmcp-server-middleware-caching",
- "python-sdk/fastmcp-server-middleware-dereference",
- "python-sdk/fastmcp-server-middleware-error_handling",
- "python-sdk/fastmcp-server-middleware-logging",
- "python-sdk/fastmcp-server-middleware-middleware",
- "python-sdk/fastmcp-server-middleware-ping",
- "python-sdk/fastmcp-server-middleware-rate_limiting",
- "python-sdk/fastmcp-server-middleware-response_limiting",
- "python-sdk/fastmcp-server-middleware-timing",
- "python-sdk/fastmcp-server-middleware-tool_injection"
- ]
- },
- {
- "group": "mixins",
- "pages": [
- "python-sdk/fastmcp-server-mixins-__init__",
- "python-sdk/fastmcp-server-mixins-lifespan",
- "python-sdk/fastmcp-server-mixins-mcp_operations",
- "python-sdk/fastmcp-server-mixins-transport"
- ]
- },
- {
- "group": "openapi",
- "pages": [
- "python-sdk/fastmcp-server-openapi-__init__",
- "python-sdk/fastmcp-server-openapi-components",
- "python-sdk/fastmcp-server-openapi-routing",
- "python-sdk/fastmcp-server-openapi-server"
- ]
- },
- {
- "group": "providers",
- "pages": [
- "python-sdk/fastmcp-server-providers-__init__",
- "python-sdk/fastmcp-server-providers-aggregate",
- "python-sdk/fastmcp-server-providers-base",
- "python-sdk/fastmcp-server-providers-fastmcp_provider",
- "python-sdk/fastmcp-server-providers-filesystem",
- "python-sdk/fastmcp-server-providers-filesystem_discovery",
- {
- "group": "local_provider",
- "pages": [
- "python-sdk/fastmcp-server-providers-local_provider-__init__",
- {
- "group": "decorators",
- "pages": [
- "python-sdk/fastmcp-server-providers-local_provider-decorators-__init__",
- "python-sdk/fastmcp-server-providers-local_provider-decorators-prompts",
- "python-sdk/fastmcp-server-providers-local_provider-decorators-resources",
- "python-sdk/fastmcp-server-providers-local_provider-decorators-tools"
- ]
- },
- "python-sdk/fastmcp-server-providers-local_provider-local_provider"
- ]
- },
- {
- "group": "openapi",
- "pages": [
- "python-sdk/fastmcp-server-providers-openapi-__init__",
- "python-sdk/fastmcp-server-providers-openapi-components",
- "python-sdk/fastmcp-server-providers-openapi-provider",
- "python-sdk/fastmcp-server-providers-openapi-routing"
- ]
- },
- "python-sdk/fastmcp-server-providers-proxy",
- {
- "group": "skills",
- "pages": [
- "python-sdk/fastmcp-server-providers-skills-__init__",
- "python-sdk/fastmcp-server-providers-skills-claude_provider",
- "python-sdk/fastmcp-server-providers-skills-directory_provider",
- "python-sdk/fastmcp-server-providers-skills-skill_provider",
- "python-sdk/fastmcp-server-providers-skills-vendor_providers"
- ]
- },
- "python-sdk/fastmcp-server-providers-wrapped_provider"
- ]
- },
- "python-sdk/fastmcp-server-proxy",
- {
- "group": "sampling",
- "pages": [
- "python-sdk/fastmcp-server-sampling-__init__",
- "python-sdk/fastmcp-server-sampling-run",
- "python-sdk/fastmcp-server-sampling-sampling_tool"
- ]
- },
- "python-sdk/fastmcp-server-server",
- {
- "group": "tasks",
- "pages": [
- "python-sdk/fastmcp-server-tasks-__init__",
- "python-sdk/fastmcp-server-tasks-capabilities",
- "python-sdk/fastmcp-server-tasks-config",
- "python-sdk/fastmcp-server-tasks-elicitation",
- "python-sdk/fastmcp-server-tasks-handlers",
- "python-sdk/fastmcp-server-tasks-keys",
- "python-sdk/fastmcp-server-tasks-notifications",
- "python-sdk/fastmcp-server-tasks-requests",
- "python-sdk/fastmcp-server-tasks-routing",
- "python-sdk/fastmcp-server-tasks-subscriptions"
- ]
- },
- "python-sdk/fastmcp-server-telemetry",
- {
- "group": "transforms",
- "pages": [
- "python-sdk/fastmcp-server-transforms-__init__",
- "python-sdk/fastmcp-server-transforms-catalog",
- "python-sdk/fastmcp-server-transforms-namespace",
- "python-sdk/fastmcp-server-transforms-prompts_as_tools",
- "python-sdk/fastmcp-server-transforms-resources_as_tools",
- {
- "group": "search",
- "pages": [
- "python-sdk/fastmcp-server-transforms-search-__init__",
- "python-sdk/fastmcp-server-transforms-search-base",
- "python-sdk/fastmcp-server-transforms-search-bm25",
- "python-sdk/fastmcp-server-transforms-search-regex"
- ]
- },
- "python-sdk/fastmcp-server-transforms-tool_transform",
- "python-sdk/fastmcp-server-transforms-version_filter",
- "python-sdk/fastmcp-server-transforms-visibility"
- ]
- }
- ]
- },
- {
- "group": "fastmcp.tools",
- "pages": [
- "python-sdk/fastmcp-tools-__init__",
- "python-sdk/fastmcp-tools-function_parsing",
- "python-sdk/fastmcp-tools-function_tool",
- "python-sdk/fastmcp-tools-tool",
- "python-sdk/fastmcp-tools-tool_transform"
- ]
- },
- {
- "group": "fastmcp.utilities",
- "pages": [
- "python-sdk/fastmcp-utilities-__init__",
- "python-sdk/fastmcp-utilities-async_utils",
- "python-sdk/fastmcp-utilities-auth",
- "python-sdk/fastmcp-utilities-cli",
- "python-sdk/fastmcp-utilities-components",
- "python-sdk/fastmcp-utilities-exceptions",
- "python-sdk/fastmcp-utilities-http",
- "python-sdk/fastmcp-utilities-inspect",
- "python-sdk/fastmcp-utilities-json_schema",
- "python-sdk/fastmcp-utilities-json_schema_type",
- "python-sdk/fastmcp-utilities-lifespan",
- "python-sdk/fastmcp-utilities-logging",
- {
- "group": "mcp_server_config",
- "pages": [
- "python-sdk/fastmcp-utilities-mcp_server_config-__init__",
- {
- "group": "v1",
- "pages": [
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-__init__",
- {
- "group": "environments",
- "pages": [
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-__init__",
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base",
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv"
- ]
- },
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config",
- {
- "group": "sources",
- "pages": [
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-__init__",
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base",
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem"
- ]
- }
- ]
- }
- ]
- },
- {
- "group": "openapi",
- "pages": [
- "python-sdk/fastmcp-utilities-openapi-__init__",
- "python-sdk/fastmcp-utilities-openapi-director",
- "python-sdk/fastmcp-utilities-openapi-formatters",
- "python-sdk/fastmcp-utilities-openapi-json_schema_converter",
- "python-sdk/fastmcp-utilities-openapi-models",
- "python-sdk/fastmcp-utilities-openapi-parser",
- "python-sdk/fastmcp-utilities-openapi-schemas"
- ]
- },
- "python-sdk/fastmcp-utilities-pagination",
- "python-sdk/fastmcp-utilities-skills",
- "python-sdk/fastmcp-utilities-tests",
- "python-sdk/fastmcp-utilities-timeout",
- "python-sdk/fastmcp-utilities-types",
- "python-sdk/fastmcp-utilities-ui",
- "python-sdk/fastmcp-utilities-version_check",
- "python-sdk/fastmcp-utilities-versions"
- ]
- }
- ]
+ "pages": {
+ "$ref": "./python-sdk-pages.json"
+ }
}
],
"dropdown": "SDK Reference",
"icon": "code"
}
],
- "version": "v3"
+ "version": "v4.0.0 (beta 1)"
},
{
- "dropdowns": [
- {
- "dropdown": "Documentation",
- "groups": [
- {
- "group": "Get Started",
- "pages": [
- "v2/getting-started/welcome",
- "v2/getting-started/installation",
- "v2/getting-started/quickstart",
- "v2/updates"
- ]
- },
- {
- "group": "Servers",
- "pages": [
- "v2/servers/server",
- {
- "group": "Core Components",
- "icon": "toolbox",
- "pages": [
- "v2/servers/tools",
- "v2/servers/resources",
- "v2/servers/prompts"
- ]
- },
- {
- "group": "Advanced Features",
- "icon": "stars",
- "pages": [
- "v2/servers/composition",
- "v2/servers/context",
- "v2/servers/elicitation",
- "v2/servers/icons",
- "v2/servers/logging",
- "v2/servers/middleware",
- "v2/servers/progress",
- "v2/servers/proxy",
- "v2/servers/sampling",
- "v2/servers/storage-backends",
- "v2/servers/tasks"
- ]
- },
- {
- "group": "Authentication",
- "icon": "shield-check",
- "pages": [
- "v2/servers/auth/authentication",
- "v2/servers/auth/token-verification",
- "v2/servers/auth/remote-oauth",
- "v2/servers/auth/oauth-proxy",
- "v2/servers/auth/oidc-proxy",
- "v2/servers/auth/full-oauth-server"
- ]
- },
- {
- "group": "Deployment",
- "icon": "rocket",
- "pages": [
- "v2/deployment/running-server",
- "v2/deployment/http",
- "deployment/prefect-horizon",
- "v2/deployment/server-configuration"
- ]
- }
- ]
- },
- {
- "group": "Clients",
- "pages": [
- {
- "group": "Essentials",
- "icon": "cube",
- "pages": [
- "v2/clients/client",
- "v2/clients/transports"
- ]
- },
- {
- "group": "Core Operations",
- "icon": "handshake",
- "pages": [
- "v2/clients/tools",
- "v2/clients/resources",
- "v2/clients/prompts"
- ]
- },
- {
- "group": "Advanced Features",
- "icon": "stars",
- "pages": [
- "v2/clients/elicitation",
- "v2/clients/logging",
- "v2/clients/progress",
- "v2/clients/sampling",
- "v2/clients/tasks",
- "v2/clients/messages",
- "v2/clients/roots"
- ]
- },
- {
- "group": "Authentication",
- "icon": "user-shield",
- "pages": [
- "v2/clients/auth/oauth",
- "v2/clients/auth/bearer"
- ]
- }
- ]
- },
- {
- "group": "Integrations",
- "pages": [
- {
- "group": "Authentication",
- "icon": "key",
- "pages": [
- "v2/integrations/auth0",
- "v2/integrations/authkit",
- "v2/integrations/aws-cognito",
- "v2/integrations/azure",
- "v2/integrations/descope",
- "v2/integrations/discord",
- "v2/integrations/github",
- "v2/integrations/google",
- "v2/integrations/oci",
- "v2/integrations/scalekit",
- "v2/integrations/supabase",
- "v2/integrations/workos"
- ]
- },
- {
- "group": "Authorization",
- "icon": "shield-check",
- "pages": [
- "v2/integrations/eunomia-authorization",
- "v2/integrations/permit"
- ]
- },
- {
- "group": "AI Assistants",
- "icon": "robot",
- "pages": [
- "v2/integrations/chatgpt",
- "v2/integrations/claude-code",
- "v2/integrations/claude-desktop",
- "v2/integrations/cursor",
- "v2/integrations/gemini-cli",
- "v2/integrations/mcp-json-configuration"
- ]
- },
- {
- "group": "AI SDKs",
- "icon": "code",
- "pages": [
- "v2/integrations/anthropic",
- "v2/integrations/gemini",
- "v2/integrations/openai"
- ]
- },
- {
- "group": "API Integration",
- "icon": "globe",
- "pages": [
- "v2/integrations/fastapi",
- "v2/integrations/openapi"
- ]
- }
- ]
- },
- {
- "group": "Patterns",
- "pages": [
- "v2/patterns/tool-transformation",
- "v2/patterns/decorating-methods",
- "v2/patterns/cli",
- "v2/patterns/contrib",
- "v2/patterns/testing"
- ]
- },
- {
- "group": "Development",
- "pages": [
- "v2/development/contributing",
- "v2/development/tests",
- "v2/development/releases",
- "v2/development/upgrade-guide",
- "v2/changelog"
- ]
- }
- ],
- "icon": "book"
- }
- ],
- "version": "v2.14.5"
+ "$ref": "./v3-navigation.json"
+ },
+ {
+ "$ref": "./v2-navigation.json"
}
]
},
"redirects": [
+ {
+ "destination": "/getting-started/whats-new",
+ "source": "/development/v4-notes/index"
+ },
+ {
+ "destination": "/getting-started/upgrading/from-fastmcp-3",
+ "source": "/development/v4-notes/change-register"
+ },
+ {
+ "destination": "/getting-started/whats-new",
+ "source": "/development/v4-notes/feature-program"
+ },
+ {
+ "destination": "/getting-started/whats-new",
+ "source": "/development/v4-notes/protocol-2026"
+ },
+ {
+ "destination": "/getting-started/upgrading/from-fastmcp-3",
+ "source": "/development/v4-notes/known-gaps"
+ },
+ {
+ "destination": "/servers/tasks",
+ "source": "/development/v4-notes/background-tasks"
+ },
+ {
+ "destination": "/apps/fastmcp-app",
+ "source": "/apps/interactive-apps"
+ },
+ {
+ "destination": "/apps/generative",
+ "source": "/apps/providers/generative"
+ },
+ {
+ "destination": "/apps/prefab",
+ "source": "/apps/patterns"
+ },
{
"destination": "/cli/overview",
"source": "/patterns/cli"
@@ -1026,12 +512,24 @@
"source": "/development/upgrade-guide"
},
{
- "destination": "/getting-started/upgrading/from-mcp-sdk",
+ "destination": "/getting-started/upgrading/from-mcp-sdk-v1",
"source": "/getting-started/upgrading-from-sdk"
},
{
- "destination": "/getting-started/upgrading/from-low-level-sdk",
+ "destination": "/getting-started/upgrading/from-mcp-sdk-v1",
+ "source": "/getting-started/upgrading/from-mcp-sdk"
+ },
+ {
+ "destination": "/getting-started/upgrading/from-low-level-sdk-v1",
"source": "/getting-started/low-level-sdk"
+ },
+ {
+ "destination": "/getting-started/upgrading/from-low-level-sdk-v1",
+ "source": "/getting-started/upgrading/from-low-level-sdk"
+ },
+ {
+ "destination": "/getting-started/upgrading/from-fastmcp-3",
+ "source": "/getting-started/upgrading/to-mcp-sdk-v2"
}
],
"search": {
@@ -1050,4 +548,4 @@
"appearance": "light",
"background": "/assets/brand/thumbnail-background-4.jpeg"
}
-}
\ No newline at end of file
+}
diff --git a/docs/fastmcp-analytics.js b/docs/fastmcp-analytics.js
new file mode 100644
index 000000000..07be00534
--- /dev/null
+++ b/docs/fastmcp-analytics.js
@@ -0,0 +1,257 @@
+(function () {
+ if (typeof window === "undefined") return;
+
+ // Public browser key for the shared Prefect Amplitude project.
+ // This is intentionally client-side; the secret key must never ship to the browser.
+ var AMPLITUDE_API_KEY = "c361ed56e7bdc1a48a38773c40120b39";
+ var AMPLITUDE_SCRIPT_URL =
+ "https://cdn.amplitude.com/libs/analytics-browser-2.8.1-min.js.gz";
+ var AMPLITUDE_SERVER_URL = "https://api2.amplitude.com/2/httpapi";
+ var PAGE_VIEW_EVENT = "Page View: FastMCP Docs";
+ var OUTBOUND_CLICK_EVENT = "Docs Outbound Clicked";
+ var SOURCE = "docs";
+ var SOURCE_DETAIL = "fastmcp";
+ var SURFACE = "fastmcp_docs";
+ var DEVICE_ID_PARAM = "deviceId";
+ var routeListenersInstalled = false;
+ var amplitudeInitialized = false;
+ var lastTrackedUrl = null;
+
+ var PREFECT_DESTINATION_HOSTNAMES = [
+ "www.prefect.io",
+ "prefect.io",
+ "horizon.prefect.io",
+ "app.prefect.cloud",
+ ];
+
+ var routeChangeCallbacks = [];
+
+ function loadScript(src, onload) {
+ var script = document.createElement("script");
+ script.src = src;
+ script.async = true;
+
+ if (typeof onload === "function") {
+ script.addEventListener("load", onload);
+ }
+
+ document.head.appendChild(script);
+ return script;
+ }
+
+ function getAmplitude() {
+ return window.amplitude || window.amplitudeAnalytics;
+ }
+
+ function normalizePathname(pathname) {
+ if (pathname === "/") return pathname;
+ return pathname.replace(/\/+$/, "");
+ }
+
+ function observeRouteChanges(callback) {
+ routeChangeCallbacks.push(callback);
+
+ if (!routeListenersInstalled) {
+ var fireCallbacks = function () {
+ routeChangeCallbacks.forEach(function (cb) {
+ window.setTimeout(cb, 0);
+ });
+ };
+
+ var wrapHistoryMethod = function (methodName) {
+ var original = window.history[methodName];
+ window.history[methodName] = function () {
+ var result = original.apply(this, arguments);
+ fireCallbacks();
+ return result;
+ };
+ };
+
+ wrapHistoryMethod("pushState");
+ wrapHistoryMethod("replaceState");
+ window.addEventListener("popstate", fireCallbacks);
+ window.addEventListener("hashchange", fireCallbacks);
+ routeListenersInstalled = true;
+ }
+
+ callback();
+ }
+
+ function buildPageViewProperties() {
+ return {
+ url: window.location.href,
+ title: document.title,
+ referrer: document.referrer || null,
+ path: normalizePathname(window.location.pathname),
+ source: SOURCE,
+ source_detail: SOURCE_DETAIL,
+ surface: SURFACE,
+ };
+ }
+
+ function trackPageView() {
+ var amplitude = getAmplitude();
+ if (!amplitude || typeof amplitude.track !== "function") {
+ return;
+ }
+
+ var url = window.location.href;
+ if (url === lastTrackedUrl) {
+ return;
+ }
+
+ amplitude.track(PAGE_VIEW_EVENT, buildPageViewProperties());
+ lastTrackedUrl = url;
+ }
+
+ function parseUrl(href) {
+ try {
+ return new URL(href, window.location.origin);
+ } catch (error) {
+ return null;
+ }
+ }
+
+ function isPrefectDestination(url) {
+ return PREFECT_DESTINATION_HOSTNAMES.indexOf(url.hostname) !== -1;
+ }
+
+ function addDeviceIdToLink(event) {
+ var amplitude = getAmplitude();
+ if (!amplitude || typeof amplitude.getDeviceId !== "function") {
+ return;
+ }
+
+ var link = event.currentTarget;
+ var href = link.getAttribute("href") || "";
+
+ var url = parseUrl(href);
+ if (!url || !isPrefectDestination(url)) {
+ return;
+ }
+
+ url.searchParams.set(DEVICE_ID_PARAM, amplitude.getDeviceId());
+ link.href = url.toString();
+ }
+
+ function removeDeviceIdFromLink(event) {
+ var link = event.currentTarget;
+ var href = link.getAttribute("href") || "";
+
+ var url = parseUrl(href);
+ if (!url || !isPrefectDestination(url)) {
+ return;
+ }
+
+ url.searchParams.delete(DEVICE_ID_PARAM);
+ link.href = url.toString();
+ }
+
+ function attachDeviceIdForwarding() {
+ var elements = document.querySelectorAll("a[href]");
+ elements.forEach(function (element) {
+ if (element.dataset.fastmcpDeviceIdBound === "true") {
+ return;
+ }
+
+ var url = parseUrl(element.getAttribute("href") || "");
+ if (!url || !isPrefectDestination(url)) {
+ return;
+ }
+
+ element.addEventListener("mouseenter", addDeviceIdToLink);
+ element.addEventListener("mouseleave", removeDeviceIdFromLink);
+ element.addEventListener("focus", addDeviceIdToLink);
+ element.addEventListener("blur", removeDeviceIdFromLink);
+ element.addEventListener("touchstart", addDeviceIdToLink);
+ element.addEventListener("touchcancel", removeDeviceIdFromLink);
+ element.dataset.fastmcpDeviceIdBound = "true";
+ });
+ }
+
+ function trackOutboundClick(event) {
+ var link = event.target && event.target.closest
+ ? event.target.closest("a[href]")
+ : null;
+
+ if (!link) {
+ return;
+ }
+
+ var href = link.getAttribute("href");
+ if (!href || href[0] === "#") {
+ return;
+ }
+
+ var destination;
+ destination = parseUrl(href);
+ if (!destination) {
+ return;
+ }
+
+ if (destination.hostname === window.location.hostname) {
+ return;
+ }
+
+ var amplitude = getAmplitude();
+ if (!amplitude || typeof amplitude.track !== "function") {
+ return;
+ }
+
+ amplitude.track(OUTBOUND_CLICK_EVENT, {
+ path: normalizePathname(window.location.pathname),
+ url: window.location.href,
+ title: document.title,
+ source: SOURCE,
+ source_detail: SOURCE_DETAIL,
+ surface: SURFACE,
+ destination: destination.href,
+ destination_domain: destination.hostname,
+ link_text: (link.textContent || "").trim().slice(0, 200),
+ is_prefect_destination: isPrefectDestination(destination),
+ });
+ }
+
+ function initializeAmplitude() {
+ var amplitude = getAmplitude();
+ if (
+ amplitudeInitialized ||
+ !amplitude ||
+ typeof amplitude.init !== "function"
+ ) {
+ return;
+ }
+
+ amplitude.init(AMPLITUDE_API_KEY, undefined, {
+ useBatch: true,
+ serverUrl: AMPLITUDE_SERVER_URL,
+ attribution: {
+ disabled: false,
+ trackNewCampaigns: true,
+ trackPageViews: true,
+ resetSessionOnNewCampaign: true,
+ },
+ defaultTracking: {
+ pageViews: false,
+ sessions: false,
+ formInteractions: true,
+ fileDownloads: true,
+ },
+ });
+
+ amplitudeInitialized = true;
+ observeRouteChanges(trackPageView);
+ observeRouteChanges(attachDeviceIdForwarding);
+ }
+
+ function initialize() {
+ document.addEventListener("click", trackOutboundClick, true);
+ loadScript(AMPLITUDE_SCRIPT_URL, initializeAmplitude);
+ }
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", initialize);
+ } else {
+ initialize();
+ }
+})();
diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx
index 20443337b..8c3167fb6 100644
--- a/docs/getting-started/installation.mdx
+++ b/docs/getting-started/installation.mdx
@@ -7,15 +7,19 @@ icon: arrow-down-to-line
We recommend using [uv](https://docs.astral.sh/uv/getting-started/installation/) to install and manage FastMCP.
+```bash
+uv add fastmcp
+```
+
+Or with pip:
+
```bash
pip install fastmcp
```
-Or with uv:
-
-```bash
-uv add fastmcp
-```
+
+**FastMCP 4 is in prerelease.** The commands above install the latest stable release, which is still 3.x. To get v4, pin the beta explicitly with `pip install "fastmcp==4.0.0b1"`, or see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease) for the uv constraint you'll need.
+
### Optional Dependencies
@@ -40,8 +44,8 @@ You should see output like the following:
```bash
$ fastmcp version
-FastMCP version: 3.0.0
-MCP version: 1.25.0
+FastMCP version: 4.0.0b1
+MCP version: 2.0.0
Python version: 3.12.2
Platform: macOS-15.3.1-arm64-arm-64bit
FastMCP root path: ~/Developer/fastmcp
@@ -62,19 +66,48 @@ Alternatively, wait for the stable v5 release. See [this issue](https://github.c
## Upgrading
+### From FastMCP 3.0
+
+Most FastMCP 3 servers run on 4 without changes. See [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) for the breaks that do exist, and [What's New](/getting-started/whats-new) for what the new version adds.
+
### From FastMCP 2.0
See the [Upgrade Guide](/getting-started/upgrading/from-fastmcp-2) for a complete list of breaking changes and migration steps.
### From the MCP SDK
-#### From FastMCP 1.0
+Which guide you want depends on which `mcp` version you're on and which of its two server APIs you used.
-If you're using FastMCP 1.0 via the `mcp` package (meaning you import FastMCP as `from mcp.server.fastmcp import FastMCP`), upgrading is straightforward — for most servers, it's a single import change. See the [full upgrade guide](/getting-started/upgrading/from-mcp-sdk) for details.
+#### From the high-level server
-#### From the Low-Level Server API
+If you're using FastMCP 1.0 via SDK v1 (meaning you import FastMCP as `from mcp.server.fastmcp import FastMCP`), upgrading is straightforward — for most servers it's a single import change. See [Upgrading from MCP SDK v1](/getting-started/upgrading/from-mcp-sdk-v1), which also explains why that route is usually easier than moving to MCP SDK v2.
-If you built your server directly on the `mcp` package's `Server` class — with `list_tools()`/`call_tool()` handlers and hand-written JSON Schema — see the [migration guide](/getting-started/upgrading/from-low-level-sdk) for a full walkthrough.
+If you already moved to SDK v2 and write against `MCPServer`, see [Upgrading from MCP SDK v2](/getting-started/upgrading/from-mcp-sdk-v2) — that migration is mostly renaming.
+
+#### From the low-level server
+
+If you built your server directly on the `mcp` package's `Server` class, the guide you want depends on how its handlers are registered. Decorators like `@server.list_tools()` mean SDK v1 — see [Upgrading from the Low-Level SDK v1](/getting-started/upgrading/from-low-level-sdk-v1). Handlers passed to the constructor as `on_list_tools=` mean SDK v2 — see [Upgrading from the Low-Level SDK v2](/getting-started/upgrading/from-low-level-sdk-v2).
+
+## Troubleshooting
+
+### `import fastmcp` fails after a pip upgrade
+
+This affects one specific case: upgrading to FastMCP 3.3 or later from FastMCP 3.2 or earlier with `pip`. Fresh installs and `uv` upgrades are unaffected, so you can skip this unless you did exactly that.
+
+If `import fastmcp` raises `ModuleNotFoundError`, or `from fastmcp import FastMCP` raises `ImportError`, immediately after the upgrade, your install is in a half-removed state. Reinstall in a single step:
+
+```bash
+pip install --force-reinstall fastmcp
+```
+
+If that doesn't resolve it, remove both distributions and reinstall from a clean state:
+
+```bash
+pip uninstall -y fastmcp fastmcp-slim
+pip install fastmcp
+```
+
+FastMCP 3.3 moved the importable code from the `fastmcp` distribution into `fastmcp-slim`. During a single-command `pip` upgrade, pip can install the new files and then delete them while uninstalling the old `fastmcp` distribution, whose file manifest still lists those paths. `uv` uninstalls before it installs, so it is unaffected.
## Versioning Policy
@@ -82,16 +115,12 @@ FastMCP follows semantic versioning with pragmatic adaptations for the rapidly e
For production use, always pin to exact versions:
```
-fastmcp==3.0.0 # Good
-fastmcp>=3.0.0 # Bad - may install breaking changes
+fastmcp==4.0.0b1 # Good - an exact version
+fastmcp>=4.0.0 # Bad - may install breaking changes
```
See the full [versioning and release policy](/development/releases#versioning-policy) for details on our public API, deprecation practices, and breaking change philosophy.
## Contributing to FastMCP
-Interested in contributing to FastMCP? See the [Contributing Guide](/development/contributing) for details on:
-- Setting up your development environment
-- Running tests and pre-commit hooks
-- Submitting issues and pull requests
-- Code standards and review process
+The [Contributing Guide](/development/contributing) covers setting up a development environment, running the test suite and pre-commit hooks, and the standards we hold contributed code to.
diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx
index 3f240cec1..79c4599a3 100644
--- a/docs/getting-started/quickstart.mdx
+++ b/docs/getting-started/quickstart.mdx
@@ -3,7 +3,7 @@ title: Quickstart
icon: rocket-launch
---
-Welcome! This guide will help you quickly set up FastMCP, run your first MCP server, and deploy a server to Prefect Horizon.
+This guide builds a working MCP server from scratch: a tool, a way to run it, a client that calls it, and a visual UI for the result. It ends with the server deployed and reachable over the internet.
If you haven't already installed FastMCP, follow the [installation instructions](/getting-started/installation).
@@ -112,14 +112,41 @@ async def call_tool(name: str):
asyncio.run(call_tool("Ford"))
```
-Note that:
-- FastMCP clients are asynchronous, so we need to use `asyncio.run` to run the client
-- We must enter a client context (`async with client:`) before using the client
-- You can make multiple client calls within the same context
+FastMCP clients are asynchronous, so the call goes through `asyncio.run`. Entering the client context with `async with client:` is what opens the connection, and it stays open for as many calls as you want to make inside the block.
-## Deploy to Prefect Horizon
+## Give Your Tool a UI
-[Prefect Horizon](https://horizon.prefect.io) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides managed hosting, authentication, access control, and observability for MCP servers.
+Tools normally return text, but any tool can return an interactive UI instead. Add `app=True` to your tool decorator and return a [Prefab](https://prefab.prefect.io) component — the host renders it as a chart, table, form, or any other visual element right in the conversation. This requires the `apps` extra (`pip install "fastmcp[apps]"`).
+
+The `app=True` flag tells FastMCP to wire up the renderer and protocol metadata automatically. The tool still works like any other MCP tool — it receives arguments and returns a result — but the result is a component tree that the host displays visually instead of as plain text.
+
+```python my_server.py
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column, Heading, Text, Badge, Row
+from fastmcp import FastMCP
+
+mcp = FastMCP("My MCP Server")
+
+
+@mcp.tool(app=True)
+def greet(name: str) -> PrefabApp:
+ """Greet someone with a visual card."""
+ with Column(gap=4, css_class="p-6") as view:
+ Heading(f"Hello, {name}!")
+ with Row(gap=2, align="center"):
+ Text("Status")
+ Badge("Greeted", variant="success")
+
+ return PrefabApp(view=view)
+```
+
+You can preview app tools locally with `fastmcp dev apps my_server.py` — no MCP host required. See the [Apps overview](/apps/overview) for the full guide, including state management, forms, charts, and server-connected interactivity.
+
+## Deploy Your Server
+
+FastMCP HTTP servers run anywhere you can host a Python application. The [HTTP deployment guide](/deployment/http) covers the transport settings and security boundaries for self-managed infrastructure.
+
+For a managed deployment, [Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides hosting, authentication, access control, and observability for MCP servers.
Horizon is **free for personal projects** and offers enterprise governance for teams.
@@ -128,7 +155,7 @@ Horizon is **free for personal projects** and offers enterprise governance for t
To deploy your server, you'll need a [GitHub account](https://github.com). Once you have one, you can deploy your server in three steps:
1. Push your `my_server.py` file to a GitHub repository
-2. Sign in to [Prefect Horizon](https://horizon.prefect.io) with your GitHub account
+2. Sign in to [Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) with your GitHub account
3. Create a new project from your repository and enter `my_server.py:mcp` as the server entrypoint
That's it! Horizon will build and deploy your server, making it available at a URL like `https://your-project.fastmcp.app/mcp`. You can chat with it to test its functionality, or connect to it from any LLM client that supports the MCP protocol.
diff --git a/docs/getting-started/upgrading/from-fastmcp-2.mdx b/docs/getting-started/upgrading/from-fastmcp-2.mdx
index 47baa5655..c371f09f6 100644
--- a/docs/getting-started/upgrading/from-fastmcp-2.mdx
+++ b/docs/getting-started/upgrading/from-fastmcp-2.mdx
@@ -1,11 +1,15 @@
---
title: Upgrading from FastMCP 2
sidebarTitle: "From FastMCP 2"
-description: Migration instructions for upgrading between FastMCP versions
+description: What changed in FastMCP 3 for servers written against FastMCP 2
icon: up
---
-This guide covers breaking changes and migration steps when upgrading FastMCP.
+This guide covers the breaking changes a FastMCP 2 server meets on its way to FastMCP 3, newest release first.
+
+
+**Going all the way to FastMCP 4?** You need this page and [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3), in that order. The two describe different transitions: this one covers the v3 API changes, while the FastMCP 3 guide covers the MCP Python SDK v2 rebuild underneath v4. Where a v3 deprecation was later removed outright, this page marks it **Removed in v4**.
+
## v3.0.0
@@ -21,7 +25,7 @@ pip install --upgrade fastmcp
uv add --upgrade fastmcp
```
-If you pin versions in a requirements file or `pyproject.toml`, update your pin to `fastmcp>=3.0.0,<4`.
+If you pin versions in a requirements file or `pyproject.toml`, update your pin to `fastmcp>=3.0.0,<4`. Going on to FastMCP 4 is a second hop: finish this page, then work through [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) and move the pin to `fastmcp>=4.0.0` at the end of it.
**New repository home.** As part of the v3 release, FastMCP's GitHub repository has moved from `jlowin/fastmcp` to [`PrefectHQ/fastmcp`](https://github.com/PrefectHQ/fastmcp) under [Prefect](https://prefect.io)'s stewardship. GitHub automatically redirects existing clones and bookmarks, so nothing breaks — but you can update your local remote whenever convenient:
@@ -68,20 +72,20 @@ BREAKING CHANGES (will crash at import or runtime):
6. WSTRANSPORT: Removed. Use StreamableHttpTransport.
-7. OPENAPI: timeout parameter removed from OpenAPIProvider. Set timeout on the httpx.AsyncClient instead.
+7. OPENAPI: timeout parameter removed from OpenAPIProvider. Set timeout on the httpx2.AsyncClient instead.
8. METADATA: Namespace changed from "_fastmcp" to "fastmcp" in tool.meta. The include_fastmcp_meta parameter is removed (always included).
9. ENV VAR: FASTMCP_SHOW_CLI_BANNER renamed to FASTMCP_SHOW_SERVER_BANNER.
10. DECORATORS: @mcp.tool, @mcp.resource, @mcp.prompt now return the original function, not a component object. Code that accesses .name, .description, or other component attributes on the decorated result will crash with AttributeError.
- Fix: set FASTMCP_DECORATOR_MODE=object for v2 compat (itself deprecated).
+ Fix: access component objects via the server (e.g. await mcp.get_tool("name")) instead of the decorated function. The FASTMCP_DECORATOR_MODE=object escape hatch that existed in v3 was removed in FastMCP 4.0.
-11. OAUTH STORAGE: Default OAuth client storage changed from DiskStore to FileTreeStore due to pickle deserialization vulnerability in diskcache (CVE-2025-69872). Clients using default storage will re-register automatically on first connection. If using DiskStore explicitly, switch to FileTreeStore or add pip install 'py-key-value-aio[disk]'.
+11. OAUTH STORAGE: Default OAuth client storage changed from DiskStore to FileTreeStore due to pickle deserialization vulnerability in diskcache (CVE-2025-69872). Clients using default storage will re-register automatically on first connection. If using DiskStore explicitly, switch to FileTreeStore (with key/collection sanitization strategies) or add pip install 'py-key-value-aio[disk]'.
12. REPO MOVE: GitHub repository moved from jlowin/fastmcp to PrefectHQ/fastmcp. Update git remotes and dependency URLs that reference the old location.
-13. BACKGROUND TASKS: FastMCP's background task system (SEP-1686) is now an optional dependency. If the code uses task=True or TaskConfig, add pip install "fastmcp[tasks]".
+13. BACKGROUND TASKS: FastMCP's background task system is now an optional dependency. If the code uses task=True or TaskConfig, add pip install "fastmcp[tasks]".
DEPRECATIONS (still work but emit warnings):
@@ -101,7 +105,7 @@ For each issue found, show the original line, explain why it breaks, and provide
In v2, you could configure transport settings directly in the `FastMCP()` constructor. In v3, `FastMCP()` is purely about your server's identity and behavior — transport configuration happens when you actually start serving. Passing any of the old kwargs now raises `TypeError` with a migration hint.
-```python
+```python test="skip"
# Before
mcp = FastMCP("server", host="0.0.0.0", port=8080)
mcp.run()
@@ -126,7 +130,11 @@ The default OAuth client storage has moved from `DiskStore` to `FileTreeStore` t
If you were using the default storage (i.e., not passing an explicit `client_storage`), clients will need to re-register on their first connection after upgrading. This happens automatically — no user action required, and it's the same flow that already occurs whenever a server restarts with in-memory storage.
-If you were passing a `DiskStore` explicitly, you can either [switch to `FileTreeStore`](/servers/storage-backends) (recommended) or keep using `DiskStore` by adding the dependency yourself:
+If you were passing a `DiskStore` explicitly, you can either [switch to `FileTreeStore`](/servers/storage-backends) (recommended) or keep using `DiskStore` by adding the dependency yourself.
+
+
+When switching to `FileTreeStore`, you **must** configure key and collection sanitization strategies. Without them, keys containing special characters (such as URL-based OAuth client IDs) will cause filesystem errors. See the [File Storage](/servers/storage-backends#file-storage) section for the recommended setup.
+
Keeping `DiskStore` requires `pip install 'py-key-value-aio[disk]'`, which re-introduces the vulnerable `diskcache` package into your dependency tree.
@@ -136,7 +144,7 @@ Keeping `DiskStore` requires `pip install 'py-key-value-aio[disk]'`, which re-in
In v2, you could enable or disable individual components by calling methods on the component object itself. In v3, visibility is controlled through the server (or provider), which lets you target components by name, tag, or type without needing a reference to the object:
-```python
+```python test="skip"
# Before
tool = await server.get_tool("my_tool")
tool.disable()
@@ -151,7 +159,7 @@ Calling `.enable()` or `.disable()` on a component object now raises `NotImpleme
The `get_tools()`, `get_resources()`, `get_prompts()`, and `get_resource_templates()` methods have been renamed to `list_tools()`, `list_resources()`, `list_prompts()`, and `list_resource_templates()`. More importantly, they now return lists instead of dicts — so code that indexes by name needs to change:
-```python
+```python test="skip"
# Before
tools = await server.get_tools()
tool = tools["my_tool"]
@@ -165,7 +173,7 @@ tool = next((t for t in tools if t.name == "my_tool"), None)
Prompt functions now use FastMCP's `Message` class instead of `mcp.types.PromptMessage`. The new class is simpler — it accepts a plain string and defaults to `role="user"`, so most prompts become one-liners:
-```python
+```python test="skip"
# Before
from mcp.types import PromptMessage, TextContent
@@ -183,7 +191,7 @@ def my_prompt() -> Message:
If your prompt functions return raw dicts with `role` and `content` keys, those also need to change. v2 silently coerced dicts into prompt messages, but v3 requires typed `Message` objects (or plain strings for single user messages):
-```python
+```python test="skip"
# Before (v2 accepted this)
@mcp.prompt
def my_prompt():
@@ -207,7 +215,7 @@ def my_prompt() -> list[Message]:
`ctx.set_state()` and `ctx.get_state()` are now async because state in v3 is session-scoped and backed by a pluggable storage backend (rather than a simple dict). This means state persists across multiple tool calls within the same session:
-```python
+```python test="skip"
# Before
ctx.set_state("key", "value")
value = ctx.get_state("key")
@@ -219,7 +227,7 @@ value = await ctx.get_state("key")
State values must also be JSON-serializable by default (dicts, lists, strings, numbers, etc.). If you need to store non-serializable values like an HTTP client, pass `serializable=False` — these values are request-scoped and only available during the current tool call:
-```python
+```python test="skip"
await ctx.set_state("client", my_http_client, serializable=False)
```
@@ -241,7 +249,7 @@ parent.mount(child, namespace="child")
In v2, auth providers like `GitHubProvider` could auto-load configuration from environment variables with a `FASTMCP_SERVER_AUTH_*` prefix. This magic has been removed — pass values explicitly:
-```python
+```python test="skip"
# Before (v2) — client_id and client_secret loaded automatically
# from FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID, etc.
auth = GitHubProvider()
@@ -260,7 +268,7 @@ auth = GitHubProvider(
The deprecated WebSocket client transport has been removed. Use `StreamableHttpTransport` instead:
-```python
+```python test="skip"
# Before
from fastmcp.client.transports import WSTransport
transport = WSTransport("ws://localhost:8000/ws")
@@ -272,14 +280,14 @@ transport = StreamableHttpTransport("http://localhost:8000/mcp")
**OpenAPI `timeout` parameter removed**
-`OpenAPIProvider` no longer accepts a `timeout` parameter. Configure timeout on the httpx client directly. The `client` parameter is also now optional — when omitted, a default client is created from the spec's `servers` URL with a 30-second timeout:
+`OpenAPIProvider` no longer accepts a `timeout` parameter. Configure timeout on the httpx2 client directly. The `client` parameter is also now optional — when omitted, a default client is created from the spec's `servers` URL with a 30-second timeout:
-```python
+```python test="skip"
# Before
provider = OpenAPIProvider(spec, client, timeout=60)
# After
-client = httpx.AsyncClient(base_url="https://api.example.com", timeout=60)
+client = httpx2.AsyncClient(base_url="https://api.example.com", timeout=60)
provider = OpenAPIProvider(spec, client)
```
@@ -287,7 +295,7 @@ provider = OpenAPIProvider(spec, client)
The FastMCP metadata key in component `meta` dicts changed from `_fastmcp` to `fastmcp`. If you read metadata from tool or resource objects, update the key:
-```python
+```python test="skip"
# Before
tags = tool.meta.get("_fastmcp", {}).get("tags", [])
@@ -305,7 +313,7 @@ Metadata is now always included — the `include_fastmcp_meta` parameter has bee
In v2, `@mcp.tool` transformed your function into a `FunctionTool` object. In v3, decorators return your original function unchanged — which means decorated functions stay callable for testing, reuse, and composition:
-```python
+```python test="skip"
@mcp.tool
def greet(name: str) -> str:
return f"Hello, {name}!"
@@ -313,11 +321,11 @@ def greet(name: str) -> str:
greet("World") # Works! Returns "Hello, World!"
```
-If you have code that treats the decorated result as a `FunctionTool` (e.g., accessing `.name` or `.description`), set `FASTMCP_DECORATOR_MODE=object` for v2 compatibility. This escape hatch is itself deprecated and will be removed in a future release.
+If you have code that treats the decorated result as a `FunctionTool` (e.g., accessing `.name` or `.description`), the v2-compatible object-returning behavior was available in v3 via `FASTMCP_DECORATOR_MODE=object`. That escape hatch was removed in FastMCP 4.0 — decorators always return the original function now.
**Background tasks require optional dependency**
-FastMCP's background task system (SEP-1686) is now behind an optional extra. If your server uses background tasks, install with:
+FastMCP's background task system is now behind an optional extra. If your server uses background tasks, install with:
```bash
pip install "fastmcp[tasks]"
@@ -327,22 +335,22 @@ Without the extra, configuring a tool with `task=True` or `TaskConfig` will rais
### Deprecated Features
-These still work but emit warnings. Update when convenient.
+These were deprecated in v3. Items marked **Removed in v4** no longer work at all — update to the replacement shown. The rest still work but emit warnings; update when convenient.
-**mount() prefix → namespace**
+**mount() prefix → namespace** (Removed in v4)
-```python
-# Deprecated
+```python test="skip"
+# Removed in v4
main.mount(subserver, prefix="api")
# New
main.mount(subserver, namespace="api")
```
-**import_server() → mount()**
+**import_server() → mount()** (Removed in v4)
-```python
-# Deprecated
+```python test="skip"
+# Removed in v4
main.import_server(subserver)
# New
@@ -351,10 +359,10 @@ main.mount(subserver)
**Module import paths for proxy and OpenAPI**
-The proxy and OpenAPI modules have moved under `providers` to reflect v3's provider-based architecture:
+The proxy and OpenAPI modules moved under `providers` to reflect v3's provider-based architecture. The old `fastmcp.server.proxy` and `fastmcp.server.openapi` compatibility shims were **removed in 4.0** — import from the `providers` location instead:
-```python
-# Deprecated
+```python test="skip"
+# Removed in 4.0
from fastmcp.server.proxy import FastMCPProxy
from fastmcp.server.openapi import FastMCPOpenAPI
@@ -363,10 +371,10 @@ from fastmcp.server.providers.proxy import FastMCPProxy
from fastmcp.server.providers.openapi import OpenAPIProvider
```
-`FastMCPOpenAPI` itself is deprecated — use `FastMCP` with an `OpenAPIProvider` instead:
+`FastMCPOpenAPI` was **removed in 4.0** — use `FastMCP` with an `OpenAPIProvider` instead:
-```python
-# Deprecated
+```python test="skip"
+# Removed in 4.0
from fastmcp.server.openapi import FastMCPOpenAPI
server = FastMCPOpenAPI(spec, client)
@@ -376,10 +384,10 @@ from fastmcp.server.providers.openapi import OpenAPIProvider
server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)])
```
-**add_tool_transformation() → add_transform()**
+**add_tool_transformation() → add_transform()** (Removed in v4)
-```python
-# Deprecated
+```python test="skip"
+# Removed in v4
mcp.add_tool_transformation("name", config)
# New
@@ -387,39 +395,51 @@ from fastmcp.server.transforms import ToolTransform
mcp.add_transform(ToolTransform({"name": config}))
```
-**FastMCP.as_proxy() → create_proxy()**
+**FastMCP.as_proxy() → create_proxy()** (Removed in v4)
-```python
-# Deprecated
+The proxy target is passed positionally in both APIs, so most calls migrate unchanged. If you passed the target by keyword, note that the parameter was renamed from `backend=` to `target=`.
+
+```python test="skip"
+# Removed in v4
proxy = FastMCP.as_proxy("http://example.com/mcp")
+proxy = FastMCP.as_proxy(backend="http://example.com/mcp") # keyword form
# New
from fastmcp.server import create_proxy
proxy = create_proxy("http://example.com/mcp")
+proxy = create_proxy(target="http://example.com/mcp") # as_proxy(backend=X) → create_proxy(target=X)
```
## v2.14.0
### OpenAPI Parser Promotion
-The experimental OpenAPI parser is now standard. Update imports:
+The experimental OpenAPI parser is now standard. The `fastmcp.experimental.server.openapi` and `fastmcp.server.openapi` shims were both **removed in 4.0** — use `FastMCP` with an `OpenAPIProvider` instead:
-```python
+```python test="skip"
# Before
from fastmcp.experimental.server.openapi import FastMCPOpenAPI
-# After
-from fastmcp.server.openapi import FastMCPOpenAPI
+# After (removed in 4.0 — use OpenAPIProvider)
+from fastmcp import FastMCP
+from fastmcp.server.providers.openapi import OpenAPIProvider
+server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)])
```
### Removed Deprecated Features
-- `BearerAuthProvider` → use `JWTVerifier`
-- `Context.get_http_request()` → use `get_http_request()` from dependencies
-- `from fastmcp import Image` → use `from fastmcp.utilities.types import Image`
-- `FastMCP(dependencies=[...])` → use `fastmcp.json` configuration
-- `FastMCPProxy(client=...)` → use `client_factory=lambda: ...`
-- `output_schema=False` → use `output_schema=None`
+A batch of long-deprecated surfaces came out in 2.14. Each fails loudly at import or call time, and each has a direct replacement:
+
+| Removed | Replacement |
+|---|---|
+| `BearerAuthProvider` | `JWTVerifier` — the same JWT validation under a name that says what it does |
+| `Context.get_http_request()` | `get_http_request()` from [dependency injection](/servers/dependency-injection) |
+| `from fastmcp import Image` | `from fastmcp.utilities.types import Image` |
+| `FastMCP(dependencies=[...])` | a [`fastmcp.json`](/deployment/server-configuration) configuration file |
+| `FastMCPProxy(client=...)` | `client_factory=lambda: ...` |
+| `output_schema=False` | `output_schema=None` |
+
+Two of these are worth understanding rather than just swapping. `FastMCPProxy` takes a factory instead of a client because a single shared client cannot serve concurrent proxied sessions safely — the factory gives each session its own backend connection. And `output_schema=False` became `output_schema=None` because `False` read as "this tool has a schema, and it is false"; `None` says plainly that there is no schema.
## v2.13.0
@@ -427,7 +447,7 @@ from fastmcp.server.openapi import FastMCPOpenAPI
The OAuth proxy now issues its own JWT tokens. For production, provide explicit keys:
-```python
+```python test="skip"
auth = GitHubProvider(
client_id=os.environ["GITHUB_CLIENT_ID"],
client_secret=os.environ["GITHUB_CLIENT_SECRET"],
diff --git a/docs/getting-started/upgrading/from-fastmcp-3.mdx b/docs/getting-started/upgrading/from-fastmcp-3.mdx
new file mode 100644
index 000000000..1cd484680
--- /dev/null
+++ b/docs/getting-started/upgrading/from-fastmcp-3.mdx
@@ -0,0 +1,455 @@
+---
+title: Upgrading from FastMCP 3
+sidebarTitle: "From FastMCP 3"
+description: What changes when you upgrade to FastMCP 4, which builds on the MCP Python SDK v2
+icon: up
+---
+
+FastMCP 4 builds on the MCP Python SDK v2, and that is the source of every change in this guide. The SDK v2 makes two sweeping changes to the protocol layer: it moves the protocol types into a standalone `mcp_types` package (still importable as `mcp.types`), and it renames every model field from camelCase to snake_case in Python (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, `isError` → `is_error`, and so on). The wire format does not change: the models keep their camelCase aliases and serialize under them, so this renames the attributes your code reads, not the JSON on the connection.
+
+FastMCP 4 absorbs almost all of this for you. Field access is bridged so your existing reads keep working, and the imports you were taught have a stable home in FastMCP itself. What the SDK cannot hide is the protocol's own direction: the new sessionless era removes the server's ability to call back into a client mid-request, and background tasks moved out of the core spec into an extension. Those two shape the changes a working server is most likely to feel.
+
+The sections below cover what FastMCP handles for you, the changes you must make in your own code, the surfaces removed outright in 4.0, the behavior shifts that compile fine but act differently, and the deprecation timeline for the compatibility shims.
+
+## Install the v4 Prerelease
+
+While FastMCP 4 is in prerelease, pin the beta explicitly. The `fastmcp` package is a thin wrapper that depends on `fastmcp-slim` at the same version, so asking for a prerelease of one means asking for a prerelease of the other. pip infers that on its own:
+
+```bash
+pip install "fastmcp==4.0.0b1"
+```
+
+uv is stricter: it allows prereleases only for packages you name, and `fastmcp-slim` arrives transitively. Constrain it alongside the requirement in `pyproject.toml`:
+
+```toml
+[project]
+dependencies = ["fastmcp==4.0.0b1"]
+
+[tool.uv]
+constraint-dependencies = ["fastmcp-slim==4.0.0b1"]
+```
+
+Then run `uv lock` or `uv sync` normally. Naming the one package keeps the rest of your graph on stable releases, where `--prerelease allow` would opt every dependency into prereleases. The MCP SDK needs no constraint at all now that it ships stable releases — pinning `mcp==2.0.0b2` here would in fact break the resolution, since a prerelease does not satisfy FastMCP's own `mcp>=2.0.0` requirement.
+
+
+You are upgrading an MCP server or client from FastMCP 3.x to FastMCP 4, which is built on the MCP Python SDK v2.
+
+FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-fastmcp-3 — it explains every item below, with the replacement code. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs.
+
+Then search the provided code for each signal below. Most FastMCP 3 servers upgrade untouched, so report only what you actually find.
+
+ENVIRONMENT
+- a pydantic pin below 2.12
+- a FastAPI pin below 0.133.0, the first release admitting Starlette 1.x (earlier ones cap it, e.g. 0.115.12 requires `starlette<0.47.0`), or any direct Starlette pin below 1.0.1
+
+IMPORTS THAT NO LONGER RESOLVE
+- `fastmcp.server.proxy`, `fastmcp.server.openapi`, `FastMCPOpenAPI`
+- `fastmcp.experimental.server.openapi`, `fastmcp.experimental.utilities.openapi`
+- `fastmcp.experimental.sampling.handlers`
+- `fastmcp.server.apps`, `fastmcp.server.app`
+- `fastmcp.tools.tool`, `fastmcp.resources.resource`, `fastmcp.prompts.prompt`
+- `fastmcp.server.tasks`, `fastmcp.server.sampling`
+- `fastmcp.server.auth.authorization`
+- `CurrentDocket` or `CurrentWorker` from `fastmcp.dependencies`
+- `SkillsProvider`
+- `CachableToolResult`, `CachablePromptResult`, and their siblings (the misspelling was corrected with no alias)
+- `PromptToolMiddleware`, `ResourceToolMiddleware`
+
+REMOVED SERVER METHODS AND KEYWORDS
+- `FastMCP.as_proxy(...)`
+- `import_server(...)` ← flag this one loudly: `mount()` is the replacement but NOT an equivalent. `import_server` took a static snapshot and skipped the child's lifespan and middleware; `mount` is a live composition that runs both.
+- `mount(prefix=...)`, `mount(as_proxy=...)`
+- `add_tool_transformation(...)`, `remove_tool_transformation(...)`
+- `remove_tool(...)` ← its replacement raises KeyError where this raised NotFoundError, so check surrounding except clauses
+- tool `serializer=`, tool `exclude_args=`
+- `StreamableHttpTransport(sse_read_timeout=...)`
+- `FASTMCP_DECORATOR_MODE` / `settings.decorator_mode`
+- `FastMCP(sampling_handler=...)`, `sampling_handler_behavior=`
+
+REMOVED CONTEXT METHODS
+- `ctx.sample(...)`, `ctx.sample_step(...)`, `ctx.list_roots(...)`
+- Note for the user: if borrowing the CALLER's model is the whole point of the server, the guide's recommendation is to stay on FastMCP 3.x rather than migrate.
+- The client side is NOT affected — `Client(sampling_handler=...)` and `Client(roots=...)` still mean what they meant.
+
+RUNTIME BREAKS THAT STILL COMPILE — the ones most likely to reach production
+- `ctx.elicit(...)` anywhere. It is era-gated in 4.0 and raises on modern connections, which is what `Client` now negotiates by default. This is the single most likely runtime failure.
+- `ctx.elicit(...)` called without `response_type`
+- `except httpx.` around any FastMCP call. FastMCP raises httpx2 exceptions now, but httpx is usually still installed transitively, so the handler imports, type-checks, and silently never matches.
+- a custom `httpx.AsyncClient`, `httpx_client_factory=`, or `httpx.Auth` handed to a FastMCP transport, `OAuth`, or `from_openapi`
+- `Middleware.on_initialize` hooks, and `ctx.set_state` values read back in a later call — neither survives a modern connection
+- middleware assuming `on_message` only sees routable requests
+- camelCase field reads (`inputSchema`, `isError`, `mimeType`, `nextCursor`, `structuredContent`, `serverInfo`, and the rest) — these still work but warn, and are scheduled for removal
+- clients matching on the resource-not-found error code -32002
+- templated resources whose parameters legitimately carry `..` or absolute paths
+- an OAuth server (`OAuthProxy` or anything built on it) with `issuer_url` set to something other than `base_url` — this forces a one-time re-authorization of every client
+
+BACKGROUND TASKS
+- `@mcp.tool(task=True)` or `TaskConfig` without `mcp.add_extension(TasksExtension())`
+- `task=` on a `@mcp.resource` or `@mcp.prompt` decorator (tools only now)
+- `client.call_tool(..., task=True)`, `read_resource(task=True)`, `get_prompt(task=True)`
+
+ERRORS
+- `McpError(ErrorData(...))` positional construction. Catching and `err.error.code` are unchanged; only construction moved.
+
+For each item found, show the original line, name what changed, and give the corrected code from the guide. Where you could not confirm a replacement in the docs, say so instead of guessing.
+
+
+## Environment Requirements
+
+The SDK v2 raises FastMCP's dependency floors, which matters before any of your code runs.
+
+**pydantic >= 2.12 is now the floor.** If your project pins an older pydantic (for example `pydantic==2.11.*`), installing this FastMCP release fails with an unsatisfiable-resolution error from your installer — bump your pin to `>=2.12` first. If you don't pin pydantic at all, installers upgrade it silently as part of the FastMCP upgrade.
+
+**The server extra floors Starlette >= 1.0.1.** This is the requirement most likely to force an unrelated upgrade, because FastAPI pinned Starlette to a sub-1.0 range for a long time — FastAPI 0.115.12, for example, requires `starlette<0.47.0`. **FastAPI 0.133.0 is the first release that admits Starlette 1.x**, so a project pinned below that gets an unsatisfiable resolution rather than a version bump. Raise your FastAPI pin to `>=0.133.0` before upgrading FastMCP. Mounting a FastMCP server inside a FastAPI app is otherwise unaffected — verified against FastAPI 0.135.2 on Starlette 1.3.1.
+
+## What FastMCP Absorbs
+
+### camelCase Field Access
+
+Objects that FastMCP hands back to you — the results of `client.list_tools()`, `client.call_tool_mcp()`, `client.read_resource()`, and the parameter objects passed to your sampling and elicitation handlers — are SDK v2 objects with snake_case fields. FastMCP installs a compatibility bridge at import time that routes the old camelCase names to their new snake_case fields, so code written against FastMCP 2.x still reads correctly:
+
+```python
+from fastmcp import Client
+
+
+async def read_schema():
+ async with Client("my_mcp_server.py") as client:
+ tools = await client.list_tools()
+ return tools[0].inputSchema # still works, warns once
+```
+
+Each bridged read emits a `FastMCPDeprecationWarning` pointing you at the snake_case name (`tools[0].input_schema` here). The bridge covers the fields users actually read: `inputSchema`/`outputSchema` on tools; `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint` on tool annotations; `mimeType` on resources and content; `isError`/`structuredContent` on tool results; `nextCursor` on paginated results; `serverInfo`/`protocolVersion` on the initialize result; the sampling parameter fields (`systemPrompt`, `maxTokens`, `stopSequences`, `modelPreferences`, `toolChoice`); and `requestedSchema` on elicitation parameters.
+
+The bridge is controlled by the `mcp_camelcase_compat` setting, which defaults to on. Set it to `False` (or the environment variable `FASTMCP_MCP_CAMELCASE_COMPAT=false`) to turn the shims off, in which case only the snake_case names resolve:
+
+```python
+import fastmcp
+
+fastmcp.settings.mcp_camelcase_compat = False
+```
+
+See [Settings](/more/settings) for the full reference.
+
+### Protocol Types
+
+Every protocol type — `TextContent`, `ImageContent`, `Tool`, `ErrorData`, `Icon`, `PromptMessage`, `SamplingMessage`, `ToolAnnotations`, notification and request wrapper types like `ToolListChangedNotification`, and everything else — now lives in a standalone `mcp_types` package. The SDK re-exports that package as `mcp.types`, so existing imports keep working and stay the preferred spelling:
+
+```python
+from mcp.types import TextContent, Tool, ToolAnnotations
+```
+
+Both names resolve to the same objects, so `from mcp_types import X` is equally valid — useful if you depend on the types without the rest of the SDK. What did change is the fields on those types: they are snake_case now (`input_schema`, not `inputSchema`), which the [compatibility bridge](#legacy-camelcase-field-access-keeps-working) covers for the objects FastMCP hands you.
+
+`fastmcp.types` still exists, but holds only types FastMCP defines itself (currently just `Textarea`, used to render a multiline textarea in form-based UIs) — it does not re-export protocol types.
+
+### The `McpError` Alias
+
+`fastmcp.exceptions.McpError` is an alias of the SDK's `MCPError`. Catching errors is unchanged — `except McpError` still catches SDK-raised errors, and reading `err.error.code` still works:
+
+```python
+from fastmcp.exceptions import McpError
+
+try:
+ ...
+except McpError as err:
+ print(err.error.code)
+```
+
+### Preserved Behavior
+
+A few client behaviors that touch the SDK are preserved so you don't have to change anything:
+
+- `Client(timeout=...)` accepts both a `timedelta` and a plain float number of seconds, as before.
+- `client.ping()` returns a `bool`.
+- `client.transport.get_session_id()` returns `None` on protocol eras that have no session, rather than raising. (The SDK v2 removed session-id access from its streamable HTTP transport; FastMCP reconstructs it on the transport object.)
+
+## What You Must Change
+
+Everything above, FastMCP handled for you. What remains lives in your own code, where FastMCP can't reach it — how you construct errors, the custom HTTP clients you hand to a transport, and any place you reach past FastMCP's surfaces into the raw SDK objects. Each surfaces as a clear failure at import or call time, and each is a mechanical fix.
+
+**`McpError` construction.** The v1 pattern of wrapping an `ErrorData` and passing it positionally fails under SDK v2 with:
+
+```
+TypeError: MCPError.__init__() missing 1 required positional argument: 'message'
+```
+
+Note the message prints the class as `MCPError` (uppercase) even though your code wrote `McpError` — the old name is an alias for the SDK's renamed class. Construct the error with keyword arguments instead:
+
+```python test="skip"
+from fastmcp.exceptions import McpError
+
+# Before (raises TypeError under SDK v2):
+# raise McpError(ErrorData(code=-32000, message="Client not supported"))
+
+# After:
+raise McpError(code=-32000, message="Client not supported")
+```
+
+Catching and `err.error.code` are unchanged — only construction moved.
+
+**Raw session access sees v2 objects.** If you reach past FastMCP's client and server surfaces into `client.session`, `ctx.session`, or the internals of `ctx.request_context`, you're now holding raw SDK v2 objects with snake_case fields and the v2 method signatures. FastMCP does not wrap these; code that depends on their v1 shape needs updating.
+
+**FastMCP now uses httpx2 exclusively.** FastMCP has replaced `httpx` with [httpx2](https://pypi.org/project/httpx2/), a next-generation httpx fork, across its entire HTTP stack — client transports and every server-side path (auth providers, the OpenAPI integration, the version check). `httpx` is no longer a FastMCP dependency. If you pass a custom client or factory into a FastMCP client transport — `StreamableHttpTransport(httpx_client_factory=...)`, `SSETransport(httpx_client_factory=...)`, `OAuth(httpx_client_factory=...)`, or a custom `httpx.Auth` as `Client(auth=...)` — those objects must now be httpx2. httpx2 is a drop-in fork with the same public API, so the change is an import swap:
+
+```python test="skip"
+# Before
+import httpx
+
+transport = StreamableHttpTransport(
+ "https://example.com/mcp",
+ httpx_client_factory=lambda **kwargs: httpx.AsyncClient(verify=False, **kwargs),
+)
+
+# After
+import httpx2
+
+transport = StreamableHttpTransport(
+ "https://example.com/mcp",
+ httpx_client_factory=lambda **kwargs: httpx2.AsyncClient(verify=False, **kwargs),
+)
+```
+
+The `client` you pass to `FastMCP.from_openapi(client=...)` (and `OpenAPIProvider(client=...)`) should now be an `httpx2.AsyncClient`. Existing `httpx.AsyncClient` instances remain temporarily accepted via duck typing, but emit a `FastMCPDeprecationWarning` and will be rejected in a future release. HTTP made inside your own tools is entirely yours and is unaffected.
+
+**The subtlest break is exception handlers, and no type checker will catch it.** `httpx` very likely remains installed in your environment (the Anthropic, OpenAI, and Google SDKs all depend on it), so code that catches old-httpx exceptions around FastMCP calls still imports and still type-checks — it just never matches, because FastMCP now raises `httpx2` exceptions. The handler silently becomes dead code:
+
+```python
+import httpx # still installed transitively — this import works
+
+
+async def fetch(client, url):
+ try:
+ return await client.call_tool("fetch", {"url": url})
+ except httpx.ConnectError: # dead code: FastMCP now raises httpx2.ConnectError
+ return fallback()
+```
+
+Grep your codebase for `except httpx.` and move those handlers to `httpx2`. The exception hierarchies match name-for-name, so the fix is an import swap — the hard part is remembering to look. One place you are covered automatically: exceptions raised *inside your tools and resources* (for example, a tool whose own old-httpx call gets a 429) are still mapped to `ToolError`/`ResourceError` by FastMCP's error boundary, which recognizes both libraries' exceptions during the transition.
+
+Two runtime behaviors shift with httpx2, and because the switch is now wholesale they apply to **all** FastMCP HTTP — including server-auth upstream calls, not just the client path. TLS verification uses the operating system's trust store (via `truststore`, honoring `SSL_CERT_FILE`/`SSL_CERT_DIR`) instead of the bundled certifi CA set, so corporate-CA or certifi-pinned setups may verify differently. And the FastMCP HTTP loggers are renamed from `httpx`/`httpcore.*` to `httpx2`/`httpcore2.*` — update any logging filters that select the HTTP stack by logger name.
+
+## Removed in FastMCP 4
+
+Deprecations that warned throughout the 3.x line are removed in 4.0. Unlike the bridged changes above, these fail immediately at the call site — a `ModuleNotFoundError`, `ImportError`, `AttributeError`, or `TypeError` — so nothing degrades silently. Every one has a direct replacement, and the fix is mechanical.
+
+### Moved Imports
+
+The proxy, OpenAPI, and app integrations moved to their permanent homes, and the internal component classes are no longer re-exported from their old aliases:
+
+| Removed import | Replacement |
+| --- | --- |
+| `fastmcp.server.proxy` | `fastmcp.server.providers.proxy` |
+| `fastmcp.server.openapi` (and `FastMCPOpenAPI`) | `FastMCP` with an `OpenAPIProvider` from `fastmcp.server.providers.openapi` |
+| `fastmcp.experimental.server.openapi` | `fastmcp.server.providers.openapi` |
+| `fastmcp.experimental.utilities.openapi` | `fastmcp.utilities.openapi` |
+| `fastmcp.server.apps`, `fastmcp.server.app` | `fastmcp.apps` (e.g. `AppConfig`) or `fastmcp` (`FastMCPApp`) |
+| `Tool` / `ToolResult` from `fastmcp.tools.tool` | `fastmcp.tools` |
+| `Resource` from `fastmcp.resources.resource` | `fastmcp.resources` |
+| `Prompt` / `Message` from `fastmcp.prompts.prompt` | `fastmcp.prompts` |
+| `FunctionTool` / `ParsedFunction` / `tool` from `fastmcp.tools.tool` | `fastmcp.tools.function_tool` |
+| `FunctionResource` / `resource` from `fastmcp.resources.resource` | `fastmcp.resources.function_resource` |
+| `FunctionPrompt` / `prompt` from `fastmcp.prompts.prompt` | `fastmcp.prompts.function_prompt` |
+| `OpenAISamplingHandler` from `fastmcp.experimental.sampling.handlers` | `fastmcp.client.sampling.handlers.openai` |
+| `AuthCheck` / `AuthContext` / `require_scopes` / `require_roles` / `restrict_tag` / `run_auth_checks` from `fastmcp.server.auth.authorization` | `fastmcp.server.auth` |
+| `run_auth_checks_with_shortfall` / `scope_requirements` from `fastmcp.server.auth.authorization` | `fastmcp.utilities.authorization` |
+| `SkillsProvider` | `SkillsDirectoryProvider` from `fastmcp.server.providers.skills` |
+| `TaskConfig` from `fastmcp.server.tasks` | `fastmcp.utilities.tasks` |
+| `CurrentDocket` / `CurrentWorker` from `fastmcp.dependencies` | `fastmcp_tasks.dependencies` |
+| `fastmcp.server.sampling` (and `SamplingTool`) | removed with [server-side sampling](#protocol-version-support) |
+
+Two renames in the same family are worth calling out because they have no compatibility alias. The response-caching wrapper models lost a spelling typo — `CachableToolResult`, `CachablePromptResult`, and their siblings became `CacheableToolResult`, `CacheablePromptResult`, etc. — so an import of the old spelling from `fastmcp.server.middleware.caching` raises `ImportError`. And `PromptToolMiddleware` / `ResourceToolMiddleware` are gone in favor of the `PromptsAsTools` / `ResourcesAsTools` transforms from `fastmcp.server.transforms` (the `ToolInjectionMiddleware` base class is retained).
+
+### Removed Server Methods
+
+These `FastMCP` methods and keywords have warned since 3.0 and are now removed:
+
+| Removed | Replacement |
+| --- | --- |
+| `FastMCP.as_proxy(sub)` | `create_proxy(sub)` (from `fastmcp.server`) |
+| `mcp.import_server(sub)` | `mcp.mount(sub)` |
+| `mcp.mount(sub, prefix="x")` | `mcp.mount(sub, namespace="x")` |
+| `mcp.mount(sub, as_proxy=True)` | wrap with `create_proxy(sub)`, then `mount` the proxy |
+| `mcp.add_tool_transformation(name, cfg)` | `mcp.add_transform(ToolTransform({name: cfg}))` |
+| `mcp.remove_tool_transformation(name)` | removed (was a no-op); hide tools with `mcp.disable(keys=[...])` |
+| `mcp.remove_tool(name)` | `mcp.local_provider.remove_tool(name)` |
+
+Two of these replacements are not exact behavioral swaps. `create_proxy` takes its target as the first positional argument (`target`), so a keyword call like `as_proxy(backend=server)` becomes `create_proxy(server)` rather than reusing the old keyword. And `local_provider.remove_tool` raises a plain `KeyError` when the tool is missing, where `FastMCP.remove_tool` raised a `NotFoundError` — update any `except NotFoundError` cleanup around a removal.
+
+`mount(as_proxy=True)` used to route the child through a proxy (an MCP-client execution boundary) rather than composing it directly. To keep that boundary, wrap the child in `create_proxy()` and mount the proxy; a plain `mount(child)` composes the child in-process. Either way, the child's lifespan and middleware now run — a direct mount no longer skips them.
+
+`import_server` → `mount` is the one row here that is not a mechanical swap, because the two never had the same semantics. `import_server` took a **one-time static snapshot** — it copied the child's tools, resources, and prompts at call time, with no live link, and did not run the child's lifespan or middleware. `mount` is a **live composition** — it holds a live link to the child and runs the child's lifespan and middleware. After switching, later changes to the child become visible through the parent, the child's lifespan runs with the parent's (entered when the server starts, held until it stops — not per request), and the child's middleware runs on the operations delegated to it. If you depended on the frozen-copy behavior (a stable snapshot, no child lifecycle), there is no drop-in replacement: register the child's components on the parent directly instead of composing the two servers.
+
+### Removed Parameters
+
+Several parameters and settings that warned in 3.x are gone:
+
+- **Tool `serializer=`** is removed from `@tool` / `mcp.tool()`, `Tool.from_function`, `Tool.from_tool`, and the OpenAPI tool. Return a `ToolResult` from your tool for full control over serialization instead.
+- **Tool `exclude_args=`** is removed. Hide a parameter from the tool schema by injecting it instead: give it a `Depends(factory)` default (from `fastmcp.dependencies`), where `factory` is a callable returning the value the argument used to carry. An injected parameter never appears in the tool's schema, which is what `exclude_args` was for.
+- **The `decorator_mode` setting** (`FASTMCP_DECORATOR_MODE`) and its `"object"` mode are removed. Decorators always return your original function with metadata attached; reach the component object through the server (`await mcp.get_tool("name")`) rather than off the decorated function.
+- **`StreamableHttpTransport(sse_read_timeout=...)`** is removed — it was a no-op under the SDK v2 client. Set the read timeout through the public `Client(transport, timeout=...)` (a `timedelta` or float seconds), or reach for a custom `httpx_client_factory` when you need finer control. (`SSETransport` still accepts `sse_read_timeout`.)
+- **`ctx.elicit()` now requires `response_type`.** Omitting it (or passing `None`) has warned since 3.2 and now raises `TypeError`. The empty-object schema it produced gave clients nothing to render, and some showed an empty, non-functional form. Pass a type describing what you expect back — `bool` is the right answer for a confirmation:
+
+ ```python test="skip"
+ # Before
+ result = await ctx.elicit("Approve this action?")
+
+ # After
+ result = await ctx.elicit("Approve this action?", response_type=bool)
+ ```
+
+ This is the server-authoring API only. Client elicitation handlers still receive `response_type=None` for URL requests and for empty schemas sent by other servers — that contract is unchanged.
+
+### Background Tasks
+
+Background tasks left the core MCP spec during the SDK v2 rebuild and came back as the `io.modelcontextprotocol/tasks` extension (SEP-2663). FastMCP follows the protocol: what was a built-in server feature in 3.x is now a registered extension, and the authoring surface changed on both sides of the connection.
+
+The extension ships in a separate package, so the pin from [Install the v4 Prerelease](#install-the-v4-prerelease) needs one more entry before any of this imports:
+
+```toml
+[project]
+dependencies = ["fastmcp[tasks]==4.0.0b1"]
+
+[tool.uv]
+constraint-dependencies = [
+ "fastmcp-slim==4.0.0b1",
+ "fastmcp-tasks==4.0.0b1",
+ "mcp==2.0.0b2",
+ "mcp-types==2.0.0b2",
+]
+```
+
+On the server, `task=True` still marks a tool as capable of running in the background, but it no longer runs anything by itself — the extension does. Register it, or the server refuses to start:
+
+```python
+from fastmcp import FastMCP
+from fastmcp_tasks import TasksExtension
+
+mcp = FastMCP("MyServer")
+mcp.add_extension(TasksExtension())
+
+@mcp.tool(task=True)
+async def slow_computation(duration: int) -> str:
+ """A long-running operation."""
+ return "done"
+```
+
+Without the registration, a `task=True` tool raises at startup rather than the first time a client calls the tool:
+
+```
+RuntimeError: Task-enabled tools (slow_computation) require the tasks extension,
+but no extension with identifier 'io.modelcontextprotocol/tasks' is registered.
+```
+
+`TaskConfig` moved from `fastmcp.server.tasks` to `fastmcp.utilities.tasks`, and the `CurrentDocket` and `CurrentWorker` dependencies moved to `fastmcp_tasks.dependencies`.
+
+`task=` is now a tool-only keyword. FastMCP 3 accepted it on resource, resource-template, and prompt decorators as well; passing it to `@mcp.resource` or `@mcp.prompt` now raises `TypeError`, and there is no replacement — the extension tasks tool calls only.
+
+The client API changed shape entirely. In 3.x you opted a single call into background execution with `task=True` and got a handle back. In 4.0 `call_tool` handles a tasked call transparently: if the server runs the call in the background, the client polls it to completion and returns the same result a synchronous call would have produced.
+
+```python
+import fastmcp_tasks # noqa: F401 — importing anywhere enables client task support
+from fastmcp import Client
+
+
+async def run(server):
+ async with Client(server) as client:
+ return await client.call_tool("slow_computation", {"duration": 10})
+```
+
+When you want the handle — to do other work while the task runs, check on it, or cancel it — `call_tool_task` returns one immediately:
+
+```python
+from fastmcp import Client
+from fastmcp_tasks import call_tool_task
+
+
+async def run(server):
+ async with Client(server) as client:
+ task = await call_tool_task(client, "slow_computation", {"duration": 10})
+ return await task.result()
+```
+
+Three things follow from this. `client.call_tool(name, args, task=True)` raises `TypeError`, as do `read_resource(task=True)` and `get_prompt(task=True)` — and those last two have no replacement. Client task support requires `fastmcp_tasks` to be imported somewhere in the process, since that import is what makes a `Client` advertise the capability. And tasks are negotiated only on modern connections, so a `mode="legacy"` client never gets them. See [Background Tasks](/servers/tasks) for the full picture.
+
+## Behavior Changes
+
+These changes compile fine and can surface at runtime. The first is the one most likely to bite a working 3.x server.
+
+**`ctx.elicit()` no longer reaches a default client.** Elicitation is era-gated in 4.0: `ctx.elicit()` works on handshake-era connections (≤ 2025-11-25) and raises on the modern `2026-07-28` protocol, which has no back-channel for a running tool to push a request down. Because `fastmcp.Client` now defaults to `mode="auto"`, an ordinary client negotiates the modern era against a FastMCP server — so a tool that elicited happily in 3.x now fails the call:
+
+```
+ToolError: elicitation via server-initiated requests is unavailable on 2026-07-28 connections.
+```
+
+The gate is strict in both directions, which is what makes it debuggable: a guard tool that returns an input request on a handshake connection raises the mirror-image error rather than misbehaving quietly. You have three ways forward. Rewrite the tool as a guard tool that *returns* a description of the input it needs, which is the form that works on modern connections. Branch on `ctx.request_context.protocol_version` and keep both paths if you serve both eras. Or keep this server's clients on the handshake era with `Client(server, mode="legacy")`, which leaves `ctx.elicit()` working as written. See [Elicitation](/servers/elicitation#which-approach-to-use) for the two shapes side by side.
+
+**Middleware sees traffic it never saw before.** Dispatch now begins in the SDK's middleware layer, the single point every inbound message passes through, so `on_message`, `on_request`, and `on_notification` observe *every* message a client sends — including `notifications/cancelled`, `notifications/initialized`, and `notifications/progress`, and including requests that fail before reaching a handler, such as an unknown method or a `tools/call` whose params fail validation. In 3.x those never reached your hooks. Middleware that assumed every message it saw was a routable request, or that counted messages to measure tool traffic, needs a guard on the message type. The operation hooks (`on_call_tool`, `on_list_tools`, and the rest) are unaffected: they still fire exactly once per request and still receive typed component results. See [What middleware sees](/servers/middleware#what-middleware-sees).
+
+**Templated resources are path-screened by default.** Every templated resource now has its extracted parameter values checked for path-traversal (`..` segments), absolute paths, and null bytes *before your handler runs*, at the server's read chokepoint. A rejected read returns a non-leaky "resource not found" error. Only a standalone `..` segment counts as traversal, so values that merely contain dots (`file.tar.gz`, `HEAD~3..HEAD`) and dotfiles (`.env`) still pass. If a template legitimately accepts `..`-bearing or absolute values, exempt the parameter with `ResourceSecurity(exempt_params={...})`, disable the check per-component with `security=None`, or set a server-wide default with `FastMCP(resource_security=...)`. See [Resources → Path Security](/servers/resources#path-security).
+
+**Resource-not-found now returns `-32602`.** The wire error code for a missing resource from the core `resources/read` handler changed from `-32002` to `-32602` (`INVALID_PARAMS`, per SEP-2164). The human-readable message ("Resource not found: ...") is unchanged, so this only affects clients that matched on the numeric code — update those to expect `-32602`. (The opt-in `ErrorHandlingMiddleware` keeps its own per-method-prefix code mapping; if you run it with `transform_errors=True` it can still map not-found to a different code, so it is unaffected by this change.)
+
+**An OAuth server whose `issuer_url` differs from its `base_url` re-authorizes its clients once.** `issuer_url` exists so a server's OAuth identity can differ from the URL its endpoints are mounted at — the usual case being a server under `/api` whose discovery lives at the host root. It now supplies the `issuer` in the authorization server metadata, the `iss` claim on every token the server mints, and the RFC 9207 `iss` on authorization responses; `base_url` still supplies `authorization_endpoint`, `token_endpoint`, and the rest, because that is where the routes are actually mounted. Both values previously came from `base_url`, which published an `issuer` contradicting the URL the client had just performed discovery at — a document RFC 8414 §3.3 requires a strict client to reject.
+
+The cost of the correction is the `iss` on tokens already in the wild, so it falls on the providers that mint their own tokens — `OAuthProxy` and everything built on it. Access *and* refresh tokens carry the claim, and the verifier compares it exactly, so clients cannot refresh their way across the upgrade; it is a one-time full re-authorization. Interactive clients re-prompt and recover on their own, while a headless deployment holding a long-lived refresh token needs someone to re-authorize it. Plan the upgrade for a window where that is acceptable. If an identity provider mints SEP-990 ID-JAG assertions for this server, repoint their `aud` at the new issuer too — unless you pin the expected value with `IdentityAssertion(audience=...)`, which overrides the issuer and keeps working untouched.
+
+Servers that leave `issuer_url` unset, or set it to the same value as `base_url`, are unaffected. It defaults to `base_url`, and the metadata and minted `iss` are byte-identical to what 3.x produced.
+
+## Deprecation Timeline
+
+The camelCase bridge is a migration aid, not a permanent fixture. It works today and warns on every bridged read so you can find and update the affected call sites. Plan to migrate your reads to snake_case: the shims will be removed in a future release, after which only the snake_case names resolve — the same state you get today by setting `mcp_camelcase_compat = False`. Turning the setting off is a good way to surface every remaining camelCase read in your code as a hard `AttributeError` before the shims go away.
+
+## SDK Deprecation Warnings
+
+Ordinary use of `ctx.info` (client logging) emits an SDK-level `MCPDeprecationWarning`:
+
+```
+The logging capability is deprecated as of 2026-07-28 (SEP-2577)
+```
+
+The warning comes from the MCP SDK, not from FastMCP, and it is benign. `ctx.info` and the rest of the logging methods keep working on every era, including the modern one — a log message is a *notification*, which rides the response stream the caller already opened. The SDK is signaling the protocol's direction for the capability declaration, not the notification itself.
+
+## Protocol Version Support
+
+FastMCP servers built on the SDK v2 serve multiple protocol eras from the same server. The SDK negotiates the era each client speaks: the sessionless `2026-07-28` era (which discovers capabilities through `server/discover`) and earlier session-based handshake versions are all handled simultaneously. This formally supersedes FastMCP's earlier "latest protocol only" stance — a single server now works with clients across the protocol transition.
+
+**`ctx.sample()`, `ctx.sample_step()`, and `ctx.list_roots()` are gone from `Context`**, along with the `sampling_handler=` and `sampling_handler_behavior=` arguments to `FastMCP()`. Touching a removed method raises `AttributeError` on every era, and `FastMCP(sampling_handler=...)` raises a `TypeError` naming the migration, so the break surfaces when you upgrade rather than in production against whichever client happens to negotiate the modern era.
+
+All three *pushed*: the server sent a request down a live back-channel and blocked for the answer, and the sessionless protocol has no such channel. Since `fastmcp.Client` now negotiates the modern protocol by default, a method like that would fail against a default client. What the protocol removed is the pushing, not the asking — sampling, elicitation, and roots all still reach the client through the [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol), where a tool *returns* an `InputRequiredResult` describing what it needs, the client answers, and it calls again with the answer attached.
+
+Migrating differs by capability. For **roots**, the guard pattern is the direct replacement: a server asks once and has what it needs, so the extra round buys the whole answer, and taking the paths as tool arguments is simpler still when the caller can just supply them. For **sampling**, the guard route works the same way, but generation usually belongs in your server, because every round is a full request-response cycle and a generation loop pays that cost repeatedly. [Call an LLM from your server](/servers/sampling) with your own API key and your tool behaves the same for every client, including the many that never implemented sampling; reach for the guard route when the point is specifically to use the caller's model. If borrowing the caller's model *is* your server — you hold no key of your own, and the token bill was never yours to pay — staying on FastMCP 3.x is the honest answer until that changes.
+
+| Context feature | Earlier eras (session-based) | `2026-07-28` (sessionless) |
+| --- | --- | --- |
+| `ctx.info` / logging notifications | Supported | Supported |
+| Tools, resources, prompts, completions | Supported | Supported |
+| `ctx.elicit` | Supported | Raises — use the guard pattern (return `InputRequiredResult`) |
+| `ctx.sample` / `ctx.sample_step` | Method removed — call an LLM server-side | Method removed — call an LLM server-side, or ask via the guard pattern |
+| `ctx.list_roots` | Method removed — take paths as tool arguments | Method removed — ask via the guard pattern, or take paths as tool arguments |
+| `client.set_logging_level()` | Supported | Raises — `logging/setLevel` needs session state the era lacks |
+| `Middleware.on_initialize` | Runs on connect | Never runs — there is no `initialize` handshake |
+| Session state (`ctx.set_state` across calls) | Persists for the session | Does not persist — every request is a fresh connection |
+| Background tasks (`task=True`) | Runs synchronously — never tasked | Supported via the tasks extension |
+
+Several of these bite by default now, because **`fastmcp.Client` defaults to `mode="auto"`** in v4 — an ordinary `Client(server)` negotiates the newest protocol both sides share, which against a FastMCP server is the sessionless `2026-07-28` era. On that era there is no `initialize` handshake, so a `Middleware.on_initialize` hook never runs; each request is a fresh connection, so state written with `ctx.set_state` in one call is not visible in the next; and a tool that calls [`ctx.elicit()`](#behavior-changes) raises. A server that gates access in `on_initialize`, relies on per-session state, or elicits mid-tool must keep its clients on the session-based era. The control is per-client: `Client(server, mode="legacy")`. There is no server-side setting that restricts which protocol versions a server offers, so a server whose behavior depends on the handshake era depends on its callers opting into it — which is only practical when you control them. If you don't, port the behavior instead: a guard tool for elicitation, [session state](/servers/sessions) for what `ctx.set_state` held, and per-request auth checks for what `on_initialize` gated.
+
+The client side is unaffected. `sampling_handler=` and `roots=` mean what they always did — see [client sampling](/clients/sampling) and [client roots](/clients/roots) — and one registration serves both routes, since a handshake-era server's pushed request and a modern server's returned one dispatch to the same handler.
+
+## Upgrade Checklist
+
+Most servers upgrade untouched. Work down this list to find the ones that don't:
+
+1. **Bump your environment.** Raise any pin below `pydantic>=2.12`; upgrade FastAPI if your resolver complains about Starlette `<1.0.1`.
+2. **Fix imports that moved out.** `from mcp.types import X` still works, but update any import from the [removed modules](#moved-imports) (`fastmcp.server.proxy`, `fastmcp.server.openapi`, `fastmcp.server.apps`, the `fastmcp.tools.tool` / `resources.resource` / `prompts.prompt` component shims).
+3. **Update removed server APIs.** Swap `as_proxy` → `create_proxy`, `import_server` → `mount`, `mount(prefix=)` → `mount(namespace=)`, and the [other removed methods and keywords](#removed-server-methods).
+4. **Replace `ctx.sample` and `ctx.list_roots`.** Both are gone from `Context`, as are `FastMCP(sampling_handler=...)` and `sampling_handler_behavior=`. Call an LLM directly from your server for generation; ask for roots through the guard pattern, or take file paths as tool arguments. A server whose purpose is to use the caller's model should stay on FastMCP 3.x rather than migrate.
+5. **Find every `ctx.elicit()` call.** It raises on modern connections, which is what a default client now negotiates. Rewrite the tool as a guard tool, branch on `ctx.request_context.protocol_version`, or keep its clients on `mode="legacy"` — see [the era gate](#behavior-changes).
+6. **Register the tasks extension.** A `task=True` tool needs `mcp.add_extension(TasksExtension())` or the server won't start. Drop `task=` from resource and prompt decorators, move `TaskConfig` to `fastmcp.utilities.tasks`, and replace client-side `call_tool(..., task=True)` with plain `call_tool` or `call_tool_task`.
+7. **Update removed tool parameters.** Replace tool `serializer=` (return a `ToolResult`), `exclude_args=` (use `Depends()`), and `StreamableHttpTransport(sse_read_timeout=)`.
+8. **Fix `McpError` construction.** Positional `McpError(ErrorData(...))` becomes keyword `McpError(code=..., message=...)`. Catching is unchanged.
+9. **Move httpx to httpx2.** Grep for `except httpx.` and for custom `httpx_client_factory` / `httpx.Auth` objects handed to FastMCP, and swap the import to `httpx2`.
+10. **Decide the client era.** `Client` now defaults to `mode="auto"`. If a server relies on `on_initialize`, per-session state, or `ctx.elicit()`, keep its clients on `mode="legacy"`, or port the behavior forward — there is no server-side protocol-version restriction.
+11. **Verify behavior changes.** Confirm templated resources that legitimately accept `..` or absolute paths are exempted, guard any middleware that now sees notifications and unroutable requests, update any client that matched the old `-32002` resource-not-found code, and if your server mints its own OAuth tokens (`OAuthProxy` and the providers built on it) under an `issuer_url` that differs from its `base_url`, schedule the [one-time re-authorization](#behavior-changes) its clients now need.
+12. **Run with the camelCase bridge off.** Set `mcp_camelcase_compat = False` (or `FASTMCP_MCP_CAMELCASE_COMPAT=false`) in CI to surface every remaining camelCase read as a hard `AttributeError` before the shims are removed.
+
+The executable version of this checklist lives in [`tests/test_upgrade_from_v3.py`](https://github.com/PrefectHQ/fastmcp/blob/main/tests/test_upgrade_from_v3.py): it builds representative 3.x-style servers and asserts they run unchanged, and pins every removed surface to the exact error it now raises.
diff --git a/docs/getting-started/upgrading/from-low-level-sdk-v1.mdx b/docs/getting-started/upgrading/from-low-level-sdk-v1.mdx
new file mode 100644
index 000000000..35f5412bb
--- /dev/null
+++ b/docs/getting-started/upgrading/from-low-level-sdk-v1.mdx
@@ -0,0 +1,623 @@
+---
+title: Upgrading from the Low-Level SDK v1
+sidebarTitle: "From Low-Level SDK v1"
+description: Upgrade your MCP server from v1 of the low-level Python SDK's Server class to FastMCP
+icon: up
+---
+
+If you've been building MCP servers directly on the `mcp` package's `Server` class — writing `list_tools()` and `call_tool()` handlers, hand-crafting JSON Schema dicts, and wiring up transport boilerplate — this guide is for you. FastMCP replaces all of that machinery with a declarative, Pythonic API where your functions *are* the protocol surface.
+
+The core idea: instead of telling the SDK what your tools look like and then separately implementing them, you write ordinary Python functions and let FastMCP derive the protocol layer from your code. Type hints become JSON Schema. Docstrings become descriptions. Return values are serialized automatically. The plumbing you wrote to satisfy the protocol just disappears.
+
+## The SDK v2 Transition
+
+MCP SDK v2 is a substantial, deliberate modernization of the protocol layer. Protocol types moved into a standalone `mcp_types` package, wire fields moved from camelCase to snake_case, and the low-level `Server` was rebuilt so handlers are passed to the constructor as `on_*` callables taking `(ctx, params)` rather than registered with decorators. A v1 server meets that change the moment its environment resolves `mcp` to v2:
+
+```
+AttributeError: 'Server' object has no attribute 'list_tools'
+```
+
+Often nobody chose that moment. An unpinned `mcp` dependency, a fresh lockfile, or a rebuilt container picks up the new major version. Nothing is wrong with your code, and nothing is wrong with the SDK — major versions are exactly where a change like this belongs. Your build just crossed it earlier than you planned to.
+
+Pinning the SDK back restores the decorator API immediately, with no code changes, and buys you time to choose deliberately:
+
+```bash
+pip install "mcp<2"
+```
+
+## Two Upgrade Paths
+
+Both directions are reasonable, and the choice is about which code you'd rather maintain.
+
+**Porting the low-level `Server` to SDK v2** keeps you in direct control of the protocol surface, which is the point of the low-level API and the right call for some servers. The work is real: your imports, every handler signature, every handler's return type, and your error construction all move.
+
+**Adopting FastMCP** is what the rest of this page walks through. What makes it less work is not that FastMCP is better — it's that the code most affected by the SDK v2 changes is precisely the code FastMCP doesn't ask you to write. Your `list_tools`/`call_tool` pair, hand-written JSON Schema, and content-block wrappers aren't ported to new signatures; they're deleted, and FastMCP derives all of it from your function signatures instead. FastMCP 4 runs on MCP SDK v2 underneath, so both paths land you on the same modern protocol layer.
+
+
+Already on SDK v2's rebuilt `Server` class, with constructor-registered `on_*` handlers? See [Upgrading from the Low-Level SDK v2](/getting-started/upgrading/from-low-level-sdk-v2) instead — the before-and-after code is different enough to warrant its own guide.
+
+Using FastMCP 1.0 via `from mcp.server.fastmcp import FastMCP`? Your upgrade is a single import — see [Upgrading from MCP SDK v1](/getting-started/upgrading/from-mcp-sdk-v1).
+
+
+
+You are rewriting an MCP server built on v1 of the `mcp` package's low-level `Server` class (`mcp.server.Server` or `mcp.server.lowlevel.server.Server`, with decorator-registered handlers) using FastMCP 4's high-level API.
+
+FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v1 — it explains every item below, with before-and-after code for each handler group. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs.
+
+Then work through the provided code. This is a rewrite, not a patch: most of what you find gets deleted rather than translated.
+
+CONSTRUCTION AND TRANSPORT
+- `Server("name")`
+- `async with stdio_server() as (r, w): await server.run(r, w, server.create_initialization_options())`
+- `SseServerTransport` / `StreamableHTTPSessionManager` and any Starlette wiring around them
+- `asyncio.run(main())` boilerplate
+- `lifespan=` — carries over directly: pass the same async context manager to `FastMCP(lifespan=...)`, and read what it yields from `ctx.lifespan_context` in any tool. Do not drop it — the tools that depended on it (a DB connection, a client pool) lose their dependency silently if you do.
+
+HANDLERS TO DELETE (each becomes one or more decorated functions)
+- `@server.list_tools()` + `@server.call_tool()` — note the `if name == ...` dispatch chain inside call_tool; each branch becomes its own `@mcp.tool`
+- `@server.list_resources()` + `@server.list_resource_templates()` + `@server.read_resource()` — note any manual URI parsing, which the `{placeholder}` syntax replaces
+- `@server.list_prompts()` + `@server.get_prompt()`
+- any other `@server.*()` handler in the file — completion, resource subscribe/unsubscribe, logging level, progress. Look these up in the FastMCP docs rather than assuming a decorator name maps one-to-one.
+
+TYPES THAT DISAPPEAR FROM YOUR CODE
+- hand-written `inputSchema` JSON Schema dicts — these come from type hints now
+- `types.Tool`, `types.Resource`, `types.ResourceTemplate`, `types.Prompt`, `types.PromptArgument`
+- `types.TextContent` wrappers around return values — return plain Python values instead
+- `types.ImageContent`, `types.EmbeddedResource`
+- `types.PromptMessage`, `types.GetPromptResult`
+- Note that in the SDK v2 that FastMCP 4 builds on, `mcp.types` aliases the standalone `mcp_types` package; the import path still works, but the fields are snake_case now.
+
+CONTEXT AND SIDE CHANNELS
+- `server.request_context`
+- `session.send_log_message(...)`, `session.send_progress_notification(...)`
+- direct session use for anything else — a FastMCP `Context` has a `ctx.session` property returning the underlying SDK session, so this still works; prefer a `Context` method where one exists, and note the remaining uses as SDK-coupled
+
+ERRORS
+- `raise ValueError(f"Unknown tool: ...")` and other dispatch fallbacks — these become unnecessary
+- `McpError` construction and any error-code mapping
+
+For each item found, show the original code, say what it did, and give the FastMCP equivalent. Where several handlers collapse into one decorated function, show the collapse rather than a line-by-line mapping. Call out anything you could not find a documented FastMCP replacement for instead of inventing one.
+
+
+## Install
+
+FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
+
+```bash
+pip install "fastmcp==4.0.0b1"
+# or
+uv add "fastmcp==4.0.0b1"
+```
+
+An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
+
+FastMCP depends on the `mcp` package, so the SDK stays installed. FastMCP 4 builds on SDK v2, where the protocol types live in a standalone `mcp_types` package that stays importable as `mcp.types`. Most of your `mcp.types` imports disappear entirely in the rewrite below, since FastMCP derives the protocol types from your function signatures.
+
+## Server and Transport
+
+The `Server` class requires you to choose a transport, connect streams, build initialization options, and run an event loop. FastMCP collapses all of that into a constructor and a `run()` call.
+
+
+
+```python Before test="skip"
+import asyncio
+from mcp.server import Server
+from mcp.server.stdio import stdio_server
+
+server = Server("my-server")
+
+# ... register handlers ...
+
+async def main():
+ async with stdio_server() as (read_stream, write_stream):
+ await server.run(
+ read_stream,
+ write_stream,
+ server.create_initialization_options(),
+ )
+
+asyncio.run(main())
+```
+
+```python After
+from fastmcp import FastMCP
+
+mcp = FastMCP("my-server")
+
+# ... register tools, resources, prompts ...
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+
+
+Need HTTP instead of stdio? With the `Server` class, you'd wire up Starlette routes and `SseServerTransport` or `StreamableHTTPSessionManager`. With FastMCP:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("my-server")
+
+if __name__ == "__main__":
+ mcp.run(transport="http", host="0.0.0.0", port=8000)
+```
+
+## Tools
+
+This is where the difference is most dramatic. The `Server` class requires two handlers — one to describe your tools (with hand-written JSON Schema) and another to dispatch calls by name. FastMCP eliminates both by deriving everything from your function signature.
+
+
+
+```python Before test="skip"
+import mcp.types as types
+from mcp.server import Server
+
+server = Server("math")
+
+@server.list_tools()
+async def list_tools() -> list[types.Tool]:
+ return [
+ types.Tool(
+ name="add",
+ description="Add two numbers",
+ inputSchema={
+ "type": "object",
+ "properties": {
+ "a": {"type": "number"},
+ "b": {"type": "number"},
+ },
+ "required": ["a", "b"],
+ },
+ ),
+ types.Tool(
+ name="multiply",
+ description="Multiply two numbers",
+ inputSchema={
+ "type": "object",
+ "properties": {
+ "a": {"type": "number"},
+ "b": {"type": "number"},
+ },
+ "required": ["a", "b"],
+ },
+ ),
+ ]
+
+@server.call_tool()
+async def call_tool(
+ name: str, arguments: dict
+) -> list[types.TextContent]:
+ if name == "add":
+ result = arguments["a"] + arguments["b"]
+ return [types.TextContent(type="text", text=str(result))]
+ elif name == "multiply":
+ result = arguments["a"] * arguments["b"]
+ return [types.TextContent(type="text", text=str(result))]
+ raise ValueError(f"Unknown tool: {name}")
+```
+
+```python After
+from fastmcp import FastMCP
+
+mcp = FastMCP("math")
+
+@mcp.tool
+def add(a: float, b: float) -> float:
+ """Add two numbers"""
+ return a + b
+
+@mcp.tool
+def multiply(a: float, b: float) -> float:
+ """Multiply two numbers"""
+ return a * b
+```
+
+
+
+Each `@mcp.tool` function is self-contained: its name becomes the tool name, its docstring becomes the description, its type annotations become the JSON Schema, and its return value is serialized automatically. No routing. No schema dictionaries. No content-type wrappers.
+
+### Type Mapping
+
+When converting your `inputSchema` to Python type hints:
+
+| JSON Schema | Python Type |
+|---|---|
+| `{"type": "string"}` | `str` |
+| `{"type": "number"}` | `float` |
+| `{"type": "integer"}` | `int` |
+| `{"type": "boolean"}` | `bool` |
+| `{"type": "array", "items": {"type": "string"}}` | `list[str]` |
+| `{"type": "object"}` | `dict` |
+| Optional property (not in `required`) | `param: str \| None = None` |
+
+### Return Values
+
+With the `Server` class, tools return `list[types.TextContent | types.ImageContent | ...]`. In FastMCP, return plain Python values — strings, numbers, dicts, lists, dataclasses, Pydantic models — and serialization is handled for you.
+
+For images or other non-text content, FastMCP provides helpers:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.utilities.types import Image
+
+mcp = FastMCP("media")
+
+@mcp.tool
+def create_chart(data: list[float]) -> Image:
+ """Generate a chart from data."""
+ png_bytes = generate_chart(data) # your logic
+ return Image(data=png_bytes, format="png")
+```
+
+## Resources
+
+The `Server` class uses three handlers for resources: `list_resources()` to enumerate them, `list_resource_templates()` for URI templates, and `read_resource()` to serve content — all with manual routing by URI. FastMCP replaces all three with per-resource decorators.
+
+
+
+```python Before test="skip"
+import json
+import mcp.types as types
+from mcp.server import Server
+from pydantic import AnyUrl
+
+server = Server("data")
+
+@server.list_resources()
+async def list_resources() -> list[types.Resource]:
+ return [
+ types.Resource(
+ uri=AnyUrl("config://app"),
+ name="app_config",
+ description="Application configuration",
+ mimeType="application/json",
+ ),
+ types.Resource(
+ uri=AnyUrl("config://features"),
+ name="feature_flags",
+ description="Active feature flags",
+ mimeType="application/json",
+ ),
+ ]
+
+@server.list_resource_templates()
+async def list_resource_templates() -> list[types.ResourceTemplate]:
+ return [
+ types.ResourceTemplate(
+ uriTemplate="users://{user_id}/profile",
+ name="user_profile",
+ description="User profile by ID",
+ ),
+ types.ResourceTemplate(
+ uriTemplate="projects://{project_id}/status",
+ name="project_status",
+ description="Project status by ID",
+ ),
+ ]
+
+@server.read_resource()
+async def read_resource(uri: AnyUrl) -> str:
+ uri_str = str(uri)
+ if uri_str == "config://app":
+ return json.dumps({"debug": False, "version": "1.0"})
+ if uri_str == "config://features":
+ return json.dumps({"dark_mode": True, "beta": False})
+ if uri_str.startswith("users://"):
+ user_id = uri_str.split("/")[2]
+ return json.dumps({"id": user_id, "name": f"User {user_id}"})
+ if uri_str.startswith("projects://"):
+ project_id = uri_str.split("/")[2]
+ return json.dumps({"id": project_id, "status": "active"})
+ raise ValueError(f"Unknown resource: {uri}")
+```
+
+```python After
+import json
+from fastmcp import FastMCP
+
+mcp = FastMCP("data")
+
+@mcp.resource("config://app", mime_type="application/json")
+def app_config() -> str:
+ """Application configuration"""
+ return json.dumps({"debug": False, "version": "1.0"})
+
+@mcp.resource("config://features", mime_type="application/json")
+def feature_flags() -> str:
+ """Active feature flags"""
+ return json.dumps({"dark_mode": True, "beta": False})
+
+@mcp.resource("users://{user_id}/profile")
+def user_profile(user_id: str) -> str:
+ """User profile by ID"""
+ return json.dumps({"id": user_id, "name": f"User {user_id}"})
+
+@mcp.resource("projects://{project_id}/status")
+def project_status(project_id: str) -> str:
+ """Project status by ID"""
+ return json.dumps({"id": project_id, "status": "active"})
+```
+
+
+
+Static resources and URI templates use the same `@mcp.resource` decorator — FastMCP detects `{placeholders}` in the URI and automatically registers a template. The function parameter `user_id` maps directly to the `{user_id}` placeholder.
+
+## Prompts
+
+Same pattern: the `Server` class uses `list_prompts()` and `get_prompt()` with manual routing. FastMCP uses one decorator per prompt.
+
+
+
+```python Before test="skip"
+import mcp.types as types
+from mcp.server import Server
+
+server = Server("prompts")
+
+@server.list_prompts()
+async def list_prompts() -> list[types.Prompt]:
+ return [
+ types.Prompt(
+ name="review_code",
+ description="Review code for issues",
+ arguments=[
+ types.PromptArgument(
+ name="code",
+ description="The code to review",
+ required=True,
+ ),
+ types.PromptArgument(
+ name="language",
+ description="Programming language",
+ required=False,
+ ),
+ ],
+ )
+ ]
+
+@server.get_prompt()
+async def get_prompt(
+ name: str, arguments: dict[str, str] | None
+) -> types.GetPromptResult:
+ if name == "review_code":
+ code = (arguments or {}).get("code", "")
+ language = (arguments or {}).get("language", "")
+ lang_note = f" (written in {language})" if language else ""
+ return types.GetPromptResult(
+ description="Code review prompt",
+ messages=[
+ types.PromptMessage(
+ role="user",
+ content=types.TextContent(
+ type="text",
+ text=f"Please review this code{lang_note}:\n\n{code}",
+ ),
+ )
+ ],
+ )
+ raise ValueError(f"Unknown prompt: {name}")
+```
+
+```python After
+from fastmcp import FastMCP
+
+mcp = FastMCP("prompts")
+
+@mcp.prompt
+def review_code(code: str, language: str | None = None) -> str:
+ """Review code for issues"""
+ lang_note = f" (written in {language})" if language else ""
+ return f"Please review this code{lang_note}:\n\n{code}"
+```
+
+
+
+Returning a `str` from a prompt function automatically wraps it as a user message. For multi-turn prompts, return a `list[Message]`:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.prompts import Message
+
+mcp = FastMCP("prompts")
+
+@mcp.prompt
+def debug_session(error: str) -> list[Message]:
+ """Start a debugging conversation"""
+ return [
+ Message(f"I'm seeing this error:\n\n{error}"),
+ Message("I'll help you debug that. Can you share the relevant code?", role="assistant"),
+ ]
+```
+
+## Request Context
+
+The `Server` class exposes request context through `server.request_context`, which gives you the raw `ServerSession` for sending notifications. FastMCP replaces this with a typed `Context` object injected into any function that declares it.
+
+
+
+```python Before test="skip"
+import mcp.types as types
+from mcp.server import Server
+
+server = Server("worker")
+
+@server.call_tool()
+async def call_tool(name: str, arguments: dict):
+ if name == "process_data":
+ ctx = server.request_context
+ await ctx.session.send_log_message(
+ level="info", data="Starting processing..."
+ )
+ # ... do work ...
+ await ctx.session.send_log_message(
+ level="info", data="Done!"
+ )
+ return [types.TextContent(type="text", text="Processed")]
+```
+
+```python After
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP("worker")
+
+@mcp.tool
+async def process_data(ctx: Context) -> str:
+ """Process data with progress logging"""
+ await ctx.info("Starting processing...")
+ # ... do work ...
+ await ctx.info("Done!")
+ return "Processed"
+```
+
+
+
+The `Context` object provides logging (`ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`), progress reporting (`ctx.report_progress()`), resource subscriptions, session state, and more. See [Context](/servers/context) for the full API.
+
+## Errors
+
+Most of the errors a low-level server raises disappear with the dispatch that raised them: the `ValueError(f"Unknown tool: {name}")` fallback is unnecessary once FastMCP routes calls, and an exception from your function body is converted to a tool error for you.
+
+Deliberate protocol errors are the exception, and they need a small rewrite. The v1 pattern wrapped an `ErrorData` and passed it positionally; FastMCP's `McpError` takes the fields directly:
+
+```python test="skip"
+from fastmcp.exceptions import McpError
+
+# Before (SDK v1):
+# raise McpError(ErrorData(code=-32000, message="Upstream unavailable"))
+
+# After:
+raise McpError(code=-32000, message="Upstream unavailable")
+```
+
+An optional third argument, `data=`, carries the structured payload `ErrorData` used to hold. Catching is unchanged — `except McpError` still works, and `err.error.code` still reads the code — so only construction sites need touching.
+
+## Complete Example
+
+A full server upgrade, showing how all the pieces fit together:
+
+
+
+```python Before expandable test="skip"
+import asyncio
+import json
+import mcp.types as types
+from mcp.server import Server
+from mcp.server.stdio import stdio_server
+from pydantic import AnyUrl
+
+server = Server("demo")
+
+@server.list_tools()
+async def list_tools() -> list[types.Tool]:
+ return [
+ types.Tool(
+ name="greet",
+ description="Greet someone by name",
+ inputSchema={
+ "type": "object",
+ "properties": {
+ "name": {"type": "string"},
+ },
+ "required": ["name"],
+ },
+ )
+ ]
+
+@server.call_tool()
+async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
+ if name == "greet":
+ return [types.TextContent(type="text", text=f"Hello, {arguments['name']}!")]
+ raise ValueError(f"Unknown tool: {name}")
+
+@server.list_resources()
+async def list_resources() -> list[types.Resource]:
+ return [
+ types.Resource(
+ uri=AnyUrl("info://version"),
+ name="version",
+ description="Server version",
+ )
+ ]
+
+@server.read_resource()
+async def read_resource(uri: AnyUrl) -> str:
+ if str(uri) == "info://version":
+ return json.dumps({"version": "1.0.0"})
+ raise ValueError(f"Unknown resource: {uri}")
+
+@server.list_prompts()
+async def list_prompts() -> list[types.Prompt]:
+ return [
+ types.Prompt(
+ name="summarize",
+ description="Summarize text",
+ arguments=[
+ types.PromptArgument(name="text", required=True)
+ ],
+ )
+ ]
+
+@server.get_prompt()
+async def get_prompt(
+ name: str, arguments: dict[str, str] | None
+) -> types.GetPromptResult:
+ if name == "summarize":
+ return types.GetPromptResult(
+ description="Summarize text",
+ messages=[
+ types.PromptMessage(
+ role="user",
+ content=types.TextContent(
+ type="text",
+ text=f"Summarize:\n\n{(arguments or {}).get('text', '')}",
+ ),
+ )
+ ],
+ )
+ raise ValueError(f"Unknown prompt: {name}")
+
+async def main():
+ async with stdio_server() as (read_stream, write_stream):
+ await server.run(
+ read_stream, write_stream,
+ server.create_initialization_options(),
+ )
+
+asyncio.run(main())
+```
+
+```python After
+import json
+from fastmcp import FastMCP
+
+mcp = FastMCP("demo")
+
+@mcp.tool
+def greet(name: str) -> str:
+ """Greet someone by name"""
+ return f"Hello, {name}!"
+
+@mcp.resource("info://version")
+def version() -> str:
+ """Server version"""
+ return json.dumps({"version": "1.0.0"})
+
+@mcp.prompt
+def summarize(text: str) -> str:
+ """Summarize text"""
+ return f"Summarize:\n\n{text}"
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+
+
+## What You Gain
+
+Deleting the handler machinery is the immediate payoff, but the reason to make this move is what becomes available once your server is a FastMCP server.
+
+[Server composition](/servers/composition) mounts one server inside another, so a surface that grew unwieldy as a single `call_tool` dispatch splits into modules developed and tested independently. [Middleware](/servers/middleware) runs across every request for logging, rate limiting, error handling, and caching — the cross-cutting concerns that, on the low-level `Server`, meant threading the same code through every handler. [Proxy servers](/servers/providers/proxy) put a FastMCP server in front of any existing MCP server, bridging transports and adding auth to a backend you don't control, and the [OpenAPI integration](/integrations/openapi) generates an entire server from an API specification you already have. [Authentication](/servers/auth/authentication) arrives as a single `auth=` provider covering token verification, OAuth, and named providers for GitHub, Google, Auth0, and others.
+
+The change most likely to affect your daily work is [testing](/servers/testing). FastMCP ships a client that connects to a server object in the same Python process, so a test calls your tools directly — no subprocess, no stdio pipes, no transport to stand up.
diff --git a/docs/getting-started/upgrading/from-low-level-sdk-v2.mdx b/docs/getting-started/upgrading/from-low-level-sdk-v2.mdx
new file mode 100644
index 000000000..f4222c0f0
--- /dev/null
+++ b/docs/getting-started/upgrading/from-low-level-sdk-v2.mdx
@@ -0,0 +1,622 @@
+---
+title: Upgrading from the Low-Level SDK v2
+sidebarTitle: "From Low-Level SDK v2"
+description: Move a server built on v2 of the low-level Python SDK's Server class to FastMCP
+icon: up
+---
+
+If your server builds on the `mcp` package's low-level `Server` class as SDK v2 rebuilt it — handlers passed to the constructor as `on_list_tools`, `on_call_tool`, and their siblings, each taking `(ctx, params)` and returning a wrapped result object — this guide is for you. FastMCP replaces that machinery with a declarative API where your functions *are* the protocol surface.
+
+The core idea: instead of describing your tools to the SDK and then separately implementing them, you write ordinary Python functions and let FastMCP derive the protocol layer from your code. Type hints become JSON Schema. Docstrings become descriptions. Return values are serialized automatically. The dispatch you wrote to route a call by name, and the schemas you wrote by hand to describe it, both disappear.
+
+Migrating from SDK v2 is the most direct of the four upgrade paths, because you and FastMCP already share a protocol layer. FastMCP 4 is built on SDK v2, so `mcp_types` imports keep working, field names are already snake_case, and the era negotiation you get is the one you have. Almost nothing about the wire changes — the one exception is [argument strictness](#stricter-arguments), covered below.
+
+
+On SDK v1's decorator-registered `Server` — `@server.list_tools()`, `@server.call_tool()` — instead? See [Upgrading from the Low-Level SDK v1](/getting-started/upgrading/from-low-level-sdk-v1), where the before-and-after code matches that API.
+
+Using SDK v2's high-level `MCPServer` class? See [Upgrading from MCP SDK v2](/getting-started/upgrading/from-mcp-sdk-v2) — that migration is mostly renaming.
+
+
+
+You are rewriting an MCP server built on the MCP Python SDK v2's low-level `Server` class (`mcp.server.lowlevel.server.Server`, with `on_*` handlers passed to the constructor) using FastMCP 4's high-level API.
+
+FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk-v2 — it explains every item below in full, with before-and-after code. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not guess at a FastMCP API you have not confirmed in the docs.
+
+Then work through the provided code looking for each of these. The guide has the replacement for every one:
+
+CONSTRUCTION AND TRANSPORT
+- `Server(name, on_list_tools=..., on_call_tool=..., ...)` — the whole constructor, including every handler passed to it
+- `server.run(read_stream, write_stream, server.create_initialization_options())` and its `stdio_server()` context manager
+- `server.streamable_http_app()` and any Starlette app assembled around it
+- `asyncio.run(main())` boilerplate
+- `lifespan=` — carries over directly: pass the same async context manager to `FastMCP(lifespan=...)`, and read what it yields from `ctx.lifespan_context` in any tool. Do not drop it — the tools that depended on it (a DB connection, a client pool) lose their dependency silently if you do.
+
+HANDLERS TO DELETE, EACH REPLACED BY ONE DECORATOR (not simply removed)
+- `on_list_tools` + `on_call_tool` → one `@mcp.tool` function per branch of the `if params.name == ...` dispatch chain inside `on_call_tool`
+- `on_list_resources` + `on_list_resource_templates` + `on_read_resource` → one `@mcp.resource` function per resource/template
+- `on_list_prompts` + `on_get_prompt` → one `@mcp.prompt` function per prompt
+- `on_completion` → one `@mcp.completion` function. This one is easy to drop by mistake: skipping it does not just remove autocomplete cleanly, it silently stops FastMCP from advertising the completions capability at all, since that capability is only advertised when a handler is registered.
+- `on_subscribe_resource` / `on_unsubscribe_resource` / `on_subscriptions_listen` — flag for the user, no single-decorator equivalent
+- `on_set_logging_level`, `on_progress`, `on_roots_list_changed`, `on_ping` — flag for the user, these are protocol-level hooks with no direct FastMCP surface
+
+TYPES THAT DISAPPEAR FROM YOUR CODE
+- Hand-written `input_schema` / `output_schema` JSON Schema dicts — these come from type hints now
+- `types.ListToolsResult`, `types.CallToolResult`, `types.ListResourcesResult`, `types.ListResourceTemplatesResult`, `types.ReadResourceResult`, `types.ListPromptsResult`, `types.GetPromptResult` — result wrappers FastMCP builds for you
+- `types.TextContent`, `types.TextResourceContents`, `types.BlobResourceContents` — return plain Python values instead
+- `types.ImageContent` / `types.AudioContent` — `fastmcp.utilities.types.Image` / `Audio`
+- `types.Tool`, `types.Resource`, `types.ResourceTemplate`, `types.Prompt`, `types.PromptArgument` — declaration types FastMCP derives
+- `types.PromptMessage` — `fastmcp.prompts.Message`
+- Note which `mcp_types` imports are still needed afterward; protocol types are unchanged in FastMCP, so surviving imports stay as they are.
+
+CONTEXT AND SIDE CHANNELS
+- `ctx.session.send_log_message(...)` — `ctx.info()` / `ctx.debug()` / `ctx.warning()` / `ctx.error()` on a `fastmcp.Context` parameter
+- `ctx.session.report_progress(...)` — `ctx.report_progress()`
+- `ctx.request_id`, `ctx.meta`, `ctx.protocol_version` — these live on `ctx.request_context` in FastMCP (`ctx.request_context.request_id`, and so on); note that `ctx.protocol_version` directly on the Context does not exist
+- `ctx.params` — no equivalent, and none is needed: the raw request params were how a low-level handler read the tool's arguments, and those are now the decorated function's typed parameters. `ctx.request_context.params` does NOT exist and raises AttributeError.
+- Direct `ctx.session` use for anything else — `Context.session` exists in FastMCP too and returns the same raw SDK session, so this still works; prefer a `Context` method where one exists, and note the remaining uses as SDK-coupled
+
+ERRORS AND AUTH
+- `raise ValueError(f"Unknown tool: ...")` dispatch fallbacks — these become unnecessary
+- `MCPError` construction and any error-code mapping
+- `auth=AuthSettings(...)`, `token_verifier=`, `auth_server_provider=` — one `auth=` provider in FastMCP
+- `TransportSecuritySettings`
+
+For each item found, show the original code, say what it did, and give the FastMCP equivalent. Where several handlers collapse into one decorated function, show the collapse rather than a line-by-line mapping. Call out anything you could not find a documented FastMCP replacement for instead of inventing one.
+
+
+## Install
+
+FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
+
+```bash
+pip install "fastmcp==4.0.0b1"
+# or
+uv add "fastmcp==4.0.0b1"
+```
+
+An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
+
+FastMCP 4 depends on the MCP SDK v2 you are already using, so `mcp_types` stays importable and every protocol type keeps its current name and fields. Most of those imports vanish from your code anyway — FastMCP derives them — but the ones you keep need no changes.
+
+## Server and Transport
+
+The `Server` class asks you to open a transport, connect its streams, build initialization options, and run an event loop. FastMCP collapses that into a constructor and a `run()` call.
+
+
+
+```python Before test="skip"
+import asyncio
+
+from mcp.server.lowlevel.server import Server
+from mcp.server.stdio import stdio_server
+
+server = Server("my-server") # plus every on_* handler
+
+async def main():
+ async with stdio_server() as (read_stream, write_stream):
+ await server.run(
+ read_stream,
+ write_stream,
+ server.create_initialization_options(),
+ )
+
+asyncio.run(main())
+```
+
+```python After
+from fastmcp import FastMCP
+
+mcp = FastMCP("my-server")
+
+# ... register tools, resources, prompts ...
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+
+
+Serving HTTP is the same shape. Where the low-level class hands you a Starlette app from `server.streamable_http_app()` and leaves the hosting to you, FastMCP runs it directly:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("my-server")
+
+if __name__ == "__main__":
+ mcp.run(transport="http", host="0.0.0.0", port=8000)
+```
+
+`mcp.http_app()` still returns a Starlette app when you need to mount the server inside a larger application.
+
+## Tools
+
+This is where the difference is largest. SDK v2 requires two handlers — one describing your tools with hand-written JSON Schema, one dispatching calls by name — and both are passed to the constructor, so the connection between a tool's declaration and its implementation lives only in your head. FastMCP derives both from the function.
+
+
+
+```python Before
+import mcp_types as types
+from mcp.server.context import ServerRequestContext
+from mcp.server.lowlevel.server import Server
+
+
+async def list_tools(ctx: ServerRequestContext, params) -> types.ListToolsResult:
+ number = {"type": "number"}
+ schema = {
+ "type": "object",
+ "properties": {"a": number, "b": number},
+ "required": ["a", "b"],
+ }
+ return types.ListToolsResult(
+ tools=[
+ types.Tool(name="add", description="Add two numbers", input_schema=schema),
+ types.Tool(
+ name="multiply", description="Multiply two numbers", input_schema=schema
+ ),
+ ]
+ )
+
+
+async def call_tool(
+ ctx: ServerRequestContext, params: types.CallToolRequestParams
+) -> types.CallToolResult:
+ arguments = params.arguments or {}
+ if params.name == "add":
+ result = arguments["a"] + arguments["b"]
+ elif params.name == "multiply":
+ result = arguments["a"] * arguments["b"]
+ else:
+ raise ValueError(f"Unknown tool: {params.name}")
+ return types.CallToolResult(content=[types.TextContent(type="text", text=str(result))])
+
+
+server = Server("math", on_list_tools=list_tools, on_call_tool=call_tool)
+```
+
+```python After
+from fastmcp import FastMCP
+
+mcp = FastMCP("math")
+
+
+@mcp.tool
+def add(a: float, b: float) -> float:
+ """Add two numbers"""
+ return a + b
+
+
+@mcp.tool
+def multiply(a: float, b: float) -> float:
+ """Multiply two numbers"""
+ return a * b
+```
+
+
+
+Each `@mcp.tool` function is self-contained: its name becomes the tool name, its docstring becomes the description, its annotations become the JSON Schema, and its return value is serialized for you. The dispatch chain, the schema dicts, the `CallToolResult` wrapper, the `TextContent` wrapper, and the unknown-tool fallback all go away — a tool that doesn't exist is now the framework's problem, not a branch you maintain.
+
+### Type Mapping
+
+Your hand-written `input_schema` becomes the function's parameters:
+
+| JSON Schema | Python type |
+|---|---|
+| `{"type": "string"}` | `str` |
+| `{"type": "number"}` | `float` |
+| `{"type": "integer"}` | `int` |
+| `{"type": "boolean"}` | `bool` |
+| `{"type": "array", "items": {"type": "string"}}` | `list[str]` |
+| `{"type": "object"}` | `dict` |
+| A property absent from `required` | `param: str \| None = None` |
+
+Constraints carry over too. A schema with `"minimum"` and `"maximum"` becomes a Pydantic `Field`, and a nested object schema becomes a Pydantic model or dataclass used as the annotation — FastMCP generates the same schema back out of it.
+
+### Return Values
+
+The low-level class requires tools to return a `CallToolResult` wrapping a list of content blocks. FastMCP takes the value itself — strings, numbers, dicts, lists, dataclasses, Pydantic models — and handles both the content block and the structured output. For images and audio, FastMCP provides wrapper types that carry the format:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.utilities.types import Image
+
+mcp = FastMCP("media")
+
+
+@mcp.tool
+def create_chart(data: list[float]) -> Image:
+ """Generate a chart from data."""
+ png_bytes = render_png(data) # your logic
+ return Image(data=png_bytes, format="png")
+```
+
+When you need full control over the wire result — multiple content blocks, or structured content that differs from the content blocks — return a `ToolResult` from `fastmcp.tools` instead.
+
+### Stricter Arguments
+
+Deriving the schema from your signature also tightens what callers may send, and this is the one behavior change the migration introduces. Your `on_call_tool` handler reads `params.arguments` as a plain dict and never looks at keys it doesn't need, so a call carrying an unexpected key succeeds. FastMCP declares `"additionalProperties": false` on the generated schema and enforces it, so the same call fails:
+
+```python test="skip"
+# Against the low-level handler: succeeds, "extra" never read.
+# Against FastMCP: raises, "extra" is not a parameter of greet().
+await client.call_tool("greet", {"name": "World", "extra": "surprise"})
+```
+
+For most servers this is an improvement that costs nothing — a caller sending keys your handler never read was already a bug, and the hand-written schema never advertised that they were allowed. It matters if a client in your fleet attaches metadata alongside real arguments, since those calls start failing the moment you migrate. Accept them explicitly as optional parameters if you need to keep them working.
+
+## Resources
+
+Resources take three handlers on the low-level class: one to list static resources, one to list URI templates, and one to read whichever URI arrives, with routing you write by hand. FastMCP replaces all three with a decorator per resource, and detects templates from the URI itself.
+
+
+
+```python Before
+import json
+
+import mcp_types as types
+from mcp.server.context import ServerRequestContext
+from mcp.server.lowlevel.server import Server
+
+
+async def list_resources(ctx: ServerRequestContext, params) -> types.ListResourcesResult:
+ return types.ListResourcesResult(
+ resources=[
+ types.Resource(
+ uri="config://app",
+ name="app_config",
+ description="Application configuration",
+ mime_type="application/json",
+ )
+ ]
+ )
+
+
+async def list_resource_templates(
+ ctx: ServerRequestContext, params
+) -> types.ListResourceTemplatesResult:
+ return types.ListResourceTemplatesResult(
+ resource_templates=[
+ types.ResourceTemplate(
+ uri_template="users://{user_id}/profile",
+ name="user_profile",
+ description="User profile by ID",
+ )
+ ]
+ )
+
+
+async def read_resource(
+ ctx: ServerRequestContext, params: types.ReadResourceRequestParams
+) -> types.ReadResourceResult:
+ uri = str(params.uri)
+ if uri == "config://app":
+ text = json.dumps({"debug": False, "version": "1.0"})
+ elif uri.startswith("users://"):
+ user_id = uri.split("/")[2]
+ text = json.dumps({"id": user_id, "name": f"User {user_id}"})
+ else:
+ raise ValueError(f"Unknown resource: {uri}")
+ return types.ReadResourceResult(
+ contents=[
+ types.TextResourceContents(
+ uri=params.uri, mime_type="application/json", text=text
+ )
+ ]
+ )
+
+
+server = Server(
+ "data",
+ on_list_resources=list_resources,
+ on_list_resource_templates=list_resource_templates,
+ on_read_resource=read_resource,
+)
+```
+
+```python After
+import json
+
+from fastmcp import FastMCP
+
+mcp = FastMCP("data")
+
+
+@mcp.resource("config://app", mime_type="application/json")
+def app_config() -> str:
+ """Application configuration"""
+ return json.dumps({"debug": False, "version": "1.0"})
+
+
+@mcp.resource("users://{user_id}/profile", mime_type="application/json")
+def user_profile(user_id: str) -> str:
+ """User profile by ID"""
+ return json.dumps({"id": user_id, "name": f"User {user_id}"})
+```
+
+
+
+The URI does the routing. A `{placeholder}` in the URI makes the resource a template, and FastMCP matches the parameter to the function argument of the same name — so the `uri.split("/")[2]` parsing goes away along with the handler that held it. Return a `str` for text content and `bytes` for binary; FastMCP builds the `TextResourceContents` or `BlobResourceContents` wrapper.
+
+Templated resources also gain a protection the low-level version left to you: FastMCP screens extracted parameter values for path traversal, absolute paths, and null bytes before your function runs. See [Path Security](/servers/resources#path-security) if a template legitimately accepts those values.
+
+## Prompts
+
+The same collapse, one more time: `on_list_prompts` declares arguments as `PromptArgument` objects, `on_get_prompt` routes by name and assembles a `GetPromptResult` of `PromptMessage` objects. FastMCP takes a function whose parameters are the arguments.
+
+
+
+```python Before
+import mcp_types as types
+from mcp.server.context import ServerRequestContext
+from mcp.server.lowlevel.server import Server
+
+
+async def list_prompts(ctx: ServerRequestContext, params) -> types.ListPromptsResult:
+ return types.ListPromptsResult(
+ prompts=[
+ types.Prompt(
+ name="review_code",
+ description="Review code for issues",
+ arguments=[
+ types.PromptArgument(
+ name="code", description="The code to review", required=True
+ ),
+ types.PromptArgument(
+ name="language", description="Programming language", required=False
+ ),
+ ],
+ )
+ ]
+ )
+
+
+async def get_prompt(
+ ctx: ServerRequestContext, params: types.GetPromptRequestParams
+) -> types.GetPromptResult:
+ if params.name != "review_code":
+ raise ValueError(f"Unknown prompt: {params.name}")
+ arguments = params.arguments or {}
+ language = arguments.get("language", "")
+ note = f" (written in {language})" if language else ""
+ text = f"Please review this code{note}:\n\n{arguments.get('code', '')}"
+ return types.GetPromptResult(
+ description="Code review prompt",
+ messages=[
+ types.PromptMessage(
+ role="user", content=types.TextContent(type="text", text=text)
+ )
+ ],
+ )
+
+
+server = Server("prompts", on_list_prompts=list_prompts, on_get_prompt=get_prompt)
+```
+
+```python After
+from fastmcp import FastMCP
+
+mcp = FastMCP("prompts")
+
+
+@mcp.prompt
+def review_code(code: str, language: str | None = None) -> str:
+ """Review code for issues"""
+ note = f" (written in {language})" if language else ""
+ return f"Please review this code{note}:\n\n{code}"
+```
+
+
+
+Returning a `str` wraps it as a single user message. Whether an argument is required is read from the signature: `code` has no default, so it's required; `language` defaults to `None`, so it isn't. Multi-turn prompts return a list of `Message` objects, which take their text positionally and default to the user role:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.prompts import Message
+
+mcp = FastMCP("prompts")
+
+
+@mcp.prompt
+def debug_session(error: str) -> list[Message]:
+ """Start a debugging conversation"""
+ return [
+ Message(f"I'm seeing this error:\n\n{error}"),
+ Message("I'll help you debug that. Can you share the relevant code?", role="assistant"),
+ ]
+```
+
+## Request Context
+
+The low-level class hands each handler a `ServerRequestContext` carrying the raw `ServerSession`, and you reach through it to send notifications. FastMCP injects a typed `Context` into any function that declares one, and puts the operations you actually want on it directly.
+
+
+
+```python Before
+import mcp_types as types
+from mcp.server.context import ServerRequestContext
+from mcp.server.lowlevel.server import Server
+
+
+async def call_tool(
+ ctx: ServerRequestContext, params: types.CallToolRequestParams
+) -> types.CallToolResult:
+ if params.name == "process_data":
+ await ctx.session.send_log_message(level="info", data="Starting processing...")
+ await ctx.session.report_progress(1, 2)
+ # ... do work ...
+ await ctx.session.send_log_message(level="info", data="Done!")
+ return types.CallToolResult(
+ content=[types.TextContent(type="text", text="Processed")]
+ )
+ raise ValueError(f"Unknown tool: {params.name}")
+
+
+server = Server("worker", on_call_tool=call_tool)
+```
+
+```python After
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP("worker")
+
+
+@mcp.tool
+async def process_data(ctx: Context) -> str:
+ """Process data with progress logging"""
+ await ctx.info("Starting processing...")
+ await ctx.report_progress(1, 2)
+ # ... do work ...
+ await ctx.info("Done!")
+ return "Processed"
+```
+
+
+
+The `Context` parameter is injected by type annotation and never appears in the tool's schema, so clients see `process_data` as taking no arguments. Beyond logging and progress, it carries resource reads, [session state](/servers/sessions), elicitation, and component visibility — see [Context](/servers/context) for the full surface.
+
+One thing to check as you migrate: `ctx.session` still exists on a FastMCP `Context` as an escape hatch, and it hands back the same raw SDK session your handlers use today. That makes it a working translation for anything with no `Context` equivalent — but it's also the one part of your server that stays coupled to SDK internals, so reach for the `Context` method first and keep the escape hatch for what genuinely has no equivalent.
+
+## Complete Example
+
+Everything above, applied at once:
+
+
+
+```python Before expandable
+import json
+
+import mcp_types as types
+from mcp.server.context import ServerRequestContext
+from mcp.server.lowlevel.server import Server
+
+
+async def list_tools(ctx: ServerRequestContext, params) -> types.ListToolsResult:
+ return types.ListToolsResult(
+ tools=[
+ types.Tool(
+ name="greet",
+ description="Greet someone by name",
+ input_schema={
+ "type": "object",
+ "properties": {"name": {"type": "string"}},
+ "required": ["name"],
+ },
+ )
+ ]
+ )
+
+
+async def call_tool(
+ ctx: ServerRequestContext, params: types.CallToolRequestParams
+) -> types.CallToolResult:
+ if params.name == "greet":
+ name = (params.arguments or {})["name"]
+ return types.CallToolResult(
+ content=[types.TextContent(type="text", text=f"Hello, {name}!")]
+ )
+ raise ValueError(f"Unknown tool: {params.name}")
+
+
+async def list_resources(ctx: ServerRequestContext, params) -> types.ListResourcesResult:
+ return types.ListResourcesResult(
+ resources=[
+ types.Resource(
+ uri="info://version", name="version", description="Server version"
+ )
+ ]
+ )
+
+
+async def read_resource(
+ ctx: ServerRequestContext, params: types.ReadResourceRequestParams
+) -> types.ReadResourceResult:
+ if str(params.uri) != "info://version":
+ raise ValueError(f"Unknown resource: {params.uri}")
+ return types.ReadResourceResult(
+ contents=[
+ types.TextResourceContents(
+ uri=params.uri, text=json.dumps({"version": "1.0.0"})
+ )
+ ]
+ )
+
+
+async def list_prompts(ctx: ServerRequestContext, params) -> types.ListPromptsResult:
+ return types.ListPromptsResult(
+ prompts=[
+ types.Prompt(
+ name="summarize",
+ description="Summarize text",
+ arguments=[types.PromptArgument(name="text", required=True)],
+ )
+ ]
+ )
+
+
+async def get_prompt(
+ ctx: ServerRequestContext, params: types.GetPromptRequestParams
+) -> types.GetPromptResult:
+ if params.name != "summarize":
+ raise ValueError(f"Unknown prompt: {params.name}")
+ text = (params.arguments or {}).get("text", "")
+ return types.GetPromptResult(
+ description="Summarize text",
+ messages=[
+ types.PromptMessage(
+ role="user",
+ content=types.TextContent(type="text", text=f"Summarize:\n\n{text}"),
+ )
+ ],
+ )
+
+
+server = Server(
+ "demo",
+ on_list_tools=list_tools,
+ on_call_tool=call_tool,
+ on_list_resources=list_resources,
+ on_read_resource=read_resource,
+ on_list_prompts=list_prompts,
+ on_get_prompt=get_prompt,
+)
+```
+
+```python After
+import json
+
+from fastmcp import FastMCP
+
+mcp = FastMCP("demo")
+
+
+@mcp.tool
+def greet(name: str) -> str:
+ """Greet someone by name"""
+ return f"Hello, {name}!"
+
+
+@mcp.resource("info://version")
+def version() -> str:
+ """Server version"""
+ return json.dumps({"version": "1.0.0"})
+
+
+@mcp.prompt
+def summarize(text: str) -> str:
+ """Summarize text"""
+ return f"Summarize:\n\n{text}"
+
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+
+
+## What You Gain
+
+Deleting the handler machinery is the immediate payoff, but the reason to make this move is what becomes available once your server is a FastMCP server.
+
+[Server composition](/servers/composition) mounts one server inside another, so a surface that grew unwieldy as a single dispatch chain splits into modules developed and tested independently. [Middleware](/servers/middleware) runs across every request for logging, rate limiting, error handling, and caching, with hooks at whichever level of specificity you need — the cross-cutting concerns that, on the low-level class, meant threading the same code through every handler. [Proxy servers](/servers/providers/proxy) put a FastMCP server in front of any existing MCP server, bridging transports and adding auth to a backend you don't control, and the [OpenAPI integration](/integrations/openapi) generates an entire server from an API specification you already have. [Authentication](/servers/auth/authentication) consolidates the SDK's separate token verifier, authorization-server provider, and `AuthSettings` into a single `auth=` provider, with named providers for GitHub, Google, Auth0, Keycloak, and others.
+
+The change most likely to affect your daily work is [testing](/servers/testing). FastMCP ships a client that connects to a server object in the same Python process, so a test calls your tools directly — no subprocess, no stdio pipes, no transport to stand up.
diff --git a/docs/getting-started/upgrading/from-mcp-sdk-v1.mdx b/docs/getting-started/upgrading/from-mcp-sdk-v1.mdx
new file mode 100644
index 000000000..ec5ac5b7e
--- /dev/null
+++ b/docs/getting-started/upgrading/from-mcp-sdk-v1.mdx
@@ -0,0 +1,264 @@
+---
+title: Upgrading from MCP SDK v1
+sidebarTitle: "From MCP SDK v1"
+description: Upgrade from FastMCP 1.0, bundled in v1 of the MCP Python SDK, to the standalone FastMCP framework
+icon: up
+---
+
+If your server starts with `from mcp.server.fastmcp import FastMCP`, you're using FastMCP 1.0 — the version bundled with v1 of the `mcp` package. Upgrading to the standalone FastMCP framework is easy. **For most servers, it's a single import change.**
+
+```python test="skip"
+# Before
+from mcp.server.fastmcp import FastMCP
+
+# After
+from fastmcp import FastMCP
+```
+
+That's it. Your `@mcp.tool`, `@mcp.resource`, and `@mcp.prompt` decorators, your `mcp.run()` call, and the rest of your server code all work as-is.
+
+
+**Why upgrade?** FastMCP 1.0 pioneered the Pythonic MCP server experience, and we're proud it was bundled into the `mcp` package. The standalone FastMCP project has since grown into a full framework for taking MCP servers from prototype to production — with composition, middleware, proxy servers, authentication, and much more. Upgrading gives you access to all of that, plus ongoing updates and fixes.
+
+
+## The SDK v2 Transition
+
+MCP SDK v2 is a substantial, deliberate modernization of the protocol layer, and part of that work rebuilt the high-level server as `MCPServer` under `mcp.server.mcpserver`. `mcp.server.fastmcp` does not exist there — so a FastMCP 1.0 server meets the change the moment its environment resolves `mcp` to v2:
+
+```
+ModuleNotFoundError: No module named 'mcp.server.fastmcp'
+```
+
+Often nobody chose that moment. An unpinned `mcp` dependency, a fresh lockfile, or a rebuilt container picks up the new major version and the module your server imports on line one has moved. Nothing is wrong with your code, and nothing is wrong with the SDK — major versions are exactly where a change like this belongs. Your build just crossed it earlier than you planned to.
+
+Pinning the SDK back restores the old module immediately, with no code changes, and buys you time to choose deliberately:
+
+```bash
+pip install "mcp<2"
+```
+
+## Two Upgrade Paths
+
+From here, both directions are reasonable, and which is less work depends on which API you already write.
+
+**`MCPServer`, the SDK's high-level server**, is a capable, well-designed API and the direct continuation of the SDK's own line. Because it was rebuilt rather than renamed, expect real work: a new class and import, a different decorator call style, and protocol types imported from the standalone `mcp_types` package with snake_case field names.
+
+**FastMCP** is the import change at the top of this page. It is short for a specific, historical reason: FastMCP 1.0 *is* early FastMCP — it was contributed into the `mcp` package, and the standalone project kept developing that same high-level API. The surface you already write against is the surface FastMCP still offers. FastMCP 4 is itself built on MCP SDK v2, so both paths land you on the same modern protocol layer; FastMCP absorbs the adaptation internally rather than asking your code to do it.
+
+The claim is narrower than it may sound. It holds for FastMCP 1.0 servers specifically, because of shared lineage — not because one library is better than the other. Both projects are moving the same direction on the same protocol.
+
+If you have already moved to SDK v2 and write against `MCPServer` today, see [Upgrading from MCP SDK v2](/getting-started/upgrading/from-mcp-sdk-v2). If your server uses the low-level `Server` class rather than the high-level one, see [Upgrading from the Low-Level SDK v1](/getting-started/upgrading/from-low-level-sdk-v1).
+
+## Install
+
+FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
+
+```bash
+pip install "fastmcp==4.0.0b1"
+# or
+uv add "fastmcp==4.0.0b1"
+```
+
+An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
+
+FastMCP depends on the `mcp` package, so the SDK stays installed and importable. What changes is which parts of it you reach for. FastMCP 4 builds on SDK v2, where `mcp.server.fastmcp` is gone — anything you imported from it needs a new home, and the sections below cover that. `mcp.types` still resolves (it aliases the standalone `mcp_types` package), though its fields are snake_case now. Update your import, run your server, and if your tools work, you're done.
+
+
+You are upgrading an MCP server from FastMCP 1.0 (bundled in v1 of the `mcp` package) to standalone FastMCP 4.
+
+FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v1 — it explains every item below, with the replacement code. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs.
+
+For most servers the entire upgrade is the first item. Work through the rest looking for signals, and report only what you actually find.
+
+THE IMPORT (every server needs this)
+- `from mcp.server.fastmcp import FastMCP` → `from fastmcp import FastMCP`
+- `from mcp.server.fastmcp import Context`
+- `from mcp.server.fastmcp import Image`
+
+CONSTRUCTOR ARGUMENTS THAT MOVED (all raise TypeError)
+- moved to run()/http_app(), and FastMCP names them in the error: host, port, log_level, debug, sse_path, message_path, streamable_http_path, json_response, stateless_http
+- moved but rejected with only a generic "unexpected keyword argument", so flag these explicitly: `event_store=` (→ `http_app(event_store=...)`; dropping it silently disables streamable-HTTP resumability), `mount_path=` (→ `http_app(path=...)`), `transport=` (→ `run(transport=...)`), `transport_security=` (→ host/origin settings on `http_app()`), `warn_on_duplicate_tools/_resources/_prompts=` (→ one `on_duplicate=`), `dependencies=` (→ a fastmcp.json file)
+- `name`, `instructions`, `website_url`, `icons`, `tools`, `lifespan` carry over unchanged
+- note when reporting: FastMCP names the streamable HTTP transport "http", not "streamable-http"
+
+CONTEXT METHODS WITH CHANGED SIGNATURES (compile fine, fail at runtime)
+- `ctx.log(level, data)` → `ctx.log(message, level=...)`, message first
+- `ctx.info(data)` / `debug` / `warning` / `error` → take a str message, not arbitrary JSON-serializable data
+- `ctx.elicit(..., schema=Model)` → `response_type=Model`
+- `ctx.read_resource(uri)` → returns a `ResourceResult`; read `.contents` rather than iterating the return value
+- `ctx.report_progress`, `ctx.request_id`, `ctx.client_id` are unchanged
+
+AUTHENTICATION (the one case where the single import change is NOT enough)
+- `token_verifier=` and `auth_server_provider=` — both raise TypeError on FastMCP 4
+- `auth=AuthSettings(...)` — the keyword survives but the value does not: FastMCP's `auth=` takes a FastMCP `AuthProvider`, not the SDK settings object
+Report these as a real migration, not a rename: FastMCP consolidates all three into one provider, and ships `JWTVerifier` for tokens you already issue, `RemoteAuthProvider` for delegating to an external authorization server, `OAuthProxy` for wrapping a provider without Dynamic Client Registration, and named providers for GitHub, Google, Auth0, Keycloak, and others. Look up the right one at https://gofastmcp.com/servers/auth/authentication rather than guessing.
+
+PROMPT RETURN VALUES
+- prompt functions returning `PromptMessage`, or `TextContent`-wrapped content
+- prompt functions returning raw dicts with "role"/"content" keys — FastMCP 1.0 coerced these silently, standalone FastMCP does not
+
+OTHER mcp.* IMPORTS
+- anything from `mcp.types` — the import path still works in the SDK v2 that FastMCP 4 builds on, but the fields were renamed from camelCase to snake_case
+- `from mcp.server.stdio import stdio_server` and any transport boilerplate around it
+- `mcp.types.TextContent` / `ImageContent` used to wrap tool return values — FastMCP has friendlier equivalents, so prefer those over keeping the raw protocol types
+
+DECORATOR RETURN VALUES
+- any code reading `.name`, `.description`, or other component attributes off a `@mcp.tool` / `@mcp.resource` / `@mcp.prompt` decorated function. Decorators return the original function now.
+
+For each item found, show the original line, name what changed, and give the corrected code from the guide. If the only change needed is the import, say so plainly rather than manufacturing work.
+
+
+## What Might Need Updating
+
+Most servers need nothing beyond the import change. Skim the sections below to see if any apply.
+
+### Constructor Settings
+
+If you passed transport settings like `host` or `port` directly to `FastMCP()`, those now belong on `run()`. This keeps your server definition independent of how it's deployed:
+
+```python test="skip"
+from fastmcp import FastMCP
+
+# Before
+mcp = FastMCP("my-server", host="0.0.0.0", port=8080)
+mcp.run()
+
+# After
+mcp = FastMCP("my-server")
+mcp.run(transport="http", host="0.0.0.0", port=8080)
+```
+
+Nine arguments move this way, and each raises a `TypeError` naming its own replacement, so you can also just run the server and follow the errors: `host`, `port`, `log_level`, `debug`, `sse_path`, `message_path`, `streamable_http_path`, `json_response`, and `stateless_http`.
+
+A second group is rejected with only a generic "unexpected keyword argument" and no hint, which makes these the ones worth reading in advance:
+
+| SDK v1 `FastMCP(...)` | FastMCP 4 |
+|---|---|
+| `event_store=` | `mcp.http_app(event_store=...)` |
+| `mount_path=` | `mcp.http_app(path=...)` |
+| `transport=` | `mcp.run(transport=...)` |
+| `transport_security=` | `host_origin_protection=`, `allowed_hosts=`, `allowed_origins=` on `http_app()` |
+| `warn_on_duplicate_tools=`, `_resources=`, `_prompts=` | a single `on_duplicate=` |
+| `dependencies=[...]` | a [`fastmcp.json`](/deployment/server-configuration) configuration file |
+| `auth_server_provider=`, `token_verifier=` | a single `auth=` provider — see [Authentication](#authentication) below |
+
+Dropping `event_store=` rather than moving it is the one to watch: it silently disables streamable-HTTP resumability, so a client that reconnects loses the events it missed instead of replaying them.
+
+`name`, `instructions`, `website_url`, `icons`, `tools`, and `lifespan` carry over to the constructor unchanged.
+
+### Authentication
+
+This is the one case where the import change alone won't do. FastMCP 1.0 exposed the SDK's auth plumbing as three separate constructor arguments — `token_verifier=`, `auth_server_provider=`, and `auth=AuthSettings(...)`. The first two raise `TypeError` on FastMCP 4, and while `auth=` survives as a keyword, its value doesn't: FastMCP expects one of its own `AuthProvider` objects rather than the SDK's settings object.
+
+The replacement is a single provider carrying the whole configuration, chosen by what you're actually doing:
+
+| What you were doing | FastMCP provider |
+|---|---|
+| Validating JWTs you already issue | `JWTVerifier` |
+| Delegating to an external authorization server | `RemoteAuthProvider` |
+| Wrapping a provider without Dynamic Client Registration | `OAuthProxy` |
+| GitHub, Google, Auth0, Keycloak, WorkOS, … | the matching named provider |
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import JWTVerifier
+
+mcp = FastMCP("my-server", auth=JWTVerifier(jwks_uri="https://example.com/.well-known/jwks.json"))
+```
+
+See [Authentication](/servers/auth/authentication) for the full set and their configuration.
+
+### Context Methods
+
+`from fastmcp import Context` gets you the injected context object, but four of its methods took a different shape in FastMCP 1.0, and a bare import swap leaves calls that compile and then fail:
+
+| SDK v1 | FastMCP 4 |
+|---|---|
+| `ctx.log(level, data)` | `ctx.log(message, level=...)` — message is first now |
+| `ctx.info(data)` and its `debug`/`warning`/`error` siblings | take a `str` message, where v1 accepted any JSON-serializable value |
+| `ctx.elicit(message, schema=Model)` | `ctx.elicit(message, response_type=Model)` |
+| `ctx.read_resource(uri)` | returns a `ResourceResult`; the payload is under `.contents` rather than being iterable directly |
+
+`ctx.report_progress()`, `ctx.request_id`, and `ctx.client_id` are unchanged.
+
+### Prompts
+
+If your prompt functions return `mcp.types.PromptMessage` objects or raw dicts with `role`/`content` keys, upgrade them to FastMCP's `Message` class. Or just return a plain string — it's automatically wrapped as a user message. FastMCP 1.0 silently coerced dicts into messages; standalone FastMCP requires typed `Message` objects or strings.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("prompts")
+
+@mcp.prompt
+def review(code: str) -> str:
+ """Review code for issues"""
+ return f"Please review this code:\n\n{code}"
+```
+
+Multi-turn prompts return a list of messages. `Message` takes the text positionally and defaults to the user role, so only the assistant turns need a `role`:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.prompts import Message
+
+mcp = FastMCP("prompts")
+
+@mcp.prompt
+def debug(error: str) -> list[Message]:
+ """Start a debugging session"""
+ return [
+ Message(f"I'm seeing this error:\n\n{error}"),
+ Message("I'll help debug that. Can you share the relevant code?", role="assistant"),
+ ]
+```
+
+### Other `mcp.*` Imports
+
+FastMCP 4 builds on MCP SDK v2, which moved the protocol types into a standalone `mcp_types` package and re-exports it as `mcp.types` — so `from mcp.types import X` keeps working. The field names did change, from camelCase to snake_case (`inputSchema` → `input_schema`, `mimeType` → `mime_type`, and so on). For everything else SDK v2 changed, see [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3), which covers the same protocol rebuild from the FastMCP side.
+
+Where FastMCP provides its own API for the same thing, it's worth switching over rather than importing the protocol type:
+
+| MCP SDK v1 | FastMCP equivalent |
+|---|---|
+| `mcp.types.TextContent(type="text", text=str(x))` | Just return `x` from your tool |
+| `mcp.types.ImageContent(...)` | `from fastmcp.utilities.types import Image` |
+| `mcp.types.PromptMessage(...)` | `from fastmcp.prompts import Message` |
+| `mcp.server.fastmcp.Context` | `from fastmcp import Context` |
+| `from mcp.server.stdio import stdio_server` | Not needed — `mcp.run()` handles transport |
+
+For protocol types without a FastMCP equivalent, import them from `mcp_types` directly.
+
+### Decorated Functions
+
+In FastMCP 1.0, `@mcp.tool` replaced your function with a `FunctionTool` object. Now decorators return your original function unchanged, so decorated functions stay callable for testing, reuse, and composition:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("greeter")
+
+@mcp.tool
+def greet(name: str) -> str:
+ """Greet someone"""
+ return f"Hello, {name}!"
+
+# This works now — the function is still a regular function
+assert greet("World") == "Hello, World!"
+```
+
+Code that reads `.name`, `.description`, or other component attributes off the decorated result needs updating. This is uncommon — most servers never touch the tool object. When you do need the component itself, reach it through the server with `await mcp.get_tool("greet")`.
+
+## Verifying the Upgrade
+
+Run your server the way you always have. To confirm every component came across, inspect the server with the FastMCP CLI:
+
+```bash
+fastmcp inspect my_server.py
+```
+
+The output lists every tool, resource, template, and prompt your server exposes, so a component that failed to register shows up here rather than at the first client call.
+
+## Looking Ahead
+
+The MCP ecosystem is evolving fast. Part of FastMCP's job is to absorb that complexity on your behalf — as the protocol and its tooling grow, we do the work so your server code doesn't have to change. The SDK v1 to v2 transition is the clearest example so far: an entire protocol layer was rewritten underneath FastMCP 4, and the servers on this page cross it with one line.
diff --git a/docs/getting-started/upgrading/from-mcp-sdk-v2.mdx b/docs/getting-started/upgrading/from-mcp-sdk-v2.mdx
new file mode 100644
index 000000000..e25489005
--- /dev/null
+++ b/docs/getting-started/upgrading/from-mcp-sdk-v2.mdx
@@ -0,0 +1,328 @@
+---
+title: Upgrading from MCP SDK v2
+sidebarTitle: "From MCP SDK v2"
+description: Move a server built on the MCP Python SDK v2's MCPServer class to FastMCP
+icon: up
+---
+
+If your server starts with `from mcp.server.mcpserver import MCPServer`, you're using the high-level server API introduced in v2 of the `mcp` package. Moving to FastMCP is a mechanical migration: the two APIs share a lineage, so most of your code carries over with a rename.
+
+```python
+# Before
+from mcp.server.mcpserver import MCPServer
+
+server = MCPServer("my-server")
+
+@server.tool()
+def greet(name: str) -> str:
+ """Greet someone by name"""
+ return f"Hello, {name}!"
+
+# After
+from fastmcp import FastMCP
+
+mcp = FastMCP("my-server")
+
+@mcp.tool
+def greet(name: str) -> str:
+ """Greet someone by name"""
+ return f"Hello, {name}!"
+```
+
+That resemblance is not a coincidence. `MCPServer` is the SDK's successor to FastMCP 1.0, the high-level server that shipped inside SDK v1; FastMCP is the standalone framework that grew from the same starting point. Both derive the protocol layer from your function signatures — type hints become JSON Schema, docstrings become descriptions, return values are serialized for you. What separates them is scope: `MCPServer` is the SDK's ergonomic surface over the protocol, while FastMCP builds on that same SDK v2 and adds the machinery a server needs in production — composition, middleware, proxying, authentication providers, tool transformation, a client, and a testing story.
+
+
+Building on the low-level `Server` class instead? See [Upgrading from the Low-Level SDK v2](/getting-started/upgrading/from-low-level-sdk-v2). Still on SDK v1's `mcp.server.fastmcp.FastMCP`? Your upgrade is a single import — see [Upgrading from MCP SDK v1](/getting-started/upgrading/from-mcp-sdk-v1).
+
+
+
+You are migrating an MCP server from the MCP Python SDK v2's high-level `MCPServer` class (`mcp.server.mcpserver`) to FastMCP 4. The two APIs are close relatives, so most of this is mechanical renaming.
+
+FIRST, fetch https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk-v2 — it carries the full mapping table and before-and-after code for everything below. Fetch https://gofastmcp.com for anything the guide doesn't cover. Do not invent a FastMCP API you have not confirmed in the docs.
+
+Then work through the provided code looking for each of these.
+
+IMPORTS AND CONSTRUCTION
+- `MCPServer`, and `Context`, `Image`, `Audio`, `Message` imported from `mcp.server.mcpserver`
+- `mcp_types` imports — these are UNCHANGED. FastMCP 4 builds on the same SDK v2, so leave them alone and say so.
+
+DECORATORS
+- `@server.tool()`, `@server.prompt()` — FastMCP takes a bare `@mcp.tool` / `@mcp.prompt` (and still accepts the called form)
+- `@server.resource(...)`, `@server.completion()`, `@server.custom_route(...)`
+
+TRANSPORT
+- `run(transport="streamable-http")` — FastMCP names this transport "http"
+- `streamable_http_app()`, `sse_app()`
+
+CONSTRUCTOR ARGUMENTS THAT DO NOT CARRY OVER
+- `debug=`, `log_level=`
+- `warn_on_duplicate_tools=` / `_resources=` / `_prompts=`
+- `dependencies=`
+- `title=`, `description=`
+- `token_verifier=`, `auth_server_provider=`, `auth=AuthSettings(...)` — FastMCP consolidates all three into one `auth=` provider
+- `cache_hints=`
+- `extensions=`
+- `tools=[...]` (rare — the SDK's `Tool` type is not exported): FastMCP takes plain callables, so pass the underlying functions
+These raise TypeError, most naming their replacement. `name`, `version`, `instructions`, `icons`, `website_url`, `lifespan`, `resource_security`, and `request_state_security` carry over unchanged.
+
+CONTEXT — these ten properties do NOT exist on FastMCP's Context and raise AttributeError if you only swap the import:
+- `ctx.mcp_server` → `ctx.fastmcp`
+- `ctx.headers` → `get_http_headers()` from `fastmcp.server.dependencies` (a function, not a property)
+- `ctx.protocol_version` → `ctx.request_context.protocol_version`
+- `ctx.client_capabilities` → read it off `ctx.session` / `ctx.request_context`
+- `ctx.notify_tools_changed()`, `notify_resources_changed()`, `notify_prompts_changed()`, `notify_resource_updated()` → `ctx.send_notification(...)` with the matching `mcp_types` notification. FastMCP emits the list-changed ones for you when components change visibility through `ctx.enable_components` / `ctx.disable_components`.
+- `ctx.elicit_url` → not the same thing as `ctx.elicit` (that one is form elicitation, with a different signature and wire behavior). The URL flow survives on the raw session as `ctx.session.elicit_url(...)` — use that rather than deleting an OAuth or payment handoff.
+- `ctx.close_standalone_sse_stream` → no public FastMCP equivalent, and NOT on `ctx.request_context`. Flag it for the user.
+These four exist on both but with DIFFERENT signatures, so a bare import swap compiles and then fails at runtime:
+- `ctx.log(level, data)` → `ctx.log(message, level=...)` — the first positional argument is now the message, not the level
+- `ctx.info(data)` / `debug` / `warning` / `error` → these take `message` as a string, where the SDK accepted any JSON-serializable `data`
+- `ctx.elicit(message, schema=Model)` → `ctx.elicit(message, response_type=Model)` — the keyword was renamed
+- `ctx.read_resource(uri)` → still takes a URI, but returns a `ResourceResult` whose payload is under `.contents`, where the SDK returned an iterable of content objects directly. Code that iterates or indexes the return value needs updating.
+
+Genuinely unchanged: `report_progress`, `request_id`, `client_id`, `input_responses`, `request_state`, `session`, and `request_context`.
+
+RESOLVERS — the one part that is not a rename, so check for it first
+- any `Annotated[T, Resolve(fn)]` parameter, and the resolvers behind it
+- resolvers returning `Elicit[...]`, `Sample`, or `ListRoots`
+FastMCP has no resolver injection, but the underlying requests survive in a different shape: on a modern connection `Elicit`, `Sample`, and `ListRoots` all ride the guard pattern, where the tool returns an `InputRequiredResult` and the client answers on the next call. Do not tell the user these capabilities are simply unavailable. Flag every resolver with the guide's per-capability reasoning (server-side LLM call is usually better than guard-routed sampling; roots are often simplest as ordinary tool arguments) rather than picking a rewrite yourself. Also note that a resolved parameter is hidden from the tool's input schema, so replacing it with an ordinary argument changes the schema clients see.
+
+For each item found, show the original code, name what changed, and give the FastMCP equivalent from the guide. Call out anything you could not find a documented replacement for instead of inventing one.
+
+
+## Install
+
+FastMCP 4 is in prerelease, so pin the exact version rather than installing unqualified — a bare `pip install fastmcp` or `uv add fastmcp` resolves to the latest *stable* release, which today is FastMCP 3:
+
+```bash
+pip install "fastmcp==4.0.0b1"
+# or
+uv add "fastmcp==4.0.0b1"
+```
+
+An exact version pin installs even though it's a prerelease — neither installer needs `--pre` or `--prerelease allow` for a version this specific, only for an open-ended range. For a reproducible lockfile that also pins the prerelease protocol dependencies, see [Install the v4 Prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
+
+FastMCP 4 depends on the MCP SDK v2, so nothing you already import from `mcp_types` moves. That is the practical benefit of migrating at this version rather than an earlier one: you and FastMCP are on the same protocol layer, with the same snake_case field names and the same type package, so the migration touches only the server API.
+
+## The Mechanical Part
+
+Most of the work is renaming. This table covers the surfaces a typical `MCPServer` server touches:
+
+| MCP SDK v2 | FastMCP |
+|---|---|
+| `from mcp.server.mcpserver import MCPServer` | `from fastmcp import FastMCP` |
+| `from mcp.server.mcpserver import Context` | `from fastmcp import Context` |
+| `from mcp.server.mcpserver import Image, Audio` | `from fastmcp.utilities.types import Image, Audio` |
+| `from mcp.server.mcpserver.prompts.base import Message` | `from fastmcp.prompts import Message` |
+| `@server.tool()` | `@mcp.tool` |
+| `@server.prompt()` | `@mcp.prompt` |
+| `@server.resource("uri://x")` | `@mcp.resource("uri://x")` |
+| `@server.completion()` | `@mcp.completion` |
+| `@server.custom_route(path, methods)` | `@mcp.custom_route(path, methods)` |
+| `server.run(transport="streamable-http")` | `mcp.run(transport="http")` |
+| `server.streamable_http_app()` | `mcp.http_app()` |
+| `server.sse_app()` | `mcp.http_app(transport="sse")` |
+| `ctx.mcp_server` | `ctx.fastmcp` |
+| `ctx.headers` | `get_http_headers()` from `fastmcp.server.dependencies` |
+| `ctx.protocol_version` | `ctx.request_context.protocol_version` |
+| `ctx.client_capabilities` | read it off `ctx.session` |
+| `from mcp_types import X` | unchanged |
+
+Two of these are worth a sentence each. The decorators lose their parentheses: `MCPServer` required `@server.tool()` and raised a `TypeError` telling you so if you wrote `@server.tool`, while FastMCP accepts both forms, so `@mcp.tool` is the idiomatic spelling and `@mcp.tool()` keeps working if you'd rather not touch every line. And the streamable HTTP transport is named `"http"` in FastMCP rather than `"streamable-http"` — the transport is the same, and `mcp.run()` still defaults to stdio.
+
+Here is a complete server before and after. Nothing in the logic changes:
+
+
+
+```python Before
+import json
+from mcp.server.mcpserver import MCPServer, Context
+
+server = MCPServer("demo")
+
+@server.tool()
+def greet(name: str) -> str:
+ """Greet someone by name"""
+ return f"Hello, {name}!"
+
+@server.tool()
+async def process(items: list[str], ctx: Context) -> str:
+ """Process a batch of items"""
+ for i, item in enumerate(items):
+ await ctx.report_progress(i, len(items))
+ return f"Processed {len(items)} items"
+
+@server.resource("config://app", mime_type="application/json")
+def app_config() -> str:
+ """Application configuration"""
+ return json.dumps({"debug": False})
+
+@server.resource("users://{user_id}/profile")
+def profile(user_id: str) -> str:
+ """User profile by ID"""
+ return json.dumps({"id": user_id})
+
+@server.prompt()
+def summarize(text: str) -> str:
+ """Summarize text"""
+ return f"Summarize:\n\n{text}"
+
+if __name__ == "__main__":
+ server.run(transport="streamable-http")
+```
+
+```python After
+import json
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP("demo")
+
+@mcp.tool
+def greet(name: str) -> str:
+ """Greet someone by name"""
+ return f"Hello, {name}!"
+
+@mcp.tool
+async def process(items: list[str], ctx: Context) -> str:
+ """Process a batch of items"""
+ for i, item in enumerate(items):
+ await ctx.report_progress(i, len(items))
+ return f"Processed {len(items)} items"
+
+@mcp.resource("config://app", mime_type="application/json")
+def app_config() -> str:
+ """Application configuration"""
+ return json.dumps({"debug": False})
+
+@mcp.resource("users://{user_id}/profile")
+def profile(user_id: str) -> str:
+ """User profile by ID"""
+ return json.dumps({"id": user_id})
+
+@mcp.prompt
+def summarize(text: str) -> str:
+ """Summarize text"""
+ return f"Summarize:\n\n{text}"
+
+if __name__ == "__main__":
+ mcp.run(transport="http")
+```
+
+
+
+## Constructor Arguments
+
+`FastMCP()` describes your server's identity and behavior; how it gets deployed is decided when you serve it. Several `MCPServer` constructor arguments move accordingly, and each raises a `TypeError` naming its replacement rather than being silently ignored.
+
+`name`, `version`, `instructions`, `icons`, `website_url`, `lifespan`, `resource_security`, and `request_state_security` all mean what they meant before. The rest map like this:
+
+| `MCPServer(...)` | FastMCP |
+|---|---|
+| `debug=True` | `FASTMCP_DEBUG` environment variable |
+| `log_level="DEBUG"` | `run_http_async(log_level=...)` or `FASTMCP_LOG_LEVEL` |
+| `warn_on_duplicate_tools`, `_resources`, `_prompts` | a single `on_duplicate=` |
+| `dependencies=[...]` | a [`fastmcp.json`](/deployment/server-configuration) configuration file |
+| `title=`, `description=` | `instructions=` |
+| `tools=[Tool, ...]` | `tools=[callable, ...]`, or FastMCP's own `Tool` |
+| `resources=[Resource, ...]` | no constructor keyword — register with `@mcp.resource` or `mcp.add_resource()` |
+| `subscriptions=` | no equivalent — see below |
+| `token_verifier=`, `auth_server_provider=`, `auth=AuthSettings(...)` | a single `auth=` provider |
+| `cache_hints={...}` | `cache_ttl=`, `cache_scope=` |
+| `extensions=[...]` | `mcp.add_extension(...)` |
+| `middleware=[ServerMiddleware, ...]` | `middleware=[Middleware, ...]` — same keyword, different class |
+
+`middleware=` is the row most likely to be mistaken for a rename. Both constructors take a `middleware=` sequence, but an `MCPServer` wants the SDK's `ServerMiddleware` — one hook wrapping every raw JSON-RPC message — while FastMCP wants its own `Middleware`, which adds typed per-operation hooks (`on_call_tool`, `on_list_tools`, and the rest) on top of the same message-level pass. Keeping the keyword and swapping the base class is the migration; see [Middleware](/servers/middleware).
+
+Authentication is the largest of these, and it consolidates rather than moves. `MCPServer` exposes the SDK's raw auth plumbing — a token verifier, an authorization-server provider, and an `AuthSettings` object, configured separately. FastMCP takes one `auth=` provider that carries the whole configuration, and ships providers for the common cases: `JWTVerifier` for validating tokens you already issue, `RemoteAuthProvider` for delegating to an external authorization server, `OAuthProxy` for wrapping a provider that lacks Dynamic Client Registration, and named providers for GitHub, Google, Auth0, Keycloak, WorkOS, and others. See [Authentication](/servers/auth/authentication).
+
+Two rows are worth reading before you delete the argument. `resources=` has no constructor equivalent, so pre-built `Resource` objects need registering through `@mcp.resource` or `mcp.add_resource()` instead — dropping the keyword silently drops the resources with it. And `subscriptions=`, which an `MCPServer` uses to plug in an external pub/sub bus so resource-update notifications reach clients across replicas, has no FastMCP equivalent at all. A multi-replica deployment that relies on it should confirm it can live without cross-replica subscription fan-out before migrating, because a mechanical rename removes that behavior without any error to warn you.
+
+### Serving HTTP
+
+Renaming `streamable_http_app()` to `http_app()` is only mechanical for a call with no arguments. The keywords were renamed and regrouped, so an existing call carries arguments `http_app()` does not accept:
+
+| SDK v2 | FastMCP |
+|---|---|
+| `streamable_http_app(streamable_http_path=...)` | `http_app(path=...)` |
+| `sse_app(sse_path=...)` | `http_app(path=..., transport="sse")` |
+| `sse_app(message_path=...)` | no equivalent |
+| `transport_security=TransportSecuritySettings(...)` | `host_origin_protection=`, `allowed_hosts=`, `allowed_origins=` |
+| `host=...` | pass to `mcp.run(host=...)` instead |
+
+`json_response`, `stateless_http`, `event_store`, and `retry_interval` keep their names. See [Deploying HTTP servers](/deployment/http) for the host and origin settings.
+
+### Stricter Arguments
+
+One behavior change survives the rename and is worth knowing before you migrate. `MCPServer` binds the arguments it recognizes and ignores the rest, so a call carrying an unexpected key succeeds. FastMCP declares `"additionalProperties": false` on every generated schema and enforces it, so the same call fails:
+
+```python test="skip"
+# Against MCPServer: succeeds, "extra" ignored.
+# Against FastMCP: raises, "extra" is not a parameter of greet().
+await client.call_tool("greet", {"name": "World", "extra": "surprise"})
+```
+
+For most servers this is an improvement that costs nothing — a caller sending keys your tool never reads was already a bug. It matters if a client in your fleet passes extra metadata alongside real arguments, since those calls start failing the moment you migrate. Accept the extras explicitly as optional parameters if you need to keep them working.
+
+## Asking for Input
+
+This is the one part of the migration that is not a rename, so read it before you start if your tools use resolvers.
+
+`MCPServer` asks the client for things through dependency-injection resolvers. A tool parameter annotated `Annotated[T, Resolve(fn)]` is filled by running `fn` before the tool body, and the resolver can return a request marker — `Elicit[T]` to ask the user, `Sample` to borrow the client's model, `ListRoots` to fetch its roots — which the framework turns into the right wire interaction for whichever protocol era the connection negotiated:
+
+```python
+from typing import Annotated
+from pydantic import BaseModel
+from mcp.server.mcpserver import MCPServer, Resolve, Elicit
+
+server = MCPServer("booking")
+
+
+class Destination(BaseModel):
+ destination: str
+
+
+def ask_destination() -> Elicit[Destination]:
+ return Elicit("Where would you like to fly?", Destination)
+
+
+@server.tool()
+def book_flight(dest: Annotated[Destination, Resolve(ask_destination)]) -> str:
+ """Book a flight"""
+ return f"Booked to {dest.destination}"
+```
+
+FastMCP has no equivalent annotation, and it makes the protocol era explicit instead of hiding it. Which replacement you want depends on which era your clients speak.
+
+On **handshake-era connections** (≤ 2025-11-25), a running tool asks the user directly with `ctx.elicit()`, and the call blocks until the answer arrives. Where the resolver returned a value or aborted the call, `ctx.elicit()` hands you the outcome to branch on, so declining and cancelling become cases your tool answers for itself:
+
+```python
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP("booking")
+
+
+@mcp.tool
+async def book_flight(ctx: Context) -> str:
+ """Book a flight"""
+ result = await ctx.elicit("Where would you like to fly?", response_type=str)
+ if result.action == "accept":
+ return f"Booked to {result.data}"
+ return "Booking cancelled"
+```
+
+On the **modern protocol** (2026-07-28), server-initiated requests are gone from the wire, so a tool asks by *returning* a description of what it needs. The client answers and calls the tool again with the answer attached, and the tool re-runs from the top. This is the [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol), and it reads the answers off `ctx.input_responses`.
+
+The two are era-gated in both directions: `ctx.elicit()` raises on a modern connection, and a guard result raises on a handshake one. A server that must serve both branches on `ctx.request_context.protocol_version`. See [Elicitation](/servers/elicitation#which-approach-to-use) for both shapes side by side.
+
+Resolvers that return `Sample` or `ListRoots` have no *injected* equivalent — FastMCP has no `ctx.sample()` or `ctx.list_roots()` — but the underlying request survives, so this is a change of shape rather than a loss of capability. On a modern connection both ride the same guard pattern as elicitation: the tool returns an `InputRequiredResult` describing the sampling or roots request, and the client answers on the next call.
+
+Which shape you want differs by capability. For **roots**, the guard route is the natural replacement, since one round buys the whole answer — and taking the paths as ordinary tool arguments is simpler still whenever the caller can supply them. For **generation**, prefer [calling an LLM from your server](/servers/sampling) with your own API key: your tool then behaves identically for every client, including the many that never implemented sampling, and you avoid paying a full request-response cycle per generation step. Reach for the guard route when using the *caller's* model is specifically the point.
+
+One schema detail is easy to miss during the rewrite. A resolved parameter never appears in the tool's input schema — `book_flight` above advertises no arguments at all. When you replace a resolver with an explicit tool argument, the schema the client sees gains a field, which is usually what you want but is a visible change to your tool's contract.
+
+## What You Gain
+
+The migration is worth doing for what sits on the other side of it. FastMCP is a framework rather than a protocol surface, and these are the capabilities that most often motivate the move:
+
+[Server composition](/servers/composition) mounts one server inside another, so a large surface splits into modules that are developed and tested independently. [Middleware](/servers/middleware) runs across every request for logging, rate limiting, error handling, and caching, with hooks at whichever level of specificity you need. [Proxy servers](/servers/providers/proxy) put a FastMCP server in front of any existing MCP server, bridging transports and adding auth to a backend you don't control. The [OpenAPI integration](/integrations/openapi) generates a whole server from an existing API specification. [Tool transformation](/servers/transforms/transforms) rewrites the tools a server exposes — renaming, hiding, and reshaping arguments — without touching the code that defines them.
+
+FastMCP also ships a [client](/clients/client), which `MCPServer` has no counterpart for. It speaks every transport, drives both protocol eras, and connects to a server object in-process — so [testing](/servers/testing) a server means calling its tools in the same Python process, with no subprocess and no network.
diff --git a/docs/getting-started/welcome.mdx b/docs/getting-started/welcome.mdx
index 28932284f..5c13e6bdf 100644
--- a/docs/getting-started/welcome.mdx
+++ b/docs/getting-started/welcome.mdx
@@ -1,26 +1,11 @@
---
-title: "Welcome to FastMCP"
+title: "FastMCP: The Framework for MCP"
sidebarTitle: "Welcome!"
-description: The fast, Pythonic way to build MCP servers, clients, and applications.
+description: FastMCP is the standard framework for building Model Context Protocol (MCP) servers, clients, and interactive applications.
icon: hand-wave
mode: center
---
-{/*
-
-
-
- */}
+**FastMCP is a full framework for building [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) applications.** It gives you one coherent API for servers, clients, and interactive apps. Use it to expose Python functions as MCP tools, connect to local or remote MCP servers, and return interactive interfaces directly from your tools. FastMCP manages schema generation, validation, transport, authentication, and protocol compatibility around your application code.
-**FastMCP is the standard framework for building MCP applications.** The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) connects LLMs to tools and data. FastMCP gives you everything you need to go from prototype to production — build servers that expose capabilities, connect clients to any MCP service, and give your tools interactive UIs:
+A FastMCP server starts with ordinary Python:
```python {1}
from fastmcp import FastMCP
mcp = FastMCP("Demo 🚀")
+
@mcp.tool
def add(a: int, b: int) -> int:
- """Add two numbers"""
+ """Add two numbers."""
return a + b
+
if __name__ == "__main__":
mcp.run()
```
+## Move fast and make things
-## Move Fast and Make Things
+An effective MCP application needs more than a function registry. Models need accurate schemas, callers need validated results, clients need compatible transports, and production servers need authentication and predictable lifecycle management.
-The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) lets you give agents access to your tools and data. But building an effective MCP application is harder than it looks.
+FastMCP treats those as framework responsibilities. Declare a Python function and FastMCP derives its schema, validates its inputs and outputs, and exposes it through MCP. Connect a client to a URL and FastMCP handles protocol negotiation, authentication, and connection lifecycle. Your application remains ordinary Python while FastMCP keeps the MCP boundary correct.
-FastMCP handles all of it. Declare a tool with a Python function, and the schema, validation, and documentation are generated automatically. Connect to a server with a URL, and transport negotiation, authentication, and protocol lifecycle are managed for you. You focus on your logic, and the MCP part just works: **with FastMCP, best practices are built in.**
+**That's why FastMCP is the standard framework for working with MCP.** FastMCP created the high-level Python API incorporated into the official MCP Python SDK in 2024. The actively maintained standalone project is now downloaded more than a million times a day, and some version of FastMCP powers 70% of MCP servers across all languages.
-**That's why FastMCP is the standard framework for working with MCP.** FastMCP 1.0 was incorporated into the official MCP Python SDK in 2024. Today, the actively maintained standalone project is downloaded a million times a day, and some version of FastMCP powers 70% of MCP servers across all languages.
+## Servers, clients, and apps
-FastMCP has three pillars:
+FastMCP covers the full MCP application lifecycle through three complementary pillars:
- Expose tools, resources, and prompts to LLMs.
+ Expose Python functions, data, and instructions as MCP tools, resources, and prompts.
- Give your tools interactive UIs rendered directly in the conversation.
+ Give MCP tools interactive user interfaces rendered directly in the conversation.
- Connect to any MCP server — local or remote, programmatic or CLI.
+ Connect to any MCP server through Python, the command line, or another MCP application.
-**[Servers](/servers/server)** wrap your Python functions into MCP-compliant tools, resources, and prompts. **[Clients](/clients/client)** connect to any server with full protocol support. And **[Apps](/apps/overview)** give your tools interactive UIs rendered directly in the conversation.
+**[Servers](/servers/server)** turn your application logic into MCP capabilities with generated schemas and validation. **[Clients](/clients/client)** connect to local or remote MCP servers with full protocol support. **[Apps](/apps/overview)** let tools return forms, tables, charts, and other interactive interfaces alongside ordinary MCP results.
-Ready to build? Start with the [installation guide](/getting-started/installation) or jump straight to the [quickstart](/getting-started/quickstart). When you're ready to deploy, [Prefect Horizon](https://www.prefect.io/horizon) offers free hosting for FastMCP users.
+The three pillars share one model: FastMCP owns the protocol machinery while your code defines what the application does.
+
+**Building in TypeScript?** [FastMCP for TypeScript](https://github.com/PrefectHQ/fastmcp-ts) is the official counterpart, built and maintained by the same team. Its servers, clients, and apps follow the same concepts, so what you learn here carries over.
+
+
+
+ Add FastMCP to your project with `uv add fastmcp`, verify the package, and find the right upgrade guide.
+
+
+ Create a tool, run its server, call it from a client, and add an interactive UI.
+
+
FastMCP is made with 💙 by [Prefect](https://www.prefect.io/).
-**This documentation reflects FastMCP's `main` branch**, meaning it always reflects the latest development version. Features are generally marked with version badges (e.g. `New in version: 3.0.0`) to indicate when they were introduced. Note that this may include features that are not yet released.
+**This documentation reflects FastMCP's `main` branch**, so it may describe features that have not reached a stable release. Version badges identify when features were introduced.
-## LLM-Friendly Docs
+## Scale MCP with Horizon
-The FastMCP documentation is available in multiple LLM-friendly formats:
+FastMCP handles the MCP application layer. **[Prefect Horizon](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_welcome&utm_content=welcome_body)** is the enterprise MCP gateway for scaling servers and tools across teams, with centralized governance over how they are deployed, discovered, secured, and used.
-### MCP Server
+Horizon applies the operational patterns developed while maintaining FastMCP: deploy servers from GitHub with branch previews and instant rollback, organize them in a private registry, protect access with SSO and tool-level RBAC, and observe activity through audit logs and telemetry.
-The FastMCP docs are accessible via MCP! The server URL is `https://gofastmcp.com/mcp`.
+Horizon can also combine approved tools into purpose-built MCP endpoints for different teams and agents, while keeping access policy and governance centralized.
-In fact, you can use FastMCP to search the FastMCP docs:
+Start with FastMCP. [Scale with Horizon →](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_welcome&utm_content=welcome_cta)
+
+## LLM-friendly docs
+
+FastMCP documentation is designed for developers and coding agents. Every page is available as Markdown, the complete documentation is published in `llms.txt` formats, and the documentation itself is exposed through an MCP server.
+
+### MCP server
+
+Point any MCP-compatible agent at `https://gofastmcp.com/mcp` to let it search the documentation as it works. You can also connect with FastMCP's Python client directly:
```python
import asyncio
+
from fastmcp import Client
-async def main():
+
+async def main() -> None:
async with Client("https://gofastmcp.com/mcp") as client:
result = await client.call_tool(
- name="SearchFastMcp",
- arguments={"query": "deploy a FastMCP server"}
+ name="search_fast_mcp",
+ arguments={"query": "deploy a FastMCP server"},
)
- print(result)
+ print(result)
+
asyncio.run(main())
```
-### Text Formats
+### Markdown formats
-The docs are also available in [llms.txt format](https://llmstxt.org/):
-- [llms.txt](https://gofastmcp.com/llms.txt) - A sitemap listing all documentation pages
-- [llms-full.txt](https://gofastmcp.com/llms-full.txt) - The entire documentation in one file (may exceed context windows)
+The documentation is also available in [`llms.txt`](https://llmstxt.org/) formats:
-Any page can be accessed as markdown by appending `.md` to the URL. For example, this page becomes `https://gofastmcp.com/getting-started/welcome.md`.
+- [`llms.txt`](https://gofastmcp.com/llms.txt) lists every documentation page.
+- [`llms-full.txt`](https://gofastmcp.com/llms-full.txt) contains the complete documentation in one file and may exceed some context windows.
-You can also copy any page as markdown by pressing "Cmd+C" (or "Ctrl+C" on Windows) on your keyboard.
+Append `.md` to any documentation URL to retrieve that page as Markdown. For example, this page is available at `https://gofastmcp.com/getting-started/welcome.md`. You can also copy the current page as Markdown by pressing `Cmd+C` or `Ctrl+C`.
diff --git a/docs/getting-started/whats-new.mdx b/docs/getting-started/whats-new.mdx
new file mode 100644
index 000000000..cebc3e682
--- /dev/null
+++ b/docs/getting-started/whats-new.mdx
@@ -0,0 +1,236 @@
+---
+title: "What's New in FastMCP 4"
+sidebarTitle: "What's New"
+description: FastMCP 4 makes stateful MCP applications work on the sessionless protocol while one server serves every protocol era.
+icon: sparkles
+---
+
+FastMCP 4 makes stateful MCP applications work on MCP's sessionless protocol. Tools can ask follow-up questions across requests, preserve authenticated user state, and move long-running work into background tasks without sticky sessions or a continuously connected client.
+
+The protocol changed completely underneath those APIs. Your application usually does not: one FastMCP server negotiates both protocol eras per connection, and most FastMCP 3 servers upgrade unchanged.
+
+That is the theme of version 4: stateless transport without stateless application code. The release also makes protocol extensions a first-class surface, adds enterprise identity for agents acting on behalf of users, and strengthens production defaults across caching, routing, and security.
+
+
+FastMCP 4 is in **beta**. Pin an exact version and expect sharp edges. See [Install the v4 prerelease](/getting-started/upgrading/from-fastmcp-3#install-the-v4-prerelease).
+
+
+## Protocol compatibility
+
+A protocol migration usually forces a choice between breaking clients that have not moved yet and holding the server back with them. FastMCP 4 serves both eras from one deployment, negotiating the best mutual version for each connection. Modern clients get the sessionless protocol while handshake-era clients continue working unchanged.
+
+Statelessness changes how that deployment scales. Each modern request carries everything needed to answer it, so any replica behind an ordinary load balancer can serve any request and session affinity stops being a requirement.
+
+The client default follows the same rule. `Client(url)` probes for the modern protocol and falls back to the handshake when necessary. Pin `mode="legacy"` only when your application specifically needs the session back-channel.
+
+```python
+from fastmcp import Client
+
+# Negotiate the best mutual protocol
+client = Client("https://example.com/mcp")
+
+# Require the handshake-era protocol
+legacy = Client("https://example.com/mcp", mode="legacy")
+```
+
+Once connected, `client.protocol_version`, `client.server_info`, `client.server_capabilities`, and `client.instructions` expose the same interface whichever era was negotiated. Application code that inspects a server does not need a protocol-version branch. See [Protocol negotiation](/clients/client#protocol-negotiation).
+
+On modern connections, FastMCP also attaches the method, target name, and opted-in argument values as HTTP headers. Gateways and load balancers can route requests without parsing JSON-RPC bodies. See [Gateway routing headers](/deployment/http#gateway-routing-headers).
+
+## Stateful applications
+
+The modern protocol removes transport-level sessions, but applications still need conversations, user state, and long-running work. FastMCP moves those concerns into explicit application primitives that survive fresh connections. Shared stores and request-state keys extend them across replicas and worker restarts.
+
+### Interactive tools
+
+Many useful tools need more than one exchange. A booking tool asks for a destination, then a date, then confirmation. A destructive operation asks the user to approve it before continuing.
+
+On the modern protocol, the tool returns a description of the input it needs. That result completes the request normally. The client fulfils the request and calls the tool again with the answer attached; the tool runs from the top, reads `ctx.input_responses`, and either asks another question or returns its final result.
+
+Each request completes while the user responds. Single-process servers use an automatic process-local key to protect the state carried between rounds; load-balanced deployments configure one shared key so any replica can validate and resume the next round:
+
+```python
+import os
+
+from fastmcp import Context, FastMCP
+from mcp.server.request_state import RequestStateSecurity
+from mcp.types import ElicitRequest, ElicitRequestFormParams, InputRequiredResult
+
+mcp = FastMCP(
+ "Booking",
+ request_state_security=RequestStateSecurity(
+ keys=[os.environ["REQUEST_STATE_KEY"].encode()]
+ ),
+)
+
+
+@mcp.tool
+async def book_flight(ctx: Context) -> str | InputRequiredResult:
+ answers = ctx.input_responses
+ if answers is None:
+ params = ElicitRequestFormParams(
+ message="Where would you like to fly?",
+ requested_schema={
+ "type": "object",
+ "properties": {"destination": {"type": "string"}},
+ "required": ["destination"],
+ },
+ )
+ return InputRequiredResult(
+ result_type="input_required",
+ input_requests={
+ "destination": ElicitRequest(
+ method="elicitation/create",
+ params=params,
+ )
+ },
+ )
+
+ response = answers["destination"]
+ if response.action != "accept" or response.content is None:
+ return "Booking cancelled."
+
+ destination = response.content["destination"]
+ return f"Booked a flight to {destination}."
+```
+
+Every replica must receive the same `REQUEST_STATE_KEY`, containing at least 32 bytes of secret key material. A FastMCP client drives the loop through its existing elicitation handler, so client code receives the terminal result without managing the intermediate rounds. See [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol).
+
+### Session state
+
+Application state follows the same explicit model. FastMCP stores state server-side and binds it to the authenticated user, so a session handle is inert in another user's hands.
+
+Most tools want one state bucket per user. Declare a `UserSession` parameter and FastMCP injects it like `Context`: it never appears in the tool schema, and the caller passes nothing because their authenticated identity selects the bucket.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.sessions import UserSession
+
+mcp = FastMCP("Assistant")
+
+
+@mcp.tool
+async def remember(fact: str, session: UserSession) -> str:
+ facts = await session.get("facts", default=[])
+ facts.append(fact)
+ await session.set("facts", facts)
+ return f"Remembered {len(facts)} facts."
+```
+
+`UserSession` requires [authentication](/servers/auth/authentication), since an unauthenticated request has no user to key on. When one user needs several independent buckets, such as separate carts or conversations, `SessionId` exposes the handle as an explicit string argument.
+
+The default in-memory state store is process-local. To preserve state across restarts or share it among replicas, pass a shared persistent `session_state_store`. See [Session state](/servers/sessions).
+
+### Background work
+
+Long-running tools create a different kind of state problem: holding a request open for several minutes invites timeouts and leaves the user unable to tell whether work is progressing. Background tasks accept the call and return a handle immediately, then let the client poll while work proceeds asynchronously.
+
+FastMCP implements the `io.modelcontextprotocol/tasks` extension in the optional `fastmcp-tasks` package. The authoring API remains `@mcp.tool(task=True)`, backed by [Docket](https://github.com/chrisguidry/docket):
+
+```python
+import asyncio
+
+from fastmcp import FastMCP
+from fastmcp_tasks import TasksExtension
+
+mcp = FastMCP("MyServer")
+mcp.add_extension(TasksExtension())
+
+
+@mcp.tool(task=True)
+async def slow_computation(duration: int) -> str:
+ """Run a long computation."""
+ await asyncio.sleep(duration)
+ return f"Completed in {duration} seconds"
+```
+
+`fastmcp.Client` handles the task handle and polling cycle, so `client.call_tool(...)` returns the same way whether the tool ran inline or in the background. See [Background tasks](/servers/tasks).
+
+`TasksExtension()` uses an in-memory, single-process backend by default. Configure a Redis or Valkey backend for durable work that survives restarts and runs across separate workers.
+
+## Extensible protocol
+
+Background tasks are built on a general extension surface. An MCP extension advertises a capability under a reverse-DNS identifier and can add behavior negotiated between a server and client.
+
+### Server extensions
+
+`FastMCP.add_extension()` lets an extension advertise capabilities, add request methods, intercept `tools/call`, and own lifespan behavior with access to the component registry, `Context`, and authentication. Client extensions use the matching `Client(extensions=...)` interface.
+
+Cross-cutting protocol behavior can therefore live in a supported plugin instead of requiring changes to FastMCP core. `TasksExtension` is a complete example of the interface. See [Server extensions](/servers/extensions).
+
+### Argument completion
+
+FastMCP 4 also lets servers answer MCP argument-completion requests. A completion handler sees the prompt or resource-template argument, its partial value, and values already supplied, so suggestions can depend on earlier choices.
+
+```python
+from fastmcp import FastMCP
+from mcp.types import PromptReference
+
+mcp = FastMCP("Docs")
+
+
+@mcp.prompt
+def write_poem(theme: str) -> str:
+ return f"Write a poem about {theme}"
+
+
+@mcp.completion
+def complete(ref, argument, context):
+ if isinstance(ref, PromptReference) and argument.name == "theme":
+ options = ["nature", "love", "adventure"]
+ return [option for option in options if option.startswith(argument.value)]
+ return None
+```
+
+Registering the handler advertises the completion capability during negotiation, so clients only send requests to servers that support them. See [Argument completion](/servers/completions).
+
+## Enterprise identity
+
+Interactive OAuth authorization assumes a person can complete a browser flow. Internal agents often act for employees without a person waiting at a keyboard, while the server still needs the employee's identity for authorization and audit.
+
+Identity assertion carries that identity through the agent. A corporate identity provider signs an assertion, the agent presents it, and the server exchanges it for a short-lived token without an interactive login or consent screen. FastMCP performs signature verification, binding checks, replay rejection, and scoped token issuance through the authentication providers you already use.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import IdentityAssertion, OAuthProxy
+
+auth = OAuthProxy(
+ # Existing upstream configuration
+ identity_assertion=IdentityAssertion(
+ trusted_issuers=["https://login.acme-corp.com"]
+ ),
+)
+mcp = FastMCP("Internal API", auth=auth)
+```
+
+The asserted subject enters the normal authentication context, so tools read it through `get_access_token()` like any other identity. See [Identity assertion](/servers/auth/oauth-proxy#identity-assertion-sep-990).
+
+Authorization gained a provider-neutral role check as well. `require_roles` accepts an extraction function for providers that store roles and groups under different claims, while [scope step-up challenges](/servers/authorization#signaling-scope-shortfalls) tell a client exactly which scopes to request.
+
+For clients with no user behind them, such as backend services and scheduled jobs, `ClientCredentialsOAuthProvider` implements the OAuth 2.0 client-credentials grant with no browser or redirect. See [Machine-to-machine authentication](/clients/auth/client-credentials).
+
+## Production defaults
+
+A server can now attach freshness hints to its results, and a caching client can reuse those results without another round trip. Set a default time-to-live and scope on the server:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("Weather", cache_ttl=300, cache_scope="public")
+```
+
+`KeyValueResponseCacheStore` can place the client cache in Redis or another key-value store so a fleet of clients or proxies shares fills. See [Response caching](/clients/client#response-caching).
+
+Resource templates now reject path traversal, absolute paths, and null bytes in their parameters before the handler runs. The protection is enabled by default and applies to mounted and proxied templates. See [Path security](/servers/resources#path-security).
+
+OAuth defaults also distinguish native clients from web applications during Dynamic Client Registration, and missing scopes now produce an `InsufficientScopeError` that names the scopes required to continue. See [Application type](/servers/auth/oauth-proxy#application-type-web-vs-native) and [scope shortfalls](/servers/authorization#signaling-scope-shortfalls).
+
+## Upgrade note
+
+The sessionless protocol has no live connection for a server to call back into during execution. FastMCP 4 therefore removes `ctx.sample()`, `ctx.sample_step()`, and `ctx.list_roots()` from every protocol era so incompatible code fails immediately during an upgrade.
+
+For generation, call an LLM directly from the server when your application owns the model. When borrowing the caller's model is the point, return an `InputRequiredResult` carrying a sampling request and read the answer on the next round. Roots use the same return-and-resume pattern. See [Sampling](/servers/sampling) and [the guard pattern](/servers/elicitation#sampling-and-roots).
+
+`ctx.elicit()` remains available on handshake-era connections; modern connections use the multi-round pattern described above. Code that constructs MCP protocol models directly must also use snake_case Python field names with SDK v2.
+
+[Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) covers these changes and every other compatibility break.
diff --git a/docs/integrations/anthropic.mdx b/docs/integrations/anthropic.mdx
index b8156ac95..6fb8841e1 100644
--- a/docs/integrations/anthropic.mdx
+++ b/docs/integrations/anthropic.mdx
@@ -69,9 +69,11 @@ You'll also need to authenticate with Anthropic. You can do this by setting the
export ANTHROPIC_API_KEY="your-api-key"
```
-Here is an example of how to call your server from Python. Note that you'll need to replace `https://your-server-url.com` with the actual URL of your server. In addition, we use `/mcp/` as the endpoint because we deployed a streamable-HTTP server with the default path; you may need to use a different endpoint if you customized your server's deployment. **At this time you must also include the `extra_headers` parameter with the `anthropic-beta` header.**
+Here is an example of how to call your server from Python. Note that you'll need to replace `https://your-server-url.com` with the actual URL of your server. In addition, we use `/mcp/` as the endpoint because we deployed a streamable-HTTP server with the default path; you may need to use a different endpoint if you customized your server's deployment.
-```python {5, 13-22}
+The connector is in beta, so the call goes through `client.beta.messages` with the `mcp-client-2025-11-20` flag. Each entry in `mcp_servers` also needs a matching `mcp_toolset` entry in `tools` that references it by name; declaring the server without the toolset is rejected as a validation error.
+
+```python {5, 14-23}
import anthropic
from rich import print
@@ -81,8 +83,9 @@ url = 'https://your-server-url.com'
client = anthropic.Anthropic()
response = client.beta.messages.create(
- model="claude-sonnet-4-20250514",
+ model="claude-sonnet-5",
max_tokens=1000,
+ betas=["mcp-client-2025-11-20"],
messages=[{"role": "user", "content": "Roll a few dice!"}],
mcp_servers=[
{
@@ -91,9 +94,7 @@ response = client.beta.messages.create(
"name": "dice-server",
}
],
- extra_headers={
- "anthropic-beta": "mcp-client-2025-04-04"
- }
+ tools=[{"type": "mcp_toolset", "mcp_server_name": "dice-server"}],
)
print(response.content)
@@ -181,7 +182,7 @@ if __name__ == "__main__":
If you try to call the authenticated server with the same Anthropic code we wrote earlier, you'll get an error indicating that the server rejected the request because it's not authenticated.
-```python
+```text
Error code: 400 - {
"type": "error",
"error": {
@@ -193,7 +194,7 @@ Error code: 400 - {
To authenticate the client, you can pass the token using the `authorization_token` parameter in your MCP server configuration:
-```python {8, 21}
+```python {8, 22}
import anthropic
from rich import print
@@ -206,8 +207,9 @@ access_token = 'your-access-token'
client = anthropic.Anthropic()
response = client.beta.messages.create(
- model="claude-sonnet-4-20250514",
+ model="claude-sonnet-5",
max_tokens=1000,
+ betas=["mcp-client-2025-11-20"],
messages=[{"role": "user", "content": "Roll a few dice!"}],
mcp_servers=[
{
@@ -217,9 +219,7 @@ response = client.beta.messages.create(
"authorization_token": access_token
}
],
- extra_headers={
- "anthropic-beta": "mcp-client-2025-04-04"
- }
+ tools=[{"type": "mcp_toolset", "mcp_server_name": "dice-server"}],
)
print(response.content)
diff --git a/docs/integrations/auth0.mdx b/docs/integrations/auth0.mdx
index 65f9d3873..ce2dcc670 100644
--- a/docs/integrations/auth0.mdx
+++ b/docs/integrations/auth0.mdx
@@ -9,9 +9,54 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
-This guide shows you how to secure your FastMCP server using **Auth0 OAuth**. While Auth0 does have support for Dynamic Client Registration, it is not enabled by default so this integration uses the [**OIDC Proxy**](/servers/auth/oidc-proxy) pattern to bridge Auth0's dynamic OIDC configuration with MCP's authentication requirements.
+FastMCP supports two Auth0 integration paths:
-## Configuration
+- **[Auth for MCP](#auth-for-mcp-dcr)** — Auth0 handles OAuth, DCR, and CIMD; FastMCP validates tokens (`Auth0MCPProvider`). Use this for MCP-native clients and Auth0's [Auth for MCP](https://auth0.com/ai/docs/mcp/intro/overview) setup.
+- **[OIDC Proxy](#oidc-proxy-fixed-credentials)** — FastMCP proxies OAuth with fixed application credentials (`Auth0Provider`). Use this when you manage an Auth0 application manually and do not need tenant-level DCR.
+
+## Auth for MCP (DCR)
+
+
+
+This path uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern. Auth0 acts as the authorization server; FastMCP is the resource server.
+
+### Prerequisites
+
+1. An **[Auth0 account](https://auth0.com/)** with **Auth for MCP** enabled
+2. **Resource Parameter Compatibility Profile** enabled (Settings → Advanced)
+3. Your FastMCP server URL (use `http://127.0.0.1:8000` in development — not `localhost`)
+
+See Auth0's [authorization quickstart](https://auth0.com/ai/docs/mcp/get-started/authorization-for-your-mcp-server) for tenant setup (API identifier, domain-level connections, CIMD approval).
+
+### Step 1: Create an Auth0 API
+
+Create an API (Resource Server) whose **identifier** is your MCP resource URL, for example `http://127.0.0.1:8000/mcp`. Use `RS256` signing and the `rfc9068_profile_authz` token dialect if you need `permissions` claims on tokens.
+
+When the server starts, it logs the exact `aud` value it validates — your API identifier must match.
+
+### Step 2: FastMCP configuration
+
+```python server_mcp.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.auth0 import Auth0MCPProvider
+
+auth_provider = Auth0MCPProvider(
+ config_url="https://YOUR_TENANT.auth0.com/.well-known/openid-configuration",
+ base_url="http://127.0.0.1:8000",
+)
+
+mcp = FastMCP(name="Auth0 MCP Server", auth=auth_provider)
+```
+
+No `client_id` or `client_secret` is required on the FastMCP side — MCP clients register with Auth0 directly.
+
+### Testing
+
+See `examples/auth/auth0_mcp/` for a runnable server and DCR client. Set `AUTH0_CONFIG_URL` to your tenant's OIDC discovery URL before starting the server.
+
+## OIDC Proxy (fixed credentials)
+
+This integration uses the [**OIDC Proxy**](/servers/auth/oidc-proxy) pattern when you use a fixed Auth0 application instead of tenant-level DCR.
### Prerequisites
@@ -137,7 +182,8 @@ async def main():
# Test the protected tool
result = await client.call_tool("get_token_info")
- print(f"Auth0 audience: {result['audience']}")
+ token_info = result.data
+ print(f"Auth0 audience: {token_info['audience']}")
if __name__ == "__main__":
asyncio.run(main())
diff --git a/docs/integrations/authkit.mdx b/docs/integrations/authkit.mdx
index e99f78351..05c6e5d7b 100644
--- a/docs/integrations/authkit.mdx
+++ b/docs/integrations/authkit.mdx
@@ -9,29 +9,32 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
-This guide shows you how to secure your FastMCP server using WorkOS's **AuthKit**, a complete authentication and user management solution. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern, where AuthKit handles user login and your FastMCP server validates the tokens.
-
-
-AuthKit does not currently support [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html) resource indicators, so FastMCP cannot validate that tokens were issued for the specific resource server. If you need resource-specific audience validation, consider using [WorkOSProvider](/integrations/workos) (OAuth proxy pattern) instead.
-
+This guide shows you how to secure your FastMCP server using WorkOS's **AuthKit**, a complete authentication and user management solution. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern with [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html) resource indicators: AuthKit issues tokens whose `aud` claim is bound to your server's resource URL, and FastMCP validates that claim automatically.
## Configuration
+
### Prerequisites
Before you begin, you will need:
1. A **[WorkOS Account](https://workos.com/)** and a new **Project**.
2. An **[AuthKit](https://www.authkit.com/)** instance configured within your WorkOS project.
-3. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`).
+3. Your FastMCP server's URL (can be localhost for development, e.g., `http://127.0.0.1:8000`).
-### Step 1: AuthKit Configuration
+### Step 1: WorkOS Dashboard
-In your WorkOS Dashboard, enable AuthKit and configure the following settings:
+In the WorkOS Dashboard, go to **Connect → Configuration** and configure:
-
- Go to **Applications → Configuration** and enable **Dynamic Client Registration**. This allows MCP clients register with your application automatically.
+
+ Enable **Dynamic Client Registration** (DCR) so MCP clients can register themselves. Alternatively, enable **Client ID Metadata Document** (CIMD) if your clients support it.
+
- 
+
+ Add your FastMCP server's resource URL (e.g., `http://127.0.0.1:8000/mcp`) as a valid resource indicator.
+
+ This must exactly match what FastMCP advertises in its protected resource metadata. Start your server first and it will log the correct URL on startup — copy that value.
+
+ Without this step, AuthKit falls back to a default environment-scoped audience and audience validation will fail with a 401.
@@ -47,16 +50,18 @@ Create your FastMCP server file and use the `AuthKitProvider` to handle all the
from fastmcp import FastMCP
from fastmcp.server.auth.providers.workos import AuthKitProvider
-# The AuthKitProvider automatically discovers WorkOS endpoints
-# and configures JWT token validation
+# AuthKitProvider automatically discovers WorkOS endpoints, configures JWT
+# validation, and binds the token audience to this server's resource URL.
auth_provider = AuthKitProvider(
authkit_domain="https://your-project-12345.authkit.app",
- base_url="http://localhost:8000" # Use your actual server URL
+ base_url="http://127.0.0.1:8000", # Use your actual server URL
)
mcp = FastMCP(name="AuthKit Secured App", auth=auth_provider)
```
+When the server starts, it logs the resource URL it is validating against. Paste that URL into your Dashboard's **MCP resource indicators** list.
+
## Testing
To test your server, you can use the `fastmcp` CLI to run it locally. Assuming you've saved the above code to `server.py` (after replacing the `authkit_domain` and `base_url` with your actual values!), you can run the following command:
@@ -75,8 +80,9 @@ import asyncio
auth = OAuth(additional_client_metadata={"token_endpoint_auth_method": "none"})
async def main():
- async with Client("http://localhost:8000/mcp", auth=auth) as client:
- assert await client.ping()
+ async with Client("http://127.0.0.1:8000/mcp", auth=auth) as client:
+ tools = await client.list_tools()
+ print(f"Authenticated. Server exposes {len(tools)} tools.")
if __name__ == "__main__":
asyncio.run(main())
@@ -94,7 +100,7 @@ from fastmcp.server.auth.providers.workos import AuthKitProvider
# Load configuration from environment variables
auth = AuthKitProvider(
authkit_domain=os.environ.get("AUTHKIT_DOMAIN"),
- base_url=os.environ.get("BASE_URL", "https://your-server.com")
+ base_url=os.environ.get("BASE_URL", "https://your-server.com"),
)
mcp = FastMCP(name="AuthKit Secured App", auth=auth)
diff --git a/docs/integrations/azure.mdx b/docs/integrations/azure.mdx
index 4376a38ce..c683fbce4 100644
--- a/docs/integrations/azure.mdx
+++ b/docs/integrations/azure.mdx
@@ -223,8 +223,9 @@ async def main():
# Test the protected tool
result = await client.call_tool("get_user_info")
- print(f"Azure user: {result['email']}")
- print(f"Name: {result['name']}")
+ user_info = result.data
+ print(f"Azure user: {user_info['email']}")
+ print(f"Name: {user_info['name']}")
if __name__ == "__main__":
asyncio.run(main())
@@ -409,7 +410,7 @@ The `EntraOBOToken` dependency handles the complete OBO flow automatically. Decl
```python
from fastmcp import FastMCP
from fastmcp.server.auth.providers.azure import AzureProvider, EntraOBOToken
-import httpx
+import httpx2
auth_provider = AzureProvider(
client_id="your-client-id",
@@ -431,7 +432,7 @@ async def get_recent_emails(
graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"]),
) -> list[dict]:
"""Get the user's recent emails from Microsoft Graph."""
- async with httpx.AsyncClient() as client:
+ async with httpx2.AsyncClient() as client:
response = await client.get(
f"https://graph.microsoft.com/v1.0/me/messages?$top={count}",
headers={"Authorization": f"Bearer {graph_token}"},
@@ -458,3 +459,85 @@ For advanced OBO scenarios, use `CurrentAccessToken()` to get the user's token,
For a complete working example of Azure OBO with FastMCP, see [Pamela Fox's blog post on OBO flow for Entra-based MCP servers](https://blog.pamelafox.org/2026/01/using-on-behalf-of-flow-for-entra-based.html).
+
+## Azure AD B2C
+
+
+
+Azure AD B2C (Business-to-Consumer) uses different endpoints, scope URIs, and
+token issuers than standard Microsoft Entra ID. The `AzureProvider.from_b2c()`
+factory handles all of these differences automatically.
+
+
+Azure AD B2C does **not** support the On-Behalf-Of (OBO) flow. If you need
+OBO for downstream API calls, use `AzureProvider` with standard Entra ID
+instead.
+
+
+### Quick Start
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.azure import AzureProvider
+
+auth = AzureProvider.from_b2c(
+ tenant_name="mytenant",
+ policy_name="B2C_1_susi",
+ client_id="00000000-0000-0000-0000-000000000000",
+ client_secret="my-secret",
+ required_scopes=["mcp-access"],
+ base_url="https://myserver.com",
+)
+
+mcp = FastMCP("My App", auth=auth)
+```
+
+`from_b2c()` derives the following values automatically:
+
+| Derived value | Formula |
+|---|---|
+| Authority host | `{tenant_name}.b2clogin.com` |
+| Authorization endpoint | `https://{tenant_name}.b2clogin.com/{tenant_name}.onmicrosoft.com/{policy_name}/oauth2/v2.0/authorize` |
+| Token endpoint | `https://{tenant_name}.b2clogin.com/{tenant_name}.onmicrosoft.com/{policy_name}/oauth2/v2.0/token` |
+| Scope identifier URI | `https://{tenant_name}.onmicrosoft.com/{client_id}` |
+
+### Token Issuer Validation
+
+B2C access tokens carry the **tenant GUID** (not the `.onmicrosoft.com` name)
+in the `iss` claim, and the exact format varies by policy and custom-domain
+configuration. `from_b2c()` therefore **disables issuer validation by
+default**; **audience validation still enforces that tokens target the correct
+application**.
+
+Once you have confirmed a successful end-to-end login, read the actual `iss`
+value from the decoded claims and enable strict validation:
+
+```python
+auth = AzureProvider.from_b2c(
+ tenant_name="mytenant",
+ policy_name="B2C_1_susi",
+ client_id="00000000-0000-0000-0000-000000000000",
+ client_secret="my-secret",
+ required_scopes=["mcp-access"],
+ base_url="https://myserver.com",
+ token_issuer="https://mytenant.b2clogin.com/11111111-2222-3333-4444-555555555555/v2.0/",
+)
+```
+
+### Custom Domains
+
+If your B2C tenant uses a [custom domain](https://learn.microsoft.com/en-us/azure/active-directory-b2c/custom-domain)
+(e.g. `auth.mycompany.com` instead of `mytenant.b2clogin.com`), pass it via
+`custom_domain`:
+
+```python
+auth = AzureProvider.from_b2c(
+ tenant_name="mytenant",
+ policy_name="B2C_1_susi",
+ client_id="00000000-0000-0000-0000-000000000000",
+ client_secret="my-secret",
+ required_scopes=["mcp-access"],
+ base_url="https://myserver.com",
+ custom_domain="auth.mycompany.com",
+)
+```
diff --git a/docs/integrations/chatgpt.mdx b/docs/integrations/chatgpt.mdx
index 92ddb7404..23249f92c 100644
--- a/docs/integrations/chatgpt.mdx
+++ b/docs/integrations/chatgpt.mdx
@@ -92,10 +92,12 @@ The connector must be explicitly enabled in each chat session through Developer
### Skip Confirmations
-Use `annotations={"readOnlyHint": True}` to skip confirmation prompts for read-only tools:
+Use `annotations=ToolAnnotations(readOnlyHint=True)` to skip confirmation prompts for read-only tools:
```python
-@mcp.tool(annotations={"readOnlyHint": True})
+from mcp.types import ToolAnnotations
+
+@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
def get_status() -> str:
"""Check system status."""
return "All systems operational"
@@ -153,4 +155,3 @@ def fetch(id: str) -> dict:
5. Ask research questions
ChatGPT will use your `search` and `fetch` tools to find and cite relevant information.
-
diff --git a/docs/integrations/claude-code.mdx b/docs/integrations/claude-code.mdx
index 8098ff51e..7addeaaad 100644
--- a/docs/integrations/claude-code.mdx
+++ b/docs/integrations/claude-code.mdx
@@ -118,7 +118,7 @@ fastmcp install claude-code server.py --project /path/to/my-project
If your server needs environment variables (like API keys), you must include them:
```bash
-fastmcp install claude-code server.py --server-name "Weather Server" \
+fastmcp install claude-code server.py --name "Weather Server" \
--env API_KEY=your-api-key \
--env DEBUG=true
```
@@ -126,7 +126,7 @@ fastmcp install claude-code server.py --server-name "Weather Server" \
Or load them from a `.env` file:
```bash
-fastmcp install claude-code server.py --server-name "Weather Server" --env-file .env
+fastmcp install claude-code server.py --name "Weather Server" --env-file .env
```
diff --git a/docs/integrations/claude-desktop.mdx b/docs/integrations/claude-desktop.mdx
index 4478bcc37..b0d1b5265 100644
--- a/docs/integrations/claude-desktop.mdx
+++ b/docs/integrations/claude-desktop.mdx
@@ -141,7 +141,7 @@ Claude Desktop runs servers in a completely isolated environment with no access
If your server needs environment variables (like API keys), you must include them:
```bash
-fastmcp install claude-desktop server.py --server-name "Weather Server" \
+fastmcp install claude-desktop server.py --name "Weather Server" \
--env API_KEY=your-api-key \
--env DEBUG=true
```
@@ -149,7 +149,7 @@ fastmcp install claude-desktop server.py --server-name "Weather Server" \
Or load them from a `.env` file:
```bash
-fastmcp install claude-desktop server.py --server-name "Weather Server" --env-file .env
+fastmcp install claude-desktop server.py --name "Weather Server" --env-file .env
```
- **`uv` must be installed and available in your system PATH**. Claude Desktop runs in its own isolated environment and needs `uv` to manage dependencies.
diff --git a/docs/integrations/cursor.mdx b/docs/integrations/cursor.mdx
index da0744ee0..787497b02 100644
--- a/docs/integrations/cursor.mdx
+++ b/docs/integrations/cursor.mdx
@@ -139,7 +139,7 @@ Cursor runs servers in a completely isolated environment with no access to your
If your server needs environment variables (like API keys), you must include them:
```bash
-fastmcp install cursor server.py --server-name "Weather Server" \
+fastmcp install cursor server.py --name "Weather Server" \
--env API_KEY=your-api-key \
--env DEBUG=true
```
@@ -147,7 +147,7 @@ fastmcp install cursor server.py --server-name "Weather Server" \
Or load them from a `.env` file:
```bash
-fastmcp install cursor server.py --server-name "Weather Server" --env-file .env
+fastmcp install cursor server.py --name "Weather Server" --env-file .env
```
@@ -164,10 +164,10 @@ You can generate MCP JSON configuration for manual use:
```bash
# Generate configuration and output to stdout
-fastmcp install mcp-json server.py --server-name "Dice Roller" --with pandas
+fastmcp install mcp-json server.py --name "Dice Roller" --with pandas
# Copy configuration to clipboard for easy pasting
-fastmcp install mcp-json server.py --server-name "Dice Roller" --copy
+fastmcp install mcp-json server.py --name "Dice Roller" --copy
```
This generates the standard `mcpServers` configuration format that can be used with any MCP-compatible client.
diff --git a/docs/integrations/descope.mdx b/docs/integrations/descope.mdx
index dba9b3509..fbe1c2b22 100644
--- a/docs/integrations/descope.mdx
+++ b/docs/integrations/descope.mdx
@@ -18,16 +18,15 @@ This guide shows you how to secure your FastMCP server using [**Descope**](https
Before you begin, you will need:
1. To [sign up](https://www.descope.com/sign-up) for a Free Forever Descope account
-2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:3000`)
+2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
### Step 1: Configure Descope
-
- 1. Go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console, and create a new MCP Server.
- 2. Give the MCP server a name and description.
- 3. Ensure that **Dynamic Client Registration (DCR)** is enabled. Then click **Create**.
- 4. Once you've created the MCP Server, note your Well-Known URL.
+
+ You can use either a resource-specific Descope MCP Server or a project-level inbound app.
+
+ To create an MCP Server, go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console, create a server, and enable **Dynamic Client Registration (DCR)**.
@@ -35,10 +34,17 @@ Before you begin, you will need:
-
- Save your Well-Known URL from [MCP Server Settings](https://app.descope.com/mcp-servers):
+
+ `DescopeProvider` accepts both resource-specific MCP Server URLs:
+
```
- Well-Known URL: https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration
+ https://api.descope.com/v1/apps/agentic/P.../M.../.well-known/openid-configuration
+ ```
+
+ and project-level inbound app URLs:
+
+ ```
+ https://api.descope.com/v1/apps/P.../.well-known/openid-configuration
```
@@ -48,30 +54,54 @@ Before you begin, you will need:
Create a `.env` file with your Descope configuration:
```bash
-DESCOPE_CONFIG_URL=https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration # Your Descope Well-Known URL
-SERVER_URL=http://localhost:3000 # Your server's base URL
+DESCOPE_CONFIG_URL=https://api.descope.com/v1/apps/P.../.well-known/openid-configuration
+BASE_URL=http://localhost:8000
```
### Step 3: FastMCP Configuration
-Create your FastMCP server file and use the DescopeProvider to handle all the OAuth integration automatically:
+Create your FastMCP server file and use the DescopeProvider to handle all the OAuth integration automatically. Nothing reads `.env` automatically, so load it explicitly with [python-dotenv](https://pypi.org/project/python-dotenv/) (`pip install python-dotenv`) before constructing the provider — otherwise the values you just wrote stay invisible to `os.environ`.
```python server.py
+import os
+
+from dotenv import load_dotenv
from fastmcp import FastMCP
from fastmcp.server.auth.providers.descope import DescopeProvider
-# The DescopeProvider automatically discovers Descope endpoints
-# and configures JWT token validation
+load_dotenv()
+
+# DescopeProvider accepts either supported Well-Known URL format.
auth_provider = DescopeProvider(
- config_url=https://.../.well-known/openid-configuration, # Your MCP Server .well-known URL
- base_url=SERVER_URL, # Your server's public URL
+ config_url=os.environ["DESCOPE_CONFIG_URL"],
+ base_url=os.environ.get("BASE_URL", "http://localhost:8000"),
)
# Create FastMCP server with auth
mcp = FastMCP(name="My Descope Protected Server", auth=auth_provider)
-
```
+### Scope discovery and validation
+
+
+
+When both `scopes_supported` and `required_scopes` are omitted, `DescopeProvider` discovers `scopes_supported` lazily from the OpenID configuration and advertises them to MCP clients. Provider construction remains network-free, and a transient discovery failure is retried on a later metadata request.
+
+Set both options when clients should request a broader set of scopes than the server requires on every token:
+
+```python
+from fastmcp.server.auth.providers.descope import DescopeProvider
+
+auth_provider = DescopeProvider(
+ config_url="https://api.descope.com/v1/apps/P.../.well-known/openid-configuration",
+ base_url="https://your-fastmcp-server.com",
+ scopes_supported=["mcp:read", "mcp:write"],
+ required_scopes=["mcp:read"],
+)
+```
+
+`scopes_supported` controls what the protected resource metadata advertises. `required_scopes` controls what the JWT verifier requires during token validation. When only `required_scopes` is set, those scopes are also advertised to clients.
+
## Testing
To test your server, you can use the `fastmcp` CLI to run it locally. Assuming you've saved the above code to `server.py` (after replacing the environment variables with your actual values!), you can run the following command:
@@ -88,7 +118,8 @@ import asyncio
async def main():
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
- assert await client.ping()
+ tools = await client.list_tools()
+ print(f"Authenticated. Server exposes {len(tools)} tools.")
if __name__ == "__main__":
asyncio.run(main())
diff --git a/docs/integrations/discord.mdx b/docs/integrations/discord.mdx
index 5d6c643b7..43586d578 100644
--- a/docs/integrations/discord.mdx
+++ b/docs/integrations/discord.mdx
@@ -108,7 +108,8 @@ async def main():
print("✓ Authenticated with Discord!")
result = await client.call_tool("get_user_info")
- print(f"Discord user: {result['username']}")
+ user_info = result.data
+ print(f"Discord user: {user_info['username']}")
if __name__ == "__main__":
asyncio.run(main())
diff --git a/docs/integrations/fastapi.mdx b/docs/integrations/fastapi.mdx
index abdc1d0e7..83aa924f8 100644
--- a/docs/integrations/fastapi.mdx
+++ b/docs/integrations/fastapi.mdx
@@ -220,7 +220,7 @@ Because FastMCP's FastAPI integration is based on its [OpenAPI integration](/int
```python
# Assumes the FastAPI app from above is already defined
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
# Custom mapping rules
mcp = FastMCP.from_fastapi(
diff --git a/docs/integrations/gemini-cli.mdx b/docs/integrations/gemini-cli.mdx
index 10613fb1b..86d35b20b 100644
--- a/docs/integrations/gemini-cli.mdx
+++ b/docs/integrations/gemini-cli.mdx
@@ -118,7 +118,7 @@ fastmcp install gemini-cli server.py --project /path/to/my-project
If your server needs environment variables (like API keys), you must include them:
```bash
-fastmcp install gemini-cli server.py --server-name "Weather Server" \
+fastmcp install gemini-cli server.py --name "Weather Server" \
--env API_KEY=your-api-key \
--env DEBUG=true
```
@@ -126,7 +126,7 @@ fastmcp install gemini-cli server.py --server-name "Weather Server" \
Or load them from a `.env` file:
```bash
-fastmcp install gemini-cli server.py --server-name "Weather Server" --env-file .env
+fastmcp install gemini-cli server.py --name "Weather Server" --env-file .env
```
diff --git a/docs/integrations/gemini.mdx b/docs/integrations/gemini.mdx
index 159e662a5..1b17ab6ee 100644
--- a/docs/integrations/gemini.mdx
+++ b/docs/integrations/gemini.mdx
@@ -89,7 +89,7 @@ Okay, I rolled 3 dice and got a 5, 4, and 1.
### Remote & Authenticated Servers
-In the above example, we connected to our local server using `stdio` transport. Because we're using a FastMCP client, you can also connect to any local or remote MCP server, using any [transport](/clients/transports) or [auth](/clients/auth) method supported by FastMCP, simply by changing the client configuration.
+In the above example, we connected to our local server using `stdio` transport. Because we're using a FastMCP client, you can also connect to any local or remote MCP server, using any [transport](/clients/transports) or [auth](/clients/auth/oauth) method supported by FastMCP, simply by changing the client configuration.
For example, to connect to a remote, authenticated server, you can use the following client:
diff --git a/docs/integrations/github.mdx b/docs/integrations/github.mdx
index 72e65d3ce..d1a2a3608 100644
--- a/docs/integrations/github.mdx
+++ b/docs/integrations/github.mdx
@@ -69,7 +69,7 @@ from fastmcp.server.auth.providers.github import GitHubProvider
# The GitHubProvider handles GitHub's token format and validation
auth_provider = GitHubProvider(
client_id="Ov23liAbcDefGhiJkLmN", # Your GitHub OAuth App Client ID
- client_secret="github_pat_...", # Your GitHub OAuth App Client Secret
+ client_secret="your-github-client-secret", # Your GitHub OAuth App Client Secret
base_url="http://localhost:8000", # Must match your OAuth App configuration
# redirect_path="/auth/callback" # Default value, customize if needed
)
@@ -119,7 +119,7 @@ async def main():
# Test the protected tool
result = await client.call_tool("get_user_info")
- print(f"GitHub user: {result['github_user']}")
+ print(f"GitHub user: {result.data['github_user']}")
if __name__ == "__main__":
asyncio.run(main())
@@ -151,7 +151,7 @@ from cryptography.fernet import Fernet
# Production setup with encrypted persistent token storage
auth_provider = GitHubProvider(
client_id="Ov23liAbcDefGhiJkLmN",
- client_secret="github_pat_...",
+ client_secret="your-github-client-secret",
base_url="https://your-production-domain.com",
# Production token management
diff --git a/docs/integrations/google.mdx b/docs/integrations/google.mdx
index 17d49d12f..141444080 100644
--- a/docs/integrations/google.mdx
+++ b/docs/integrations/google.mdx
@@ -130,8 +130,9 @@ async def main():
# Test the protected tool
result = await client.call_tool("get_user_info")
- print(f"Google user: {result['email']}")
- print(f"Name: {result['name']}")
+ user_info = result.data
+ print(f"Google user: {user_info['email']}")
+ print(f"Name: {user_info['name']}")
if __name__ == "__main__":
asyncio.run(main())
diff --git a/docs/integrations/huggingface.mdx b/docs/integrations/huggingface.mdx
new file mode 100644
index 000000000..55794024b
--- /dev/null
+++ b/docs/integrations/huggingface.mdx
@@ -0,0 +1,304 @@
+---
+title: Hugging Face OAuth 🤝 FastMCP
+sidebarTitle: Hugging Face
+description: Secure your FastMCP server with Hugging Face OAuth
+icon: hugging-face
+iconType: brands
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+This guide shows you how to secure your FastMCP server using **Hugging Face OAuth**.
+The `HuggingFaceProvider` uses FastMCP's [OAuth Proxy](/servers/auth/oauth-proxy)
+pattern with Hugging Face's OAuth and OpenID Connect endpoints. It works with
+manually created confidential apps, public PKCE apps, and Client ID Metadata
+Documents (CIMD).
+
+When deploying your MCP server to Hugging Face Spaces, Spaces can create and
+manage the OAuth app for you.
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+
+1. A **[Hugging Face account](https://huggingface.co/join)** with access to create OAuth apps
+2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
+
+### Step 1: Create a Hugging Face OAuth app
+
+Create an OAuth app from your [Hugging Face application settings](https://huggingface.co/settings/applications/new).
+For details, see Hugging Face's [OAuth documentation](https://huggingface.co/docs/hub/oauth).
+
+
+
+ Go to your [Hugging Face application settings](https://huggingface.co/settings/applications/new)
+ and create a new OAuth application.
+
+ Choose a name users will recognize, then configure the redirect URL for
+ your FastMCP server:
+
+ - Development: `http://localhost:8000/auth/callback`
+ - Production: `https://your-domain.com/auth/callback`
+
+
+ The redirect URL must match exactly. The default path is `/auth/callback`,
+ but you can customize it using the `redirect_path` parameter. For
+ production, use HTTPS.
+
+
+
+
+ After creating the app, save:
+
+ - **Client ID**: The public identifier for your Hugging Face OAuth app
+ - **Client Secret**: The app secret, if you created a confidential app
+
+
+ Store the client secret securely. Never commit it to version control. Use
+ environment variables or a secrets manager in production.
+
+
+
+
+### Step 2: Configure FastMCP
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.huggingface import HuggingFaceProvider
+
+# The HuggingFaceProvider handles Hugging Face's opaque OAuth access tokens
+# and stores user data in token claims.
+auth_provider = HuggingFaceProvider(
+ client_id="your-huggingface-client-id", # Your Hugging Face OAuth app client ID
+ client_secret="your-huggingface-client-secret", # Your Hugging Face OAuth app client secret
+ base_url="http://localhost:8000", # Must match your OAuth configuration
+ required_scopes=["openid", "profile"], # Default value
+ # redirect_path="/auth/callback" # Default value, customize if needed
+)
+
+mcp = FastMCP(name="Hugging Face Secured App", auth=auth_provider)
+
+
+# Add a protected tool to test authentication
+@mcp.tool
+async def get_user_info() -> dict:
+ """Returns information about the authenticated Hugging Face user."""
+ from fastmcp.server.dependencies import get_access_token
+
+ token = get_access_token()
+ return {
+ "subject": token.claims.get("sub"),
+ "username": token.claims.get("preferred_username"),
+ "profile": token.claims.get("profile"),
+ }
+```
+
+## Public OAuth apps, DCR, and CIMD
+
+Hugging Face supports public OAuth apps (no client secret). For public apps,
+omit `client_secret` and provide a `jwt_signing_key` so FastMCP can sign its
+own proxy tokens:
+
+```python
+auth_provider = HuggingFaceProvider(
+ client_id="your-public-huggingface-client-id",
+ base_url="http://localhost:8000",
+ jwt_signing_key="replace-with-a-secure-secret",
+)
+```
+
+MCP clients can use Dynamic Client Registration with your FastMCP server. The
+`HuggingFaceProvider` inherits FastMCP's OAuth Proxy behavior, which handles
+client registration locally and forwards authorization to Hugging Face using
+your configured Hugging Face OAuth app. In other words, MCP clients register
+with FastMCP, while FastMCP uses your Hugging Face `client_id` and optional
+`client_secret` for the upstream OAuth flow.
+
+You can also use a Client ID Metadata Document URL as the `client_id` when your
+client metadata is hosted at a stable HTTPS URL:
+
+```python
+auth_provider = HuggingFaceProvider(
+ client_id="https://your-client.example/.well-known/oauth-cimd",
+ base_url="http://localhost:8000",
+ jwt_signing_key="replace-with-a-secure-secret",
+)
+```
+
+## Testing
+
+### Running the Server
+
+Start your server with HTTP transport:
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+Your server is now running and protected by Hugging Face OAuth authentication.
+
+### Testing with a Client
+
+Create a test client that authenticates with your Hugging Face-protected server:
+
+```python test_client.py
+import asyncio
+from fastmcp import Client
+
+
+async def main():
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ result = await client.call_tool("get_user_info")
+ print(result)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+When you run the client for the first time:
+
+1. Your browser will open to Hugging Face's authorization page
+2. Sign in with your Hugging Face account and grant the requested permissions
+3. After authorization, you'll be redirected back
+4. The client receives the token and can make authenticated requests
+
+
+The client caches tokens locally, so you won't need to re-authenticate for
+subsequent runs unless the token expires or you explicitly clear the cache.
+
+
+## Hugging Face Spaces
+
+When deploying to [Hugging Face Spaces](https://huggingface.co/docs/hub/spaces-oauth),
+Spaces can create and manage the OAuth app for you. Add OAuth metadata to your
+Space README:
+
+```yaml
+---
+title: FastMCP Hugging Face OAuth
+sdk: docker
+hf_oauth: true
+hf_oauth_expiration_minutes: 480
+hf_oauth_scopes:
+ - email
+ - inference-api
+---
+```
+
+Spaces provide `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET`, `OAUTH_SCOPES`,
+`OPENID_PROVIDER_URL`, and `SPACE_HOST` environment variables:
+
+```python
+import os
+
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.huggingface import HuggingFaceProvider
+from fastmcp.utilities.auth import parse_scopes
+
+base_url = f"https://{os.environ['SPACE_HOST']}"
+
+auth_provider = HuggingFaceProvider(
+ client_id=os.environ["OAUTH_CLIENT_ID"],
+ client_secret=os.environ["OAUTH_CLIENT_SECRET"],
+ base_url=base_url,
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ required_scopes=parse_scopes(os.environ.get("OAUTH_SCOPES")) or ["openid", "profile"],
+)
+
+mcp = FastMCP(name="Hugging Face Space App", auth=auth_provider)
+```
+
+Set `JWT_SIGNING_KEY` as a Space secret.
+
+## Hugging Face scopes
+
+The default scopes are `openid` and `profile`. Add more scopes when your tools
+need Hub capabilities:
+
+| Scope | Description |
+|-------|-------------|
+| `email` | Access the user's email address |
+| `read-billing` | Know whether the user has a payment method set up |
+| `read-repos` | Read the user's personal repositories |
+| `gated-repos` | Read public gated repositories the user can access |
+| `contribute-repos` | Create repositories and access app-created repositories |
+| `write-repos` | Read and write the user's personal repositories |
+| `manage-repos` | Full repository access, including creation and deletion |
+| `read-collections` | Read the user's personal collections |
+| `write-collections` | Read and write the user's personal collections, including collection creation and deletion |
+| `inference-api` | Use Hugging Face Inference Providers as the user |
+| `jobs` | Run Hugging Face Jobs |
+| `webhooks` | Manage webhooks |
+| `write-discussions` | Open discussions and pull requests, and interact with discussions |
+
+```python
+auth_provider = HuggingFaceProvider(
+ client_id="your-huggingface-client-id",
+ client_secret="your-huggingface-client-secret",
+ base_url="https://your-domain.com",
+ required_scopes=["openid", "profile", "inference-api", "jobs"],
+)
+```
+
+For organization resources, use Hugging Face's normal OAuth organization grant
+flow. If you need a specific organization, pass Hugging Face's `orgIds`
+authorization parameter. The value is the organization ID from the
+`organizations.sub` field in the Hugging Face userinfo response:
+
+```python
+auth_provider = HuggingFaceProvider(
+ client_id="your-huggingface-client-id",
+ client_secret="your-huggingface-client-secret",
+ base_url="https://your-domain.com",
+ extra_authorize_params={"orgIds": "your-org-id"},
+)
+```
+
+## Production Configuration
+
+For production deployments with persistent token management across server
+restarts, configure `jwt_signing_key` and `client_storage`:
+
+```python server.py
+import os
+from cryptography.fernet import Fernet
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.huggingface import HuggingFaceProvider
+from key_value.aio.stores.redis import RedisStore
+from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
+
+# Production setup with encrypted persistent token storage
+auth_provider = HuggingFaceProvider(
+ client_id="your-huggingface-client-id",
+ client_secret=os.environ["HUGGINGFACE_CLIENT_SECRET"],
+ base_url="https://your-production-domain.com",
+ required_scopes=["openid", "profile", "email"],
+
+ # Production token management
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ client_storage=FernetEncryptionWrapper(
+ key_value=RedisStore(
+ host=os.environ["REDIS_HOST"],
+ port=int(os.environ["REDIS_PORT"])
+ ),
+ fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
+ )
+)
+
+mcp = FastMCP(name="Production Hugging Face App", auth=auth_provider)
+```
+
+
+Parameters (`jwt_signing_key` and `client_storage`) work together to ensure
+tokens and client registrations survive server restarts. **Wrap your storage in
+`FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without
+it, tokens are stored in plaintext. Store secrets in environment variables and
+use a persistent storage backend like Redis for distributed deployments.
+
+For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
+
diff --git a/docs/integrations/images/permit/role_assignement.png b/docs/integrations/images/permit/role_assignment.png
similarity index 100%
rename from docs/integrations/images/permit/role_assignement.png
rename to docs/integrations/images/permit/role_assignment.png
diff --git a/docs/integrations/keycloak.mdx b/docs/integrations/keycloak.mdx
new file mode 100644
index 000000000..22d61f132
--- /dev/null
+++ b/docs/integrations/keycloak.mdx
@@ -0,0 +1,141 @@
+---
+title: Keycloak OAuth 🤝 FastMCP
+sidebarTitle: Keycloak
+description: Secure your FastMCP server with Keycloak OAuth
+icon: shield-check
+tag: NEW
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+This guide shows you how to secure your FastMCP server using **Keycloak OAuth**. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern with Dynamic Client Registration (DCR), where Keycloak handles user login and your FastMCP server validates the tokens.
+
+
+**Keycloak 26.6.0 or later is required.** Earlier versions had a DCR incompatibility with MCP clients ([PR #45309](https://github.com/keycloak/keycloak/pull/45309)) that is fixed in 26.6.0.
+
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+1. A running **[Keycloak](https://keycloak.org/)** instance (e.g., `http://localhost:8080`)
+2. A Keycloak realm with **Dynamic Client Registration** enabled and a trusted host policy that allows your server URL (e.g., `http://localhost:8000/*`)
+3. Your FastMCP server's public URL (e.g., `http://localhost:8000`)
+
+### FastMCP Configuration
+
+Create your FastMCP server and use `KeycloakAuthProvider` to handle OAuth:
+
+```python server.py
+import os
+
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
+from fastmcp.server.dependencies import get_access_token
+
+auth = KeycloakAuthProvider(
+ realm_url=os.getenv("KEYCLOAK_REALM_URL") or "http://localhost:8080/realms/myrealm",
+ base_url="http://localhost:8000",
+ # audience="http://localhost:8000", # Recommended for production
+)
+
+mcp = FastMCP("Keycloak Example Server", auth=auth)
+
+
+@mcp.tool
+async def get_access_token_claims() -> dict:
+ """Get the authenticated user's access token claims."""
+ token = get_access_token()
+ return {
+ "sub": token.claims.get("sub"),
+ "scope": token.claims.get("scope"),
+ "azp": token.claims.get("azp"),
+ }
+```
+
+
+**Production security**: Always configure the `audience` parameter in production. Without it, your server accepts tokens issued for any audience. Configure Keycloak audience mappers and set `audience` to your server's base URL to ensure tokens are specifically intended for your server.
+
+
+## Local Development
+
+Local infrastructure tooling is deliberately kept out of the FastMCP core library to keep auth integrations slim and the associated maintenance burden as low as possible. That said, Keycloak is a popular identity provider for local development and testing, so a dedicated FastMCP-compatible setup blueprint lives in the companion project [**fastmcp-keycloak-local**](https://github.com/stephaneberle9/fastmcp-keycloak-local).
+
+It provides everything needed to develop and test FastMCP servers with Keycloak OAuth locally: a Docker-based Keycloak setup with a pre-configured `fastmcp` realm (Dynamic Client Registration enabled, test user included), cross-platform start scripts, and integration guides for the MCP Inspector, Claude Desktop, and Claude Code CLI.
+
+## Testing
+
+### Running the Server
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+### Testing with a Client
+
+```python client.py
+import asyncio
+from fastmcp import Client
+
+async def main():
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ print("✓ Authenticated with Keycloak!")
+ result = await client.call_tool("get_access_token_claims")
+ print(f"sub: {result.data.get('sub', 'N/A')}")
+
+asyncio.run(main())
+```
+
+On first run, your browser will open to Keycloak's authorization page. After login, the client receives a token and caches it for subsequent runs.
+
+## Features
+
+### JWT Token Validation
+
+- **Signature Verification**: Validates tokens against Keycloak's JWKS endpoint
+- **Expiration Checking**: Automatically rejects expired tokens
+- **Issuer Validation**: Ensures tokens come from your specific Keycloak realm
+- **Scope Enforcement**: Verifies required OAuth scopes are present
+- **Audience Validation**: Optional validation that tokens target your server (configure `audience`)
+
+### User Claims
+
+Access user information from Keycloak JWT tokens:
+
+```python
+from fastmcp.server.dependencies import get_access_token
+
+@mcp.tool
+async def admin_only_tool() -> str:
+ """A tool only available to admin users."""
+ token = get_access_token()
+ roles = token.claims.get("realm_access", {}).get("roles", [])
+ if "admin" not in roles:
+ raise ValueError("This tool requires admin access")
+ return "Admin access granted!"
+```
+
+## Advanced Configuration
+
+### Custom Token Verifier
+
+```python
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
+
+custom_verifier = JWTVerifier(
+ jwks_uri="http://localhost:8080/realms/myrealm/protocol/openid-connect/certs",
+ issuer="http://localhost:8080/realms/myrealm",
+ audience="my-resource-server",
+ required_scopes=["api:read", "api:write"],
+)
+
+auth = KeycloakAuthProvider(
+ realm_url="http://localhost:8080/realms/myrealm",
+ base_url="http://localhost:8000",
+ token_verifier=custom_verifier,
+)
+```
diff --git a/docs/integrations/mcp-json-configuration.mdx b/docs/integrations/mcp-json-configuration.mdx
index b44b15e76..b516c9954 100644
--- a/docs/integrations/mcp-json-configuration.mdx
+++ b/docs/integrations/mcp-json-configuration.mdx
@@ -70,7 +70,7 @@ An object containing environment variables to set when launching the server. All
This format is widely adopted across the MCP ecosystem:
-- **Claude Desktop**: Uses `~/.claude/claude_desktop_config.json`
+- **Claude Desktop**: Uses `~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows
- **Cursor**: Uses `~/.cursor/mcp.json`
- **VS Code**: Uses workspace `.vscode/mcp.json`
- **Other clients**: Many MCP-compatible applications follow this standard
@@ -357,6 +357,98 @@ echo "$CONFIG" | jq '."CI Server".command'
# Output: "uv"
```
+### UV-Managed Project Dependencies
+
+For servers that live inside a uv-managed project (with `pyproject.toml`), use the `--project` flag to run within that project's environment:
+
+```bash
+fastmcp install mcp-json server.py --project .
+```
+
+Output:
+```json
+{
+ "My Server": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--project",
+ "/absolute/path/to/project",
+ "--with",
+ "fastmcp",
+ "fastmcp",
+ "run",
+ "/absolute/path/to/project/server.py"
+ ]
+ }
+}
+```
+
+You can also use `fastmcp.json` with a local project:
+
+```json fastmcp.json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py"
+ },
+ "environment": {
+ "project": "."
+ }
+}
+```
+
+If your server needs additional packages beyond those in `pyproject.toml`, add them via the `dependencies` array or `--with`.
+
+### Published Packages with `uvx`
+
+If your team publishes MCP servers as pip packages, you can configure clients to run them with `uvx` directly instead of `uv run`. For example, if your package is called `my-mcp-server` and provides a CLI entry point of the same name:
+
+```json
+{
+ "mcpServers": {
+ "My Server": {
+ "command": "uvx",
+ "args": ["my-mcp-server"]
+ }
+ }
+}
+```
+
+If the package name differs from the CLI command (e.g., package `weather-mcp` with command `weather-server`):
+
+```json
+{
+ "mcpServers": {
+ "Weather": {
+ "command": "uvx",
+ "args": ["--from", "weather-mcp", "weather-server"]
+ }
+ }
+}
+```
+
+You can also pin Python versions or add extra dependencies:
+
+```json
+{
+ "mcpServers": {
+ "My Server": {
+ "command": "uvx",
+ "args": [
+ "--python", "3.12",
+ "--with", "requests",
+ "my-mcp-server"
+ ]
+ }
+ }
+}
+```
+
+
+`fastmcp install mcp-json` generates `uv run` configurations for local development. For published packages, you'll typically write the `uvx` configuration manually or generate it through your own packaging workflow.
+
+
## Integration with MCP Clients
The generated configuration works with any MCP-compatible application:
@@ -365,7 +457,7 @@ The generated configuration works with any MCP-compatible application:
**Prefer [`fastmcp install claude-desktop`](/integrations/claude-desktop)** for automatic installation. Use MCP JSON for advanced configuration needs.
-Copy the `mcpServers` object into `~/.claude/claude_desktop_config.json`
+Copy the `mcpServers` object into Claude Desktop's config file (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows)
### Cursor
diff --git a/docs/integrations/oci.mdx b/docs/integrations/oci.mdx
index 02fa36dae..72165238d 100644
--- a/docs/integrations/oci.mdx
+++ b/docs/integrations/oci.mdx
@@ -75,7 +75,7 @@ Follow the Steps as mentioned below to create an OAuth client.
Click on "Edit OAuth configuration" button.
Configure the application as OAuth client by selecting "Configure this application as a client now" radio button.
Select "Authorization code" grant type. If you are planning to use the same OAuth client application for token exchange, select "Client credentials" grant type as well. In the sample, we will use the same client.
- For Authorization grant type, select redirect URL. In most cases, this will be the MCP server URL followed by "/oauth/callback".
+ For Authorization grant type, select redirect URL. In most cases, this will be the MCP server URL followed by "/auth/callback".
diff --git a/docs/integrations/openai.mdx b/docs/integrations/openai.mdx
index 6f88193f5..94ca82b40 100644
--- a/docs/integrations/openai.mdx
+++ b/docs/integrations/openai.mdx
@@ -178,8 +178,8 @@ if __name__ == "__main__":
If you try to call the authenticated server with the same OpenAI code we wrote earlier, you'll get an error like this:
-```python
-pythonAPIStatusError: Error code: 424 - {
+```text
+APIStatusError: Error code: 424 - {
"error": {
"message": "Error retrieving tool list from MCP server: 'dice_server'. Http status code: 401 (Unauthorized)",
"type": "external_connector_error",
diff --git a/docs/integrations/openapi.mdx b/docs/integrations/openapi.mdx
index 88dc19b14..4769f6576 100644
--- a/docs/integrations/openapi.mdx
+++ b/docs/integrations/openapi.mdx
@@ -26,14 +26,14 @@ We recommend using the FastAPI integration for bootstrapping and prototyping, no
To convert an OpenAPI specification to an MCP server, use the `FastMCP.from_openapi()` class method:
```python server.py
-import httpx
+import httpx2
from fastmcp import FastMCP
# Create an HTTP client for your API
-client = httpx.AsyncClient(base_url="https://api.example.com")
+client = httpx2.AsyncClient(base_url="https://api.example.com")
# Load your OpenAPI spec
-openapi_spec = httpx.get("https://api.example.com/openapi.json").json()
+openapi_spec = httpx2.get("https://api.example.com/openapi.json").json()
# Create the MCP server
mcp = FastMCP.from_openapi(
@@ -51,20 +51,19 @@ if __name__ == "__main__":
If your API requires authentication, configure it on the HTTP client:
```python
-import httpx
+import httpx2
from fastmcp import FastMCP
# Bearer token authentication
-api_client = httpx.AsyncClient(
+api_client = httpx2.AsyncClient(
base_url="https://api.example.com",
headers={"Authorization": "Bearer YOUR_TOKEN"}
)
# Create MCP server with authenticated client
mcp = FastMCP.from_openapi(
- openapi_spec=spec,
+ openapi_spec=spec,
client=api_client,
- timeout=30.0 # 30 second timeout for all requests
)
```
@@ -85,7 +84,7 @@ Each `RouteMap` specifies a combination of methods, patterns, and tags, as well
Here is FastMCP's default rule:
```python
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
DEFAULT_ROUTE_MAPPINGS = [
# All routes become tools
@@ -101,7 +100,7 @@ For example, prior to FastMCP 2.8.0, GET requests were automatically mapped to `
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
# Restore pre-2.8.0 semantic mapping
semantic_maps = [
@@ -124,7 +123,7 @@ Here is a more complete example that uses custom route maps to convert all `GET`
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -164,7 +163,7 @@ You can use this to remove sensitive or internal routes by targeting them specif
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -180,7 +179,7 @@ Or you can use a catch-all rule to exclude everything that your maps don't handl
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -212,7 +211,8 @@ The `route_map_fn` is called on all routes, even those that matched `MCPType.EXC
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType, HTTPRoute
+from fastmcp.server.providers.openapi import RouteMap, MCPType
+from fastmcp.utilities.openapi import HTTPRoute
def custom_route_mapper(route: HTTPRoute, mcp_type: MCPType) -> MCPType | None:
"""Advanced route type mapping."""
@@ -277,7 +277,7 @@ FastMCP provides several ways to add tags to your MCP components, allowing you t
You can add custom tags to components created from specific routes using the `mcp_tags` parameter in `RouteMap`. These tags will be applied to all components created from routes that match that particular route map.
```python
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -368,12 +368,12 @@ Your `mcp_component_fn` is expected to modify the component in-place, not to ret
```python
-from fastmcp.server.openapi import (
- HTTPRoute,
+from fastmcp.server.providers.openapi import (
OpenAPITool,
OpenAPIResource,
OpenAPIResourceTemplate,
)
+from fastmcp.utilities.openapi import HTTPRoute
def customize_components(
route: HTTPRoute,
@@ -403,7 +403,7 @@ FastMCP intelligently handles different types of parameters in OpenAPI requests:
### Query Parameters
-By default, FastMCP only includes query parameters that have non-empty values. Parameters with `None` values or empty strings are automatically filtered out.
+By default, FastMCP skips parameters whose value is `None`. Empty strings are still sent as empty query values, which is useful for APIs that distinguish between an omitted parameter and an explicitly blank one.
```python
# When calling this tool...
@@ -411,10 +411,10 @@ await client.call_tool("search_products", {
"category": "electronics", # ✅ Included
"min_price": 100, # ✅ Included
"max_price": None, # ❌ Excluded
- "brand": "", # ❌ Excluded
+ "brand": "", # ✅ Included as an empty value
})
-# The HTTP request will be: GET /products?category=electronics&min_price=100
+# The HTTP request will be: GET /products?category=electronics&min_price=100&brand=
```
### Path Parameters
@@ -452,4 +452,21 @@ FastMCP handles array parameters according to OpenAPI specifications:
### Headers
-Header parameters are automatically converted to strings and included in the HTTP request.
\ No newline at end of file
+Header parameters are automatically converted to strings and included in the HTTP request.
+
+### Composed Request Bodies
+
+A request body becomes a flat set of tool arguments, which is the shape LLM tool-calling APIs fill in most reliably. Schemas composed with `allOf` are resolved first, following `$ref` members, so fields inherited from a parent schema appear alongside the ones a schema declares itself.
+
+Schemas that use a `discriminator` are flattened the same way. FastMCP merges in the fields of every subtype named in the discriminator's `mapping`, marks them optional, and names the accepted values on the discriminator's own description. Given a `Pet` body discriminated by `petType` and mapped onto `Cat` and `Dog`, the tool takes the discriminator plus whichever fields that variant uses:
+
+```python
+await client.call_tool("create_pet", {
+ "petType": "cat",
+ "meowVolume": 11,
+})
+```
+
+The discriminator stays required; every variant field is optional, because only one variant applies to any given call.
+
+This trades local strictness for a schema models complete accurately. The generated schema permits any combination of variant fields, so sending `packSize` with `petType: "cat"` passes FastMCP's validation and is rejected by the API itself, exactly as it would be for any other HTTP client. Where two variants declare the same field differently, the declarations are combined with `anyOf` so that neither variant's constraints are advertised as applying to both.
\ No newline at end of file
diff --git a/docs/integrations/permit.mdx b/docs/integrations/permit.mdx
index 066f5b1ea..66b8c896e 100644
--- a/docs/integrations/permit.mdx
+++ b/docs/integrations/permit.mdx
@@ -31,7 +31,7 @@ The middleware automatically maps MCP methods to Permit.io resources and actions
> **Note:**
> Don't forget to assign the relevant role (e.g., Admin, User) to the user authenticating to your MCP server (such as the user in the JWT) in the Permit.io Directory. Without the correct role assignment, users will not have access to the resources and actions you've configured in your policies.
>
-> 
+> 
>
> *Example: In Permit.io Directory, both 'client' and 'admin' users are assigned the 'Admin' role, granting them the permissions defined in your policy mapping.*
@@ -300,10 +300,12 @@ For advanced configuration options and custom middleware extensions, see [Advanc
See the [example server](https://github.com/permitio/permit-fastmcp/blob/main/permit_fastmcp/example_server/example.py) for a full implementation with JWT-based authentication. For additional examples and usage patterns, see [Example Server](https://github.com/permitio/permit-fastmcp/blob/main/permit_fastmcp/example_server/):
```python
+import os
+import datetime
+
+import jwt
from fastmcp import FastMCP, Context
from permit_fastmcp.middleware.middleware import PermitMcpMiddleware
-import jwt
-import datetime
# Configure JWT identity extraction
os.environ["PERMIT_MCP_IDENTITY_MODE"] = "jwt"
diff --git a/docs/integrations/propelauth.mdx b/docs/integrations/propelauth.mdx
index 7f21d2010..7875a1ce2 100644
--- a/docs/integrations/propelauth.mdx
+++ b/docs/integrations/propelauth.mdx
@@ -101,7 +101,8 @@ import asyncio
async def main():
async with Client("http://localhost:8000/mcp", auth="oauth") as client:
- assert await client.ping()
+ tools = await client.list_tools()
+ print(f"Authenticated. Server exposes {len(tools)} tools.")
if __name__ == "__main__":
asyncio.run(main())
diff --git a/docs/integrations/pydantic-ai.mdx b/docs/integrations/pydantic-ai.mdx
new file mode 100644
index 000000000..0c8ffa524
--- /dev/null
+++ b/docs/integrations/pydantic-ai.mdx
@@ -0,0 +1,137 @@
+---
+title: Pydantic AI 🤝 FastMCP
+sidebarTitle: Pydantic AI
+description: Connect FastMCP servers to Pydantic AI agents using the FastMCPToolset
+icon: message-code
+---
+
+[Pydantic AI](https://ai.pydantic.dev/) ships a [`FastMCPToolset`](https://ai.pydantic.dev/mcp/fastmcp-client/) that lets a Pydantic AI agent call tools exposed by any MCP server through the [FastMCP Client](/clients/client). Because the toolset is built on the FastMCP Client, it works with FastMCP servers as well as any other MCP server, and supports the full range of [transports](/clients/transports): in-memory, STDIO, Streamable HTTP, and SSE.
+
+This page shows how to point `FastMCPToolset` at a FastMCP server, with examples for each transport. For the toolset's full API, see the [Pydantic AI documentation](https://ai.pydantic.dev/mcp/fastmcp-client/).
+
+
+The `FastMCPToolset` currently exposes **tools** to the agent. Other MCP features such as elicitation and sampling are not yet supported through this toolset; use Pydantic AI's standard [`MCPServer`](https://ai.pydantic.dev/mcp/client/) client if you need them.
+
+
+## Install
+
+`FastMCPToolset` lives in `pydantic-ai-slim` behind the `fastmcp` optional group:
+
+```bash
+pip install "pydantic-ai-slim[fastmcp]"
+```
+
+## Create a Server
+
+Create a FastMCP server with the tools you want to expose. We'll use a single dice-rolling tool throughout this guide.
+
+```python server.py
+import random
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="Dice Roller")
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ """Roll `n_dice` 6-sided dice and return the results."""
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
+```
+
+## In-Memory
+
+If your FastMCP server lives in the same process as your agent, pass the `FastMCP` instance directly. The toolset reuses an [in-memory transport](/clients/transports#in-memory-transport), which avoids a network round trip and is the fastest option for tests and embedded use.
+
+```python
+import asyncio
+import random
+from fastmcp import FastMCP
+from pydantic_ai import Agent
+from pydantic_ai.toolsets.fastmcp import FastMCPToolset
+
+mcp = FastMCP(name="Dice Roller")
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+toolset = FastMCPToolset(mcp)
+agent = Agent("openai:gpt-4.1", toolsets=[toolset])
+
+async def main():
+ result = await agent.run("Roll 3 dice!")
+ print(result.output)
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+## Streamable HTTP
+
+For a remote FastMCP server reachable over HTTP, pass the URL as a string. The toolset infers the [Streamable HTTP transport](/clients/transports#http-transport) from the URL.
+
+```python
+from pydantic_ai import Agent
+from pydantic_ai.toolsets.fastmcp import FastMCPToolset
+
+toolset = FastMCPToolset("https://your-server-url.com/mcp")
+agent = Agent("openai:gpt-4.1", toolsets=[toolset])
+```
+
+For [SSE](/clients/transports#sse-transport), use a `/sse` URL instead.
+
+## STDIO
+
+To launch a FastMCP server as a subprocess, pass a script path and the toolset will use the [STDIO transport](/clients/transports#stdio-transport).
+
+```python
+from pydantic_ai import Agent
+from pydantic_ai.toolsets.fastmcp import FastMCPToolset
+
+toolset = FastMCPToolset("server.py")
+agent = Agent("openai:gpt-4.1", toolsets=[toolset])
+```
+
+You can also pass a [`StdioTransport`](/clients/transports#stdio-transport) directly when you need control over the command, args, or environment.
+
+## MCP Configuration
+
+To wire up multiple servers at once, pass an [MCP configuration](/integrations/mcp-json-configuration) dictionary. The toolset opens one client per server and exposes all of their tools to the agent.
+
+```python
+from pydantic_ai import Agent
+from pydantic_ai.toolsets.fastmcp import FastMCPToolset
+
+mcp_config = {
+ "mcpServers": {
+ "dice": {"command": "python", "args": ["server.py"]},
+ "weather": {"url": "https://weather.example.com/mcp"},
+ }
+}
+
+toolset = FastMCPToolset(mcp_config)
+agent = Agent("openai:gpt-4.1", toolsets=[toolset])
+```
+
+## Authentication
+
+Because `FastMCPToolset` wraps a [FastMCP `Client`](/clients/client), it inherits the client's full [authentication](/clients/auth/bearer) story. To pass credentials such as a bearer token to a remote server, build a `Client` (or `StreamableHttpTransport`) yourself and hand it to the toolset.
+
+```python
+from fastmcp import Client
+from fastmcp.client.transports import StreamableHttpTransport
+from pydantic_ai import Agent
+from pydantic_ai.toolsets.fastmcp import FastMCPToolset
+
+transport = StreamableHttpTransport(
+ url="https://your-server-url.com/mcp",
+ headers={"Authorization": "Bearer your-access-token"},
+)
+
+toolset = FastMCPToolset(Client(transport))
+agent = Agent("openai:gpt-4.1", toolsets=[toolset])
+```
+
+For OAuth flows, use FastMCP's [`OAuth` helper](/clients/auth/oauth) when constructing the `Client`. For server-side token verification, see [Token Verification](/servers/auth/token-verification).
diff --git a/docs/integrations/scalekit.mdx b/docs/integrations/scalekit.mdx
index 191b81ca2..ac04b46df 100644
--- a/docs/integrations/scalekit.mdx
+++ b/docs/integrations/scalekit.mdx
@@ -28,12 +28,12 @@ In your Scalekit dashboard:
2. Enter server details: a name, a resource identifier, and the desired MCP client authentication settings
3. Save, then copy the **Resource ID** (for example, res_92015146095)
-In your FastMCP project's `.env`:
+Record these values in a `.env` file in your FastMCP project:
-```sh
-SCALEKIT_ENVIRONMENT_URL=
-SCALEKIT_RESOURCE_ID= # res_926EXAMPLE5878
-BASE_URL=http://localhost:8000/
+```sh .env
+SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com
+SCALEKIT_RESOURCE_ID=res_926EXAMPLE5878
+BASE_URL=http://localhost:8000
# Optional: additional scopes tokens must have
# SCALEKIT_REQUIRED_SCOPES=read,write
```
@@ -43,20 +43,25 @@ BASE_URL=http://localhost:8000/
### Step 2: Add auth to FastMCP server
-Create your FastMCP server file and use the ScalekitProvider to handle all the OAuth integration automatically:
+Create your FastMCP server file and use the ScalekitProvider to handle all the OAuth integration automatically. Nothing reads `.env` automatically, so load it explicitly with [python-dotenv](https://pypi.org/project/python-dotenv/) (`pip install python-dotenv`) before constructing the provider.
> **Warning:** The legacy `mcp_url` and `client_id` parameters are deprecated and will be removed in a future release. Use `base_url` instead of `mcp_url` and remove `client_id` from your configuration.
```python server.py
+import os
+
+from dotenv import load_dotenv
from fastmcp import FastMCP
from fastmcp.server.auth.providers.scalekit import ScalekitProvider
-# Discovers Scalekit endpoints and set up JWT token validation
+load_dotenv()
+
+# Discovers Scalekit endpoints and sets up JWT token validation
auth_provider = ScalekitProvider(
- environment_url=SCALEKIT_ENVIRONMENT_URL, # Scalekit environment URL
- resource_id=SCALEKIT_RESOURCE_ID, # Resource server ID
- base_url=SERVER_URL, # Public MCP endpoint
- required_scopes=["read"], # Optional scope enforcement
+ environment_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], # Scalekit environment URL
+ resource_id=os.environ["SCALEKIT_RESOURCE_ID"], # Resource server ID
+ base_url=os.environ.get("BASE_URL", "http://localhost:8000"),
+ required_scopes=["read"], # Optional scope enforcement
)
# Create FastMCP server with auth
@@ -86,7 +91,7 @@ Set `required_scopes` when you need tokens to carry specific permissions. Leave
uv run python server.py
```
-Use any MCP client (for example, mcp-inspector, Claude, VS Code, or Windsurf) to connect to the running serve. Verify that authentication succeeds and requests are authorized as expected.
+Use any MCP client (for example, mcp-inspector, Claude, VS Code, or Windsurf) to connect to the running server. Verify that authentication succeeds and requests are authorized as expected.
## Production Configuration
@@ -99,8 +104,8 @@ from fastmcp.server.auth.providers.scalekit import ScalekitProvider
# Load configuration from environment variables
auth = ScalekitProvider(
- environment_url=os.environ.get("SCALEKIT_ENVIRONMENT_URL"),
- resource_id=os.environ.get("SCALEKIT_RESOURCE_ID"),
+ environment_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
+ resource_id=os.environ["SCALEKIT_RESOURCE_ID"],
base_url=os.environ.get("BASE_URL", "https://your-server.com")
)
@@ -134,22 +139,15 @@ logging.basicConfig(level=logging.DEBUG)
You can inspect JWT tokens in your tools to understand the user context:
```python
-from fastmcp.server.context import request_ctx
-import jwt
+from fastmcp.server.dependencies import get_access_token
@mcp.tool
def inspect_token() -> dict:
"""Inspect the current JWT token claims."""
- context = request_ctx.get()
+ token = get_access_token()
+ if token is None:
+ return {"error": "No token found"}
- # Extract token from Authorization header
- if hasattr(context, 'request') and hasattr(context.request, 'headers'):
- auth_header = context.request.headers.get('authorization', '')
- if auth_header.startswith('Bearer '):
- token = auth_header[7:]
- # Decode without verification (already verified by provider)
- claims = jwt.decode(token, options={"verify_signature": False})
- return claims
-
- return {"error": "No token found"}
+ # Claims were already verified by the auth provider.
+ return token.claims
```
diff --git a/docs/integrations/supabase.mdx b/docs/integrations/supabase.mdx
index 9ffda444d..d39696da7 100644
--- a/docs/integrations/supabase.mdx
+++ b/docs/integrations/supabase.mdx
@@ -30,7 +30,8 @@ Before you begin, you will need:
2. **OAuth Server enabled** in your Supabase Dashboard (Authentication → OAuth Server)
3. **Dynamic Client Registration enabled** in the same settings
4. A **consent UI** hosted at your configured authorization path (see above)
-5. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
+5. Your Supabase Auth JWT signing algorithm. `SupabaseProvider` defaults to `ES256`; set `algorithm="RS256"` if your project is configured for RSA signing.
+6. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
### Step 1: Enable Supabase OAuth Server
@@ -58,6 +59,7 @@ from fastmcp.server.auth.providers.supabase import SupabaseProvider
auth = SupabaseProvider(
project_url="https://abc123.supabase.co",
base_url="http://localhost:8000",
+ algorithm="ES256", # Match your Supabase Auth JWT signing algorithm
)
mcp = FastMCP("Supabase Protected Server", auth=auth)
@@ -117,6 +119,7 @@ from fastmcp.server.auth.providers.supabase import SupabaseProvider
auth = SupabaseProvider(
project_url=os.environ["SUPABASE_PROJECT_URL"],
base_url=os.environ.get("BASE_URL", "https://your-server.com"),
+ algorithm=os.environ.get("SUPABASE_JWT_ALGORITHM", "ES256"),
)
mcp = FastMCP(name="Supabase Secured App", auth=auth)
diff --git a/docs/language-dropdown.js b/docs/language-dropdown.js
new file mode 100644
index 000000000..4eb5131f6
--- /dev/null
+++ b/docs/language-dropdown.js
@@ -0,0 +1,77 @@
+// Language dropdown: a small Python/TypeScript switcher injected into the
+// sidebar footer, next to Mintlify's theme selector. Selecting the other
+// language navigates to that project's docs site; selecting the current
+// language is a no-op. Styling lives in css/language-dropdown.css.
+(function () {
+ if (typeof window === "undefined") return;
+
+ var CURRENT_LANGUAGE = "python";
+
+ // TODO: fastmcp-ts has no public docs site URL discoverable in either repo
+ // yet. Until it exists, point at the repo README (the same cross-link the
+ // welcome page uses), then replace with the real docs URL.
+ var TYPESCRIPT_DOCS_URL = "https://github.com/PrefectHQ/fastmcp-ts";
+ var PYTHON_DOCS_URL = "https://gofastmcp.com";
+
+ var URLS = { python: PYTHON_DOCS_URL, typescript: TYPESCRIPT_DOCS_URL };
+
+ function findThemeSelector() {
+ // Mintlify's sidebar-footer DOM is not a stable public API, so probe a
+ // few markers (almond theme first) and give up quietly if none match.
+ return (
+ document.querySelector("[data-theme-preference-switch]") ||
+ document.querySelector('[role="group"][aria-label="Theme preference"]')
+ );
+ }
+
+ function buildDropdown() {
+ var label = document.createElement("label");
+ label.id = "language-switch";
+
+ var select = document.createElement("select");
+ select.setAttribute("aria-label", "Switch documentation language");
+
+ [
+ ["python", "Python"],
+ ["typescript", "TypeScript"],
+ ].forEach(function (entry) {
+ var option = document.createElement("option");
+ option.value = entry[0];
+ option.textContent = entry[1];
+ if (entry[0] === CURRENT_LANGUAGE) option.selected = true;
+ select.appendChild(option);
+ });
+
+ select.addEventListener("change", function () {
+ if (select.value === CURRENT_LANGUAGE) return;
+ window.location.href = URLS[select.value];
+ });
+
+ label.appendChild(select);
+ return label;
+ }
+
+ function addDropdown() {
+ if (document.getElementById("language-switch")) return;
+ var theme = findThemeSelector();
+ if (!theme || !theme.parentElement) return;
+ // Insert after the theme pill; margin-left:auto floats it right.
+ theme.parentElement.insertBefore(buildDropdown(), theme.nextSibling);
+ }
+
+ function run() {
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", addDropdown);
+ } else {
+ addDropdown();
+ }
+ }
+
+ run();
+
+ // Mintlify re-renders the sidebar on client-side navigation; re-inject when
+ // the dropdown disappears.
+ new MutationObserver(function () {
+ if (!document.getElementById("language-switch")) addDropdown();
+ }).observe(document.body, { subtree: true, childList: true });
+})();
diff --git a/docs/more/faq.mdx b/docs/more/faq.mdx
new file mode 100644
index 000000000..5566e908f
--- /dev/null
+++ b/docs/more/faq.mdx
@@ -0,0 +1,140 @@
+---
+title: FAQ
+description: Direct answers to the questions that come up most often about FastMCP 4, the protocol eras, and installation
+icon: circle-question
+---
+
+## Do I need to change my server code for FastMCP 4?
+
+Most servers run untouched. The defining change in FastMCP 4 is its engine — the MCP Python SDK v2 — and FastMCP absorbs nearly all of it for you, including the wire-wide rename from camelCase to snake_case, which is bridged so your existing reads keep working.
+
+Most of what does reach your code fails loudly at import or call time, and the fix is mechanical: `McpError(ErrorData(...))` becomes `McpError(code=..., message=...)`, custom `httpx` clients handed to a transport become `httpx2`, and `ctx.sample()` and `ctx.list_roots()` are gone.
+
+One change is silent, so go looking for it: an `except httpx.ConnectError:` around a FastMCP call still imports and still type-checks, because `httpx` usually remains installed through some other dependency — but FastMCP now raises the `httpx2` exception, so the handler simply stops matching and your fallback quietly never runs. Grep for `except httpx.` and move those to `httpx2`. [Upgrading from FastMCP 3](/getting-started/upgrading/from-fastmcp-3) covers each one and ends with a checklist.
+
+## Why does my client connect with a different protocol version than before?
+
+`fastmcp.Client` defaults to `mode="auto"` as of FastMCP 4, so it negotiates the newest era both sides speak rather than pinning the handshake. Over streamable HTTP or stdio to a FastMCP server that means the sessionless `2026-07-28` protocol, where FastMCP 3 connected at `2025-11-25`.
+
+Two transports are exceptions: SSE predates the sessionless era and cannot carry it, and a multi-server `MCPConfigTransport` mounts each backend behind a legacy-era composite. Under `mode="auto"` the client recognizes both and settles on the handshake without probing, so seeing `2025-11-25` there is correct rather than a negotiation failure. Pinning a modern version explicitly on either skips that substitution and asks the transport for something it cannot serve, so leave them on auto or legacy.
+
+The client probes `server/discover` and adopts the modern protocol when the server answers, falling back to the `initialize` handshake for anything that is not positive evidence of a modern peer — so a mixed fleet of servers still connects. Pin the old behavior per client with `Client(url, mode="legacy")`. See [Protocol negotiation](/clients/client#protocol-negotiation).
+
+## What are the two protocol eras, and which one does my server speak?
+
+Both. A FastMCP 4 server supports the handshake revisions `2024-11-05`, `2025-03-26`, `2025-06-18`, and `2025-11-25`, plus the modern `2026-07-28` protocol. It serves all of them from one deployment and one URL, and the SDK negotiates per connection — the client picks, not the server.
+
+The *handshake* era (`2025-11-25` and earlier) opens each connection with `initialize` and holds a session, which gives the server a back-channel it can push requests down. The *modern* era (`2026-07-28`) is sessionless: the client learns what the server offers through `server/discover`, every request stands alone, and there is no back-channel. Inside a tool, `ctx.request_context.protocol_version` tells you which era the current call arrived on; on the client, `client.protocol_version` reports it after connecting.
+
+A protocol version establishes the wire format, while capabilities describe which optional operations a particular server provides. The capabilities returned by `server/discover` or `initialize` are therefore the authoritative way for a client to determine what is available.
+
+## Can FastMCP 4 talk to older clients and servers?
+
+Yes, in both directions, with no configuration. A FastMCP 4 server answers a handshake-era client and a modern one from the same process: the old client sends `initialize` and gets a session id, the modern client discovers and stays stateless.
+
+A FastMCP 4 client is equally happy against an old server, because `mode="auto"` falls back to the handshake when discovery finds no modern peer. The client-side handlers for server-initiated capabilities are all still there too — passing `sampling_handler=` or `roots=` answers a legacy server's requests exactly as before, which is what a modern client needs in order to interoperate. See [client sampling](/clients/sampling) and [client roots](/clients/roots).
+
+## How does FastMCP verify protocol conformance?
+
+FastMCP runs the [official MCP conformance suite](https://github.com/modelcontextprotocol/conformance) in CI against a pinned suite release. A failing scenario for a released capability that FastMCP advertises as supported is treated as a regression.
+
+The suite's `all` mode also exercises draft, pending, retired, and deliberately unsupported capabilities, so its raw pass count is broader than FastMCP's support contract. Known exceptions are recorded in [`expected-failures.yml`](https://github.com/PrefectHQ/fastmcp/blob/main/tests/conformance/expected-failures.yml) with their rationale, and new upstream scenarios arrive through deliberate suite-version updates rather than silently changing CI.
+
+## When should I pin `mode="legacy"`?
+
+Pin it when your code depends on the session the handshake creates: `client.ping()` and `transport.get_session_id()` have no modern equivalent, since a sessionless connection has neither a live back-channel to ping nor an id to hold. It is also the escape hatch when a server misbehaves under discovery or you need the classic `initialize` result object.
+
+You do not need to pin it just because you registered a `sampling_handler`, `roots=`, or an `elicitation_handler`. None of the three require the handshake on their own: `mode="auto"` reaches whichever era the connection negotiates, and on a modern connection a tool can still exercise any of them through the guard pattern — it manually returns an `InputRequiredResult` embedding the request, and the same handler you already registered answers it. [Roots](/clients/roots) and [elicitation](/clients/elicitation) document this pattern directly; [sampling](/clients/sampling) works through the identical mechanism, though calling an LLM directly from the server is the recommended path there rather than a round trip for it.
+
+Pinning is per client, not a deployment setting: `Client(url, mode="legacy")`. The trade runs the other way as well — [background tasks](/clients/tasks) are modern-only, so a legacy client never triggers one and a task-enabled tool simply runs synchronously.
+
+## Why did my `ctx.sample()` code stop working?
+
+`ctx.sample()` and `ctx.sample_step()` are not part of FastMCP 4. Calling either raises `AttributeError` on every protocol era, and `FastMCP(sampling_handler=...)` raises `TypeError` naming the migration.
+
+Sampling was a server-to-client *request*: the server sent `sampling/createMessage` and blocked until an answer came back down the session. The modern protocol has no server-to-client request direction at all, so the pushed form has nowhere to go.
+
+The asking survives in a different shape. A tool can return an `InputRequiredResult` carrying a `CreateMessageRequest`; the client answers it through the same `sampling_handler` it already registers, and your tool runs again with the completion. Reach for that when using *the caller's* model is the point. Otherwise put generation in your server — hold a provider API key and call the model directly, which has the side benefit that your tool behaves identically for every client, including the many that never implemented sampling. [Sampling](/servers/sampling) shows both.
+
+## What happened to `ctx.list_roots()`?
+
+Removed, for the same reason as sampling: `roots/list` was a server-to-client request, and the modern protocol has no channel to send one.
+
+Take the paths you need as ordinary tool arguments. The agent already knows which directory it is working in, and an explicit argument is visible in the tool's schema instead of hidden in a protocol round-trip. When the caller genuinely has to be asked mid-run, the [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol) carries a roots request in its `input_requests` map alongside elicitation, and `fastmcp.Client` answers it from the `roots=` you already configured.
+
+## Why does `ctx.info()` still work when sampling doesn't?
+
+Because logging is a *notification* and sampling was a *request*. A notification is fire-and-forget: your server emits it down the response stream the caller already opened, and nothing has to be held open on the server's behalf. A request needs an answer to come back the other way, which requires a live connection the server can reach into.
+
+The modern protocol kept every server notification — `notifications/message`, `notifications/progress`, and the list-changed family — and removed the server-to-client request direction entirely. So `ctx.info()`, `ctx.debug()`, and `ctx.report_progress()` reach the client mid-call on every era, while sampling and roots have no era-agnostic form and were dropped. [Sampling](/servers/sampling#the-removed-methods) works through the distinction in full.
+
+You may see an `MCPDeprecationWarning` from the SDK about the logging capability being deprecated as of `2026-07-28`. It refers to the capability declaration, not to the notification, and delivery is unaffected.
+
+## Why can't I call `client.set_logging_level()` anymore?
+
+On a modern connection it raises, because `logging/setLevel` is not in the `2026-07-28` protocol. The method asked the server to remember a level for the rest of the session, and a sessionless protocol has nowhere to keep that.
+
+The messages themselves are unaffected — the server still sends whatever its own configuration allows. Filter on the receiving side in your `log_handler`, which sees each message's `level` field. See [Client Logging](/clients/logging). On a handshake-era connection (`Client(url, mode="legacy")`) the call works as before.
+
+Receiving-side filtering only narrows what already arrives. A server that sets `FastMCP(client_log_level="error")` drops anything below that threshold before it reaches the wire, and a modern client has no way to ask for the missing levels — the server operator has to lower `client_log_level` for them to be sent at all.
+
+## What replaces elicitation on the modern protocol?
+
+The guard pattern. Rather than pausing mid-execution to ask, a tool *returns* an `InputRequiredResult` describing what it needs. That round completes normally, the client collects the answer, and it calls the tool again with the answer attached. Any state you carry between rounds is sealed by the framework before it reaches the wire, so the client holds an opaque token it cannot read or forge.
+
+`ctx.elicit()` still works on handshake-era connections and raises on modern ones, so a server that must serve both eras needs both paths. `fastmcp.Client` drives whichever the connection negotiated with no extra wiring on your side. See [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol).
+
+## Why doesn't my middleware's `on_initialize` hook run?
+
+Because the modern protocol has no `initialize` request. The hook fires on handshake-era connections and never on modern ones, and since `Client` now defaults to `mode="auto"`, that is the common case against a FastMCP 4 server.
+
+Work that must happen once per process belongs in the server [lifespan](/servers/lifespan). Per-request work such as an auth check belongs in `on_request` or a specific operation hook, both of which run on every era — on a modern connection `on_request` sees `server/discover` where a handshake connection sees `initialize`. See [Middleware](/servers/middleware).
+
+## Why doesn't state I set in one tool call show up in the next?
+
+On a modern connection every request is a fresh connection, so `ctx.set_state` lives only for the duration of the call that wrote it. The same code persists state across calls on a handshake-era connection, which is why it appears to break the moment a client negotiates `2026-07-28`.
+
+[Session state](/servers/sessions) is the durable answer, following MCP's own decision to move session semantics up into the application. Declare a `UserSession` parameter and FastMCP injects one bucket of stored state keyed to the authenticated user, with nothing to pass around. Declare a `SessionId` argument when a single user needs several independent sessions, and the caller mints an id with `create_session` and supplies it on each call — register `mcp.add_provider(SessionProvider())` first, since `create_session` doesn't exist until a `SessionProvider` contributes it. Both store server-side and key to the authenticated caller's identity, so a handle is inert in anyone else's hands.
+
+That isolation comes from authentication, not from the id. On an unauthenticated server there is no principal to key on, so every session shares one anonymous namespace and a `SessionId` becomes a bearer capability — anyone holding it can read and write that state. Treat unauthenticated sessions as single-tenant or trusted-network only; `UserSession` sidesteps the question by requiring an authenticated principal outright.
+
+## How do I run background tasks now?
+
+The same way, plus one registration. `@mcp.tool(task=True)` is still the authoring surface and [Docket](https://github.com/chrisguidry/docket) still runs the work. What changed is underneath: tasks left the core MCP spec and returned as the `io.modelcontextprotocol/tasks` extension (SEP-2663), which FastMCP implements in the optional `fastmcp-tasks` package.
+
+Install `fastmcp[tasks]` and register the extension on your server. A `task=True` tool on a server with no tasks extension refuses to start and names the fix, so a missing registration is impossible to ship by accident.
+
+```python
+from fastmcp import FastMCP
+from fastmcp_tasks import TasksExtension
+
+mcp = FastMCP("MyServer")
+mcp.add_extension(TasksExtension())
+
+
+@mcp.tool(task=True)
+async def slow_computation(duration: int) -> str:
+ return "done"
+```
+
+Tasks are modern-only: the capability is negotiated over `2026-07-28`, so a `mode="legacy"` client never triggers one. A tool marked `task=True` (equivalently `mode="optional"`) then just runs synchronously. A tool that sets `TaskConfig(mode="required")` has no synchronous form to fall back to, so the call fails with a missing-required-capability error instead. See [Background Tasks](/servers/tasks).
+
+## `import fastmcp` stopped working after I upgraded with pip
+
+This can happen when you upgrade to FastMCP 3.3 or later from FastMCP 3.2 or earlier with `pip`. The quick fix is `pip install --force-reinstall fastmcp`. See [Troubleshooting](/getting-started/installation#troubleshooting) for the clean-reinstall fallback and an explanation of why it happens.
+
+## What's the difference between `fastmcp` and `fastmcp-slim`?
+
+`fastmcp` is the full distribution. Installing it gives you the complete framework — server, client, CLI, and the common integrations — and is the right choice for most users:
+
+```bash
+pip install fastmcp
+```
+
+`fastmcp-slim` ships the same importable `fastmcp` package with a minimal set of required dependencies. You opt into the pieces you need through extras, which keeps environments lean when you only use part of the framework:
+
+```bash
+pip install "fastmcp-slim[client]"
+```
+
+Both distributions expose the same `import fastmcp`, so application code is identical regardless of which one you install.
diff --git a/docs/more/settings.mdx b/docs/more/settings.mdx
new file mode 100644
index 000000000..e6ea0f584
--- /dev/null
+++ b/docs/more/settings.mdx
@@ -0,0 +1,113 @@
+---
+title: Settings
+description: Configure FastMCP behavior through environment variables or a .env file.
+icon: gear
+---
+
+FastMCP uses [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) for configuration. Every setting is available as an environment variable with a `FASTMCP_` prefix. Settings are loaded from environment variables and from a `.env` file.
+
+```bash
+# Set via environment
+export FASTMCP_LOG_LEVEL=DEBUG
+export FASTMCP_PORT=3000
+
+# Or use a .env file (loaded automatically)
+echo "FASTMCP_LOG_LEVEL=DEBUG" >> .env
+```
+
+You can change which `.env` file is loaded by setting the `FASTMCP_ENV_FILE` environment variable (defaults to `.env`). Because this controls which file is loaded, it must be set as an environment variable — it cannot be set inside a `.env` file itself.
+
+## Logging
+
+| Environment Variable | Type | Default | Description |
+|---|---|---|---|
+| `FASTMCP_LOG_LEVEL` | `Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]` | `INFO` | Log level for FastMCP's own logging output. Case-insensitive. |
+| `FASTMCP_LOG_ENABLED` | `bool` | `true` | Enable or disable FastMCP logging entirely. |
+| `FASTMCP_CLIENT_LOG_LEVEL` | `Literal["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"]` | None | Default minimum log level for messages sent to MCP clients via `context.log()`. When set, messages below this level are suppressed. Handshake-era clients can override this per-session using the MCP `logging/setLevel` request; the modern protocol has no session to hold that level, so clients on it filter by level in their own log handler instead. |
+| `FASTMCP_ENABLE_RICH_LOGGING` | `bool` | `true` | Use rich formatting for log output. Set to `false` for plain Python logging. |
+| `FASTMCP_ENABLE_RICH_TRACEBACKS` | `bool` | `true` | Use rich tracebacks for errors. |
+| `FASTMCP_DEPRECATION_WARNINGS` | `bool` | `true` | Show deprecation warnings. |
+| `FASTMCP_MCP_CAMELCASE_COMPAT` | `bool` | `true` | Bridge legacy camelCase reads on MCP SDK objects (e.g. `tool.inputSchema`, `result.isError`) to their snake_case fields after the SDK v2 rename. Each bridged read emits a `FastMCPDeprecationWarning`. Set to `false` to disable the shims, in which case only the snake_case names resolve. |
+
+## Transport & HTTP
+
+These control how the server listens when running with an HTTP transport.
+
+| Environment Variable | Type | Default | Description |
+|---|---|---|---|
+| `FASTMCP_TRANSPORT` | `Literal["stdio", "http", "sse", "streamable-http"]` | `stdio` | Default transport. |
+| `FASTMCP_HOST` | `str` | `127.0.0.1` | Host to bind to. |
+| `FASTMCP_PORT` | `int` | `8000` | Port to bind to. |
+| `FASTMCP_SSE_PATH` | `str` | `/sse` | Path for SSE endpoint. |
+| `FASTMCP_MESSAGE_PATH` | `str` | `/messages/` | Path for SSE message endpoint. |
+| `FASTMCP_STREAMABLE_HTTP_PATH` | `str` | `/mcp` | Path for Streamable HTTP endpoint. |
+| `FASTMCP_STATELESS_HTTP` | `bool` | `false` | Enable stateless HTTP mode (new transport per request). Useful for multi-worker deployments. |
+| `FASTMCP_JSON_RESPONSE` | `bool` | `false` | Use JSON responses instead of SSE for Streamable HTTP. |
+| `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION` | `bool \| "auto"` | `false` | Validate `Host` and browser `Origin` headers for Streamable HTTP requests. `auto` protects localhost-bound servers and explicit host/origin allowlists. |
+| `FASTMCP_HTTP_ALLOWED_HOSTS` | `list[str] \| null` | `null` | Additional trusted hostnames when Host and Origin protection is enabled. Use a JSON array, such as `["mcp.example.com"]`. |
+| `FASTMCP_HTTP_ALLOWED_ORIGINS` | `list[str] \| null` | `null` | Browser origins trusted when Host and Origin protection is enabled. Configure CORS separately for cross-origin browser reads. Use a JSON array, such as `["https://app.example.com"]`. |
+| `FASTMCP_HTTP_SESSION_IDLE_TIMEOUT` | `float \| null` | `null` | Seconds a Streamable HTTP session may remain idle before it is terminated. The deadline resets on every request. When `null`, sessions never expire from inactivity. Not supported in stateless mode. |
+| `FASTMCP_DEBUG` | `bool` | `false` | Enable debug mode. |
+
+## Error Handling
+
+| Environment Variable | Type | Default | Description |
+|---|---|---|---|
+| `FASTMCP_MASK_ERROR_DETAILS` | `bool` | `false` | Mask error details before sending to clients. When enabled, only messages from explicitly raised `ToolError`, `ResourceError`, or `PromptError` are included in responses. |
+| `FASTMCP_STRICT_INPUT_VALIDATION` | `bool` | `false` | Strictly validate tool inputs against the JSON schema. When disabled, compatible inputs are coerced (e.g., the string `"10"` becomes the integer `10`). |
+| `FASTMCP_MOUNTED_COMPONENTS_RAISE_ON_LOAD_ERROR` | `bool` | `false` | Raise errors when loading mounted components instead of logging warnings. |
+
+## Client
+
+| Environment Variable | Type | Default | Description |
+|---|---|---|---|
+| `FASTMCP_CLIENT_INIT_TIMEOUT` | `float \| None` | None | Timeout in seconds for the client initialization handshake. Set to `0` or leave unset to disable. |
+| `FASTMCP_CLIENT_DISCONNECT_TIMEOUT` | `float` | `5` | Maximum time in seconds to wait for a clean disconnect before giving up. |
+| `FASTMCP_TASKS_CLIENT_POLL_INTERVAL` | `float` | `0.5` | Ceiling in seconds for the fallback poll backoff while waiting on a [background task](/servers/tasks). Requires the `fastmcp-tasks` package. Applies **only** when the server does not advertise its own `pollInterval`: in that case `Task.wait()` starts polling fast (~20ms) and doubles up to this ceiling rather than polling at a fixed cadence. When the server advertises a `pollInterval`, that interval is honored exactly and this setting is ignored. |
+| `FASTMCP_CLIENT_RAISE_FIRST_EXCEPTIONGROUP_ERROR` | `bool` | `true` | When an `ExceptionGroup` is raised, re-raise the first error directly instead of the group. Simplifies debugging but may mask secondary errors. |
+
+## CLI & Display
+
+| Environment Variable | Type | Default | Description |
+|---|---|---|---|
+| `FASTMCP_SHOW_SERVER_BANNER` | `bool` | `true` | Show the server banner on startup. Also controllable via `--no-banner` or `server.run(show_banner=False)`. |
+| `FASTMCP_CHECK_FOR_UPDATES` | `Literal["stable", "prerelease", "off"]` | `stable` | Update checking on CLI startup. `stable` checks stable releases only, `prerelease` includes pre-releases, `off` disables checking. |
+
+## Telemetry
+
+| Environment Variable | Type | Default | Description |
+|---|---|---|---|
+| `FASTMCP_TELEMETRY_MODE` | `Literal["native", "propagation_only", "off"]` | `native` | Controls FastMCP's native [OpenTelemetry instrumentation](/servers/telemetry). `native` emits FastMCP's MCP spans and propagates trace context; because FastMCP uses only the OpenTelemetry API, this costs almost nothing unless an SDK and exporter are configured. `propagation_only` keeps `_meta` trace propagation and still parents downstream spans from the incoming context, but emits none of FastMCP's own spans, so another instrumentation layer can own the MCP span hierarchy. `off` is a full pass-through: no spans, and no trace context extracted or attached. |
+
+## Tasks (Docket)
+
+Task settings (the `FASTMCP_DOCKET_` and `FASTMCP_TASKS_` variables) live in the optional `fastmcp-tasks` package. See [server tasks](/servers/tasks) for configuration, including `FASTMCP_TASKS_ENCRYPTION_KEY` for [encrypting task snapshots at rest](/servers/tasks#credentials-at-rest).
+
+## Security
+
+These control FastMCP's SSRF protection for the outbound fetches it makes during authentication (OAuth client metadata and JWKS).
+
+| Environment Variable | Type | Default | Description |
+|---|---|---|---|
+| `FASTMCP_SSRF_TRUST_PROXY` | `bool` | `false` | Trust an outbound HTTP proxy for SSRF-protected fetches. When `false`, FastMCP resolves the target hostname itself and refuses to connect if it maps to a private, loopback, link-local, or reserved IP. When `true`, FastMCP routes auth metadata and JWKS fetches through the configured `HTTPS_PROXY`/`ALL_PROXY` and does not honor `NO_PROXY`; if no proxy is configured the fetch is refused. |
+
+By default, FastMCP protects its OAuth and JWKS fetches against [SSRF](https://owasp.org/www-community/attacks/Server_Side_Request_Forgery) by resolving the target hostname, rejecting any address that maps to a private, loopback, link-local, or reserved IP, and then pinning the connection to that validated IP.
+
+This breaks when a corporate `CONNECT` proxy is the only egress path: the container often cannot resolve external DNS at all (only the proxy can), and even when it can, pinning to the IP makes TLS verification fail because public certificates list hostnames, not IP addresses.
+
+Set `FASTMCP_SSRF_TRUST_PROXY=true` when a trusted proxy is your mandated egress. FastMCP then skips DNS resolution and the IP blocklist entirely and makes a single request to the hostname URL, explicitly routed through the proxy named by the standard `HTTPS_PROXY` / `ALL_PROXY` environment variables (checked in that order). The HTTPS-only and hostname checks still apply.
+
+
+This is a deliberate trust shift: the IP blocklist cannot be enforced through a proxy (the proxy does its own DNS, so an address FastMCP resolved is not the one the proxy dials). Only enable it when the proxy itself is trusted to mediate egress.
+
+FastMCP reads the proxy URL from the environment and passes it to the HTTP client explicitly, with the client's own environment-based proxy routing turned off — so the request either goes through that exact proxy or fails outright, with no routing decision left for the client to make on its own. One consequence: `NO_PROXY` is **not honored** in this mode. A host that `NO_PROXY` would otherwise exclude is still routed through the configured proxy rather than fetched direct with the IP blocklist disabled — the safer of the two options, since the blocklist cannot apply to a direct fetch here anyway. If you set `FASTMCP_SSRF_TRUST_PROXY=true` but neither `HTTPS_PROXY` nor `ALL_PROXY` is present in the server process's environment (an `HTTP_PROXY` alone never routes these HTTPS-only fetches), the request would otherwise go out **direct with the IP blocklist disabled** — no SSRF protection at all. Rather than send it, FastMCP refuses the fetch and raises `SSRFError` with an actionable message. The contract is crisp: proxy-trust mode delegates SSRF protection to the proxy, and with no proxy configured the fetch cannot proceed. Enable this setting only together with an active proxy that routes your auth endpoints.
+
+
+## Advanced
+
+| Environment Variable | Type | Default | Description |
+|---|---|---|---|
+| `FASTMCP_HOME` | `Path` | Platform default | Data directory for FastMCP. Defaults to the platform-specific user data directory. |
+| `FASTMCP_ENV_FILE` | `str` | `.env` | Path to the `.env` file to load settings from. Must be set as an environment variable (see above). |
+| `FASTMCP_SERVER_DEPENDENCIES` | `list[str]` | `[]` | Additional dependencies to install in the server environment. |
+| `FASTMCP_TEST_MODE` | `bool` | `false` | Enable test mode. |
diff --git a/docs/patterns/contrib.mdx b/docs/patterns/contrib.mdx
index d2f812f52..648cf8722 100644
--- a/docs/patterns/contrib.mdx
+++ b/docs/patterns/contrib.mdx
@@ -12,13 +12,13 @@ FastMCP includes a `contrib` package that holds community-contributed modules. T
Contrib modules provide additional features, integrations, or patterns that complement the core FastMCP library. They offer a way for the community to share useful extensions while keeping the core library focused and maintainable.
-The available modules can be viewed in the [contrib directory](https://github.com/PrefectHQ/fastmcp/tree/main/src/fastmcp/contrib).
+The available modules can be viewed in the [contrib directory](https://github.com/PrefectHQ/fastmcp/tree/main/fastmcp_slim/fastmcp/contrib).
## Usage
To use a contrib module, import it from the `fastmcp.contrib` package:
-```python
+```python test="skip"
from fastmcp.contrib import my_module
```
@@ -30,12 +30,12 @@ from fastmcp.contrib import my_module
## Contributing
-We welcome contributions to the `contrib` package! If you have a module that extends FastMCP in a useful way, consider contributing it:
+Contrib modules are accepted selectively. Before opening a PR, first open an issue with the problem, intended maintenance model, and why the pattern belongs in-repo instead of a standalone package. If maintainers agree it belongs in `contrib`, prepare the module with:
-1. Create a new directory in `src/fastmcp/contrib/` for your module
-3. Add proper tests for your module in `tests/contrib/`
-2. Include comprehensive documentation in a README.md file, including usage and examples, as well as any additional dependencies or installation instructions
-5. Submit a pull request
+1. Create a new directory in `fastmcp_slim/fastmcp/contrib/` for your module
+2. Add proper tests for your module in `tests/contrib/`
+3. Include comprehensive documentation in a README.md file, including usage and examples, as well as any additional dependencies or installation instructions
+4. Submit a focused pull request linked to the maintainer-approved issue
The ideal contrib module:
- Solves a specific use case or integration need
diff --git a/docs/prefab-demo-payloads.js b/docs/prefab-demo-payloads.js
new file mode 100644
index 000000000..885ce713b
--- /dev/null
+++ b/docs/prefab-demo-payloads.js
@@ -0,0 +1 @@
+window.__FASTMCP_PREFAB_DEMOS__ = {"bar-chart":"\n\n\n Prefab \n \n \n \n \n\n\n
\n \n\n","contacts":"\n\n\n Prefab \n \n \n \n \n\n\n
\n \n\n","dashboard":"\n\n\n Prefab \n \n \n \n \n\n\n
\n \n\n","data-table":"\n\n\n Prefab \n \n \n \n \n\n\n
\n \n\n","hitchhikers":"\n\n\n Prefab Showcase \n \n \n \n \n\n\n
\n \n\n","pie-chart":"\n\n\n Prefab \n \n \n \n \n\n\n
\n \n\n","reactive":"\n\n\n Prefab \n \n \n \n \n\n\n
\n \n\n","team-directory-reactive":"\n\n\n Prefab \n \n \n \n \n\n\n
\n \n\n","team-directory":"\n\n\n Prefab \n \n \n \n \n\n\n
\n \n\n"};
diff --git a/docs/python-sdk-pages.json b/docs/python-sdk-pages.json
new file mode 100644
index 000000000..32abc995c
--- /dev/null
+++ b/docs/python-sdk-pages.json
@@ -0,0 +1,116 @@
+[
+ "python-sdk/fastmcp-cli",
+ "python-sdk/fastmcp-decorators",
+ "python-sdk/fastmcp-dependencies",
+ "python-sdk/fastmcp-exceptions",
+ "python-sdk/fastmcp-mcp_config",
+ "python-sdk/fastmcp-settings",
+ "python-sdk/fastmcp-telemetry",
+ "python-sdk/fastmcp-types",
+ {
+ "group": "fastmcp.apps",
+ "pages": [
+ "python-sdk/fastmcp-apps-__init__",
+ "python-sdk/fastmcp-apps-app",
+ "python-sdk/fastmcp-apps-approval",
+ "python-sdk/fastmcp-apps-choice",
+ "python-sdk/fastmcp-apps-config",
+ "python-sdk/fastmcp-apps-file_upload",
+ "python-sdk/fastmcp-apps-form",
+ "python-sdk/fastmcp-apps-generative"
+ ]
+ },
+ {
+ "group": "fastmcp.experimental",
+ "pages": [
+ {
+ "group": "transforms",
+ "pages": [
+ "python-sdk/fastmcp-experimental-transforms-code_mode"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "fastmcp.server",
+ "pages": [
+ "python-sdk/fastmcp-server-caching",
+ "python-sdk/fastmcp-server-completions",
+ "python-sdk/fastmcp-server-context",
+ "python-sdk/fastmcp-server-dependencies",
+ "python-sdk/fastmcp-server-elicitation",
+ "python-sdk/fastmcp-server-event_store",
+ "python-sdk/fastmcp-server-extensions",
+ "python-sdk/fastmcp-server-http",
+ "python-sdk/fastmcp-server-lifespan",
+ "python-sdk/fastmcp-server-low_level",
+ "python-sdk/fastmcp-server-mixins",
+ "python-sdk/fastmcp-server-providers",
+ "python-sdk/fastmcp-server-server",
+ "python-sdk/fastmcp-server-session_scoped_event_store",
+ "python-sdk/fastmcp-server-sessions",
+ "python-sdk/fastmcp-server-telemetry",
+ "python-sdk/fastmcp-server-transforms"
+ ]
+ },
+ {
+ "group": "fastmcp.utilities",
+ "pages": [
+ "python-sdk/fastmcp-utilities-__init__",
+ "python-sdk/fastmcp-utilities-asgi_transport",
+ "python-sdk/fastmcp-utilities-async_utils",
+ "python-sdk/fastmcp-utilities-auth",
+ "python-sdk/fastmcp-utilities-authorization",
+ "python-sdk/fastmcp-utilities-cli",
+ "python-sdk/fastmcp-utilities-components",
+ "python-sdk/fastmcp-utilities-docstring_parsing",
+ "python-sdk/fastmcp-utilities-exceptions",
+ "python-sdk/fastmcp-utilities-http",
+ "python-sdk/fastmcp-utilities-inspect",
+ "python-sdk/fastmcp-utilities-json_schema",
+ "python-sdk/fastmcp-utilities-json_schema_type",
+ "python-sdk/fastmcp-utilities-lifespan",
+ "python-sdk/fastmcp-utilities-logging",
+ {
+ "group": "mcp_server_config",
+ "pages": [
+ "python-sdk/fastmcp-utilities-mcp_server_config-__init__",
+ {
+ "group": "v1",
+ "pages": [
+ {
+ "group": "environments",
+ "pages": [
+ "python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-__init__",
+ "python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base",
+ "python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv"
+ ]
+ },
+ "python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config",
+ {
+ "group": "sources",
+ "pages": [
+ "python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base",
+ "python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "python-sdk/fastmcp-utilities-mime",
+ "python-sdk/fastmcp-utilities-openapi",
+ "python-sdk/fastmcp-utilities-pagination",
+ "python-sdk/fastmcp-utilities-prefab",
+ "python-sdk/fastmcp-utilities-skills",
+ "python-sdk/fastmcp-utilities-tasks",
+ "python-sdk/fastmcp-utilities-tests",
+ "python-sdk/fastmcp-utilities-timeout",
+ "python-sdk/fastmcp-utilities-token_cache",
+ "python-sdk/fastmcp-utilities-types",
+ "python-sdk/fastmcp-utilities-ui",
+ "python-sdk/fastmcp-utilities-version_check",
+ "python-sdk/fastmcp-utilities-versions"
+ ]
+ }
+]
diff --git a/docs/python-sdk/fastmcp-apps-__init__.mdx b/docs/python-sdk/fastmcp-apps-__init__.mdx
new file mode 100644
index 000000000..5f69a4b59
--- /dev/null
+++ b/docs/python-sdk/fastmcp-apps-__init__.mdx
@@ -0,0 +1,16 @@
+---
+title: __init__
+sidebarTitle: __init__
+---
+
+# `fastmcp.apps`
+
+
+FastMCP Apps — interactive UIs for MCP tools.
+
+This package contains the app-related components:
+
+- ``FastMCPApp`` — composable provider for interactive apps with backend tools
+- ``AppConfig`` — configuration for MCP App tools and resources
+- ``ResourceCSP`` / ``ResourcePermissions`` — security configuration
+
diff --git a/docs/python-sdk/fastmcp-apps-app.mdx b/docs/python-sdk/fastmcp-apps-app.mdx
new file mode 100644
index 000000000..4d2e8a421
--- /dev/null
+++ b/docs/python-sdk/fastmcp-apps-app.mdx
@@ -0,0 +1,146 @@
+---
+title: app
+sidebarTitle: app
+---
+
+# `fastmcp.apps.app`
+
+
+FastMCPApp — a Provider that represents a composable MCP application.
+
+FastMCPApp binds entry-point tools (model calls these) together with backend
+tools (the UI calls these via CallTool). Backend tools are tagged with
+``meta["fastmcp"]["app"]`` so they can be found through the provider chain
+even when transforms (namespace, visibility, etc.) have renamed or hidden
+them — the server sets a context var that tells ``Provider.get_tool`` to
+fall back to a direct lookup for app-visible tools.
+
+Usage::
+
+ from fastmcp import FastMCP, FastMCPApp
+
+ app = FastMCPApp("Dashboard")
+
+ @app.ui()
+ def show_dashboard() -> Component:
+ return Column(...)
+
+ @app.tool()
+ def save_contact(name: str, email: str) -> str:
+ return name
+
+ server = FastMCP("Platform")
+ server.add_provider(app)
+
+
+## Classes
+
+### `FastMCPApp`
+
+
+A Provider that represents an MCP application.
+
+Binds together entry-point tools (``@app.ui``), backend tools
+(``@app.tool``), and the Prefab renderer resource. Backend tools
+are tagged with ``meta["fastmcp"]["app"]`` so ``Provider.get_tool``
+can find them by original name even when transforms have been applied.
+
+
+**Methods:**
+
+#### `tool`
+
+```python
+tool(self, name_or_fn: F) -> F
+```
+
+#### `tool`
+
+```python
+tool(self, name_or_fn: str | None = None) -> Callable[[F], F]
+```
+
+#### `tool`
+
+```python
+tool(self, name_or_fn: str | AnyFunction | None = None) -> Any
+```
+
+Register a backend tool that the UI calls via CallTool.
+
+Backend tools default to ``visibility=["app"]``. Pass ``model=True``
+to also expose the tool to the model (``visibility=["app", "model"]``).
+
+Supports multiple calling patterns::
+
+ @app.tool
+ def save(name: str): ...
+
+ @app.tool()
+ def save(name: str): ...
+
+ @app.tool("custom_name")
+ def save(name: str): ...
+
+
+#### `ui`
+
+```python
+ui(self, name_or_fn: F) -> F
+```
+
+#### `ui`
+
+```python
+ui(self, name_or_fn: str | None = None) -> Callable[[F], F]
+```
+
+#### `ui`
+
+```python
+ui(self, name_or_fn: str | AnyFunction | None = None) -> Any
+```
+
+Register a UI entry-point tool that the model calls.
+
+Entry-point tools default to ``visibility=["model"]`` and auto-wire
+the Prefab renderer resource and CSP. They are tagged with the app
+name so structured content includes ``_meta.fastmcp.app``.
+
+Supports multiple calling patterns::
+
+ @app.ui
+ def dashboard() -> Component: ...
+
+ @app.ui()
+ def dashboard() -> Component: ...
+
+ @app.ui("my_dashboard")
+ def dashboard() -> Component: ...
+
+
+#### `add_tool`
+
+```python
+add_tool(self, tool: Tool | Callable[..., Any]) -> Tool
+```
+
+Add a tool to this app programmatically.
+
+The tool is tagged with this app's name for routing.
+
+
+#### `lifespan`
+
+```python
+lifespan(self) -> AsyncIterator[None]
+```
+
+#### `run`
+
+```python
+run(self, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, **kwargs: Any) -> None
+```
+
+Create a temporary FastMCP server and run this app standalone.
+
diff --git a/docs/python-sdk/fastmcp-apps-approval.mdx b/docs/python-sdk/fastmcp-apps-approval.mdx
new file mode 100644
index 000000000..81285a76d
--- /dev/null
+++ b/docs/python-sdk/fastmcp-apps-approval.mdx
@@ -0,0 +1,58 @@
+---
+title: approval
+sidebarTitle: approval
+---
+
+# `fastmcp.apps.approval`
+
+
+Approval — a Provider that adds human-in-the-loop approval to any server.
+
+The LLM presents a summary of what it's about to do, and the user
+approves or rejects via buttons. The result is sent back into the
+conversation as a message, prompting the LLM's next turn.
+
+Requires ``fastmcp[apps]`` (prefab-ui).
+
+Usage::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.approval import Approval
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(Approval())
+
+
+## Classes
+
+### `Approval`
+
+
+A Provider that adds human-in-the-loop approval to a server.
+
+The LLM calls the ``request_approval`` tool with a summary and
+optional details. The user sees an approval card with Approve and
+Reject buttons. Clicking either sends a message back into the
+conversation (via ``SendMessage``), triggering the LLM's next turn.
+
+The message appears as if the user sent it, so the LLM sees
+something like ``'"Deploy v3.2 to production" is APPROVED'``.
+
+Example::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.approval import Approval
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(Approval())
+
+Customized::
+
+ Approval(
+ title="Deploy Gate",
+ approve_text="Ship it",
+ approve_variant="default",
+ reject_text="Abort",
+ reject_variant="destructive",
+ )
+
diff --git a/docs/python-sdk/fastmcp-apps-choice.mdx b/docs/python-sdk/fastmcp-apps-choice.mdx
new file mode 100644
index 000000000..8b7572622
--- /dev/null
+++ b/docs/python-sdk/fastmcp-apps-choice.mdx
@@ -0,0 +1,44 @@
+---
+title: choice
+sidebarTitle: choice
+---
+
+# `fastmcp.apps.choice`
+
+
+Choice — a Provider that lets the user pick from a set of options.
+
+The LLM presents options, the user clicks one, and the selection
+flows back into the conversation as a message.
+
+Requires ``fastmcp[apps]`` (prefab-ui).
+
+Usage::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.choice import Choice
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(Choice())
+
+
+## Classes
+
+### `Choice`
+
+
+A Provider that lets the user choose from a set of options.
+
+The LLM calls ``choose`` with a prompt and a list of options.
+The user sees a card with one button per option. Clicking a button
+sends the selection back into the conversation via ``SendMessage``,
+triggering the LLM's next turn.
+
+Example::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.choice import Choice
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(Choice())
+
diff --git a/docs/python-sdk/fastmcp-apps-config.mdx b/docs/python-sdk/fastmcp-apps-config.mdx
new file mode 100644
index 000000000..a7b9d6151
--- /dev/null
+++ b/docs/python-sdk/fastmcp-apps-config.mdx
@@ -0,0 +1,113 @@
+---
+title: config
+sidebarTitle: config
+---
+
+# `fastmcp.apps.config`
+
+
+MCP Apps support — extension negotiation and typed UI metadata models.
+
+Provides constants and Pydantic models for the MCP Apps extension
+(io.modelcontextprotocol/ui), enabling tools and resources to carry
+UI metadata for clients that support interactive app rendering.
+
+
+## Functions
+
+### `app_config_to_meta_dict`
+
+```python
+app_config_to_meta_dict(app: AppConfig | dict[str, Any]) -> dict[str, Any]
+```
+
+
+Convert an AppConfig or dict to the wire-format dict for ``meta["ui"]``.
+
+
+### `is_model_visible`
+
+```python
+is_model_visible(component: FastMCPComponent) -> bool
+```
+
+
+Whether a component may be shown to, or invoked by, the model.
+
+Visibility is a declaration, and the MCP Apps spec puts the filtering on
+the host — so ``tools/list`` carries app-only tools and the host keeps
+them from the model. That division only works where a host stands between
+the server and the model.
+
+It does not hold for surfaces a server drives itself. A search result or
+a code-mode catalog reaches the model as ordinary tool output, and a
+call-tool proxy invokes on a name the model supplies; nothing downstream
+can filter either. Those surfaces have to apply the declaration here.
+
+A component with no ``visibility`` is visible: the field marks the
+exception, and the spec's default is both audiences.
+
+
+## Classes
+
+### `ResourceCSP`
+
+
+Content Security Policy for MCP App resources.
+
+Declares which external origins the app is allowed to connect to or
+load resources from. Hosts use these declarations to build the
+``Content-Security-Policy`` header for the sandboxed iframe.
+
+
+### `ResourcePermissions`
+
+
+Iframe sandbox permissions for MCP App resources.
+
+Each field, when set (typically to ``{}``), requests that the host
+grant the corresponding Permission Policy feature to the sandboxed
+iframe. Hosts MAY honour these; apps should use JS feature detection
+as a fallback.
+
+
+### `AppConfig`
+
+
+Configuration for MCP App tools and resources.
+
+Controls how a tool or resource participates in the MCP Apps extension.
+On tools, ``resource_uri`` and ``visibility`` specify which UI resource
+to render and where the tool appears. On resources, those fields must
+be left unset (the resource itself is the UI).
+
+All fields use ``exclude_none`` serialization so only explicitly-set
+values appear on the wire. Aliases match the MCP Apps wire format
+(camelCase).
+
+
+### `PrefabAppConfig`
+
+
+App configuration for Prefab tools with sensible defaults.
+
+Like ``app=True`` but customizable. Auto-wires the Prefab renderer
+URI and merges the renderer's CSP with any additional domains you
+specify. The renderer resource is registered automatically.
+
+Example::
+
+ @mcp.tool(app=PrefabAppConfig()) # same as app=True
+
+ @mcp.tool(app=PrefabAppConfig(
+ csp=ResourceCSP(frame_domains=["https://example.com"]),
+ ))
+
+
+**Methods:**
+
+#### `model_post_init`
+
+```python
+model_post_init(self, __context: Any) -> None
+```
diff --git a/docs/python-sdk/fastmcp-apps-file_upload.mdx b/docs/python-sdk/fastmcp-apps-file_upload.mdx
new file mode 100644
index 000000000..878cbae32
--- /dev/null
+++ b/docs/python-sdk/fastmcp-apps-file_upload.mdx
@@ -0,0 +1,144 @@
+---
+title: file_upload
+sidebarTitle: file_upload
+---
+
+# `fastmcp.apps.file_upload`
+
+
+FileUpload — a Provider that adds drag-and-drop file upload to any server.
+
+Lets users upload files directly to the server through an interactive UI,
+bypassing the LLM context window entirely. The LLM can then read and work
+with uploaded files through model-visible tools.
+
+Requires ``fastmcp[apps]`` (prefab-ui).
+
+Usage::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps import FileUpload
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(FileUpload())
+
+For custom persistence, override the storage methods::
+
+ class S3Upload(FileUpload):
+ def on_store(self, files, ctx):
+ # write to S3, return summaries
+ ...
+
+ def on_list(self, ctx):
+ # list from S3
+ ...
+
+ def on_read(self, name, ctx):
+ # read from S3
+ ...
+
+
+## Classes
+
+### `FileUpload`
+
+
+A Provider that adds file upload capabilities to a server.
+
+Registers a drag-and-drop UI tool, a backend storage tool, and
+model-visible tools for listing and reading uploaded files.
+
+Files are scoped by MCP session and stored in memory by default.
+Override ``on_store``, ``on_list``, and ``on_read`` for custom
+persistence (filesystem, S3, database, etc.). Each method receives
+the current ``Context``, giving access to session ID, auth tokens,
+and request metadata for partitioning and authorization.
+
+**Session scoping:** The default storage uses ``ctx.session_id`` to
+isolate files by session. This works with stdio, SSE, and stateful
+HTTP transports. In **stateless HTTP** mode, each request creates a
+new session, so files won't persist across requests. For stateless
+deployments, override the storage methods to partition by a stable
+identifier from the auth context::
+
+ class UserScopedUpload(FileUpload):
+ def on_store(self, files, ctx):
+ user_id = ctx.access_token["sub"]
+ ...
+
+Example::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.file_upload import FileUpload
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(FileUpload())
+
+
+**Methods:**
+
+#### `on_store`
+
+```python
+on_store(self, files: list[dict[str, Any]], ctx: Context) -> list[dict[str, Any]]
+```
+
+Store uploaded files and return summaries.
+
+**Args:**
+- `files`: List of file dicts, each with ``name``, ``size``,
+``type``, and ``data`` (base64-encoded content).
+- `ctx`: The current request context. Use for session ID,
+auth tokens, or any metadata needed for partitioning.
+
+Override this method for custom persistence. The default
+implementation stores files in memory, scoped by
+``_get_scope_key(ctx)``.
+
+**Returns:**
+- List of file summary dicts (``name``, ``type``, ``size``,
+- ``size_display``, ``uploaded_at``).
+
+
+#### `on_list`
+
+```python
+on_list(self, ctx: Context) -> list[dict[str, Any]]
+```
+
+List all stored files.
+
+**Args:**
+- `ctx`: The current request context.
+
+Override this method for custom persistence. The default
+implementation returns files from the current scope.
+
+**Returns:**
+- List of file summary dicts.
+
+
+#### `on_read`
+
+```python
+on_read(self, name: str, ctx: Context) -> dict[str, Any]
+```
+
+Read a file's contents by name.
+
+**Args:**
+- `name`: The filename to read.
+- `ctx`: The current request context.
+
+Override this method for custom persistence. The default
+implementation reads from the current scope's in-memory store.
+Text files are decoded from base64; binary files return a
+truncated base64 preview.
+
+**Returns:**
+- Dict with file metadata and ``content`` (text) or
+- ``content_base64`` (binary preview).
+
+**Raises:**
+- `ValueError`: If the file is not found.
+
diff --git a/docs/python-sdk/fastmcp-apps-form.mdx b/docs/python-sdk/fastmcp-apps-form.mdx
new file mode 100644
index 000000000..c99110c7b
--- /dev/null
+++ b/docs/python-sdk/fastmcp-apps-form.mdx
@@ -0,0 +1,69 @@
+---
+title: form
+sidebarTitle: form
+---
+
+# `fastmcp.apps.form`
+
+
+FormInput — a Provider that collects structured input from the user.
+
+Define a Pydantic model for the data you need, and ``FormInput``
+generates a form UI. The user fills it out, the submission is
+validated, and an optional callback processes the result.
+
+Requires ``fastmcp[apps]`` (prefab-ui).
+
+Usage::
+
+ from pydantic import BaseModel
+ from fastmcp import FastMCP
+ from fastmcp.apps.form import FormInput
+
+ class ShippingAddress(BaseModel):
+ street: str
+ city: str
+ state: str
+ zip_code: str
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(FormInput(model=ShippingAddress))
+
+
+## Classes
+
+### `FormInput`
+
+
+A Provider that collects structured input via a Pydantic model.
+
+Define a model for the data you need, and ``FormInput`` generates
+a form from it using ``Form.from_model()``. Field types, labels,
+descriptions, and validation are all derived from the model.
+
+Optionally provide an ``on_submit`` callback to process the
+validated data. The callback receives a model instance and returns
+a string that goes back to the LLM. Without a callback, the
+validated JSON is sent directly.
+
+Example::
+
+ from pydantic import BaseModel
+ from fastmcp import FastMCP
+ from fastmcp.apps.form import FormInput
+
+ class Contact(BaseModel):
+ name: str
+ email: str
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(FormInput(model=Contact))
+
+With a callback::
+
+ def save_contact(contact: Contact) -> str:
+ db.insert(contact.model_dump())
+ return f"Saved {contact.name}"
+
+ mcp.add_provider(FormInput(model=Contact, on_submit=save_contact))
+
diff --git a/docs/python-sdk/fastmcp-apps-generative.mdx b/docs/python-sdk/fastmcp-apps-generative.mdx
new file mode 100644
index 000000000..bb5756b96
--- /dev/null
+++ b/docs/python-sdk/fastmcp-apps-generative.mdx
@@ -0,0 +1,56 @@
+---
+title: generative
+sidebarTitle: generative
+---
+
+# `fastmcp.apps.generative`
+
+
+GenerativeUI — a Provider that adds LLM-generated UI capabilities.
+
+Registers tools and resources from ``prefab_ui.generative`` so that an
+LLM can write Prefab Python code, execute it in a sandbox, and render
+the result as a streaming interactive UI.
+
+Requires ``fastmcp[apps]`` (prefab-ui).
+
+Usage::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.generative import GenerativeUI
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(GenerativeUI())
+
+
+## Classes
+
+### `GenerativeUI`
+
+
+A Provider that adds generative UI capabilities to a server.
+
+Registers:
+
+- A ``generate_ui`` tool that accepts Prefab Python code, executes
+ it in a Pyodide sandbox, and returns the rendered PrefabApp.
+ Supports streaming via ``ontoolinputpartial``.
+- A ``components`` tool that searches the Prefab component library.
+- The generative renderer resource with CSP for Pyodide CDN access.
+
+Example::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.generative import GenerativeUI
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(GenerativeUI())
+
+
+**Methods:**
+
+#### `lifespan`
+
+```python
+lifespan(self) -> AsyncIterator[None]
+```
diff --git a/docs/python-sdk/fastmcp-cli-auth.mdx b/docs/python-sdk/fastmcp-cli-auth.mdx
deleted file mode 100644
index 586a53505..000000000
--- a/docs/python-sdk/fastmcp-cli-auth.mdx
+++ /dev/null
@@ -1,9 +0,0 @@
----
-title: auth
-sidebarTitle: auth
----
-
-# `fastmcp.cli.auth`
-
-
-Authentication-related CLI commands.
diff --git a/docs/python-sdk/fastmcp-cli-cimd.mdx b/docs/python-sdk/fastmcp-cli-cimd.mdx
deleted file mode 100644
index 2b4b73457..000000000
--- a/docs/python-sdk/fastmcp-cli-cimd.mdx
+++ /dev/null
@@ -1,43 +0,0 @@
----
-title: cimd
-sidebarTitle: cimd
----
-
-# `fastmcp.cli.cimd`
-
-
-CIMD (Client ID Metadata Document) CLI commands.
-
-## Functions
-
-### `create_command`
-
-```python
-create_command() -> None
-```
-
-
-Generate a CIMD document for hosting.
-
-Create a Client ID Metadata Document that you can host at an HTTPS URL.
-The URL where you host this document becomes your client_id.
-
-After creating the document, host it at an HTTPS URL with a non-root path,
-for example: https://myapp.example.com/oauth/client.json
-
-
-### `validate_command`
-
-```python
-validate_command(url: Annotated[str, cyclopts.Parameter(help='URL of the CIMD document to validate')]) -> None
-```
-
-
-Validate a hosted CIMD document.
-
-Fetches the document from the given URL and validates:
-- URL is valid CIMD URL (HTTPS, non-root path)
-- Document is valid JSON
-- Document conforms to CIMD schema
-- client_id in document matches the URL
-
diff --git a/docs/python-sdk/fastmcp-cli-cli.mdx b/docs/python-sdk/fastmcp-cli-cli.mdx
deleted file mode 100644
index 60804a298..000000000
--- a/docs/python-sdk/fastmcp-cli-cli.mdx
+++ /dev/null
@@ -1,130 +0,0 @@
----
-title: cli
-sidebarTitle: cli
----
-
-# `fastmcp.cli.cli`
-
-
-FastMCP CLI tools using Cyclopts.
-
-## Functions
-
-### `with_argv`
-
-```python
-with_argv(args: list[str] | None)
-```
-
-
-Temporarily replace sys.argv if args provided.
-
-This context manager is used at the CLI boundary to inject
-server arguments when needed, without mutating sys.argv deep
-in the source loading logic.
-
-Args are provided without the script name, so we preserve sys.argv[0]
-and replace the rest.
-
-
-### `version`
-
-```python
-version()
-```
-
-
-Display version information and platform details.
-
-
-### `inspector`
-
-```python
-inspector(server_spec: str | None = None) -> None
-```
-
-
-Run an MCP server with the MCP Inspector for development.
-
-**Args:**
-- `server_spec`: Python file to run, optionally with \:object suffix, or None to auto-detect fastmcp.json
-
-
-### `run`
-
-```python
-run(server_spec: str | None = None, *server_args: str) -> None
-```
-
-
-Run an MCP server or connect to a remote one.
-
-The server can be specified in several ways:
-1. Module approach: "server.py" - runs the module directly, looking for an object named 'mcp', 'server', or 'app'
-2. Import approach: "server.py:app" - imports and runs the specified server object
-3. URL approach: "http://server-url" - connects to a remote server and creates a proxy
-4. MCPConfig file: "mcp.json" - runs as a proxy server for the MCP Servers in the MCPConfig file
-5. FastMCP config: "fastmcp.json" - runs server using FastMCP configuration
-6. No argument: looks for fastmcp.json in current directory
-7. Module mode: "-m my_module" - runs the module directly via python -m
-
-Server arguments can be passed after -- :
-fastmcp run server.py -- --config config.json --debug
-
-**Args:**
-- `server_spec`: Python file, object specification (file\:obj), config file, URL, or None to auto-detect
-
-
-### `inspect`
-
-```python
-inspect(server_spec: str | None = None) -> None
-```
-
-
-Inspect an MCP server and display information or generate a JSON report.
-
-This command analyzes an MCP server. Without flags, it displays a text summary.
-Use --format to output complete JSON data.
-
-**Examples:**
-
-# Show text summary
-fastmcp inspect server.py
-
-# Output FastMCP format JSON to stdout
-fastmcp inspect server.py --format fastmcp
-
-# Save MCP protocol format to file (format required with -o)
-fastmcp inspect server.py --format mcp -o manifest.json
-
-# Inspect from fastmcp.json configuration
-fastmcp inspect fastmcp.json
-fastmcp inspect # auto-detect fastmcp.json
-
-**Args:**
-- `server_spec`: Python file to inspect, optionally with \:object suffix, or fastmcp.json
-
-
-### `prepare`
-
-```python
-prepare(config_path: Annotated[str | None, cyclopts.Parameter(help='Path to fastmcp.json configuration file')] = None, output_dir: Annotated[str | None, cyclopts.Parameter(help='Directory to create the persistent environment in')] = None, skip_source: Annotated[bool, cyclopts.Parameter(help='Skip source preparation (e.g., git clone)')] = False) -> None
-```
-
-
-Prepare a FastMCP project by creating a persistent uv environment.
-
-This command creates a persistent uv project with all dependencies installed:
-- Creates a pyproject.toml with dependencies from the config
-- Installs all Python packages into a .venv
-- Prepares the source (git clone, download, etc.) unless --skip-source
-
-After running this command, you can use:
-fastmcp run <config> --project <output-dir>
-
-This is useful for:
-- CI/CD pipelines with separate build and run stages
-- Docker images where you prepare during build
-- Production deployments where you want fast startup times
-
diff --git a/docs/python-sdk/fastmcp-cli-client.mdx b/docs/python-sdk/fastmcp-cli-client.mdx
deleted file mode 100644
index 726663bfb..000000000
--- a/docs/python-sdk/fastmcp-cli-client.mdx
+++ /dev/null
@@ -1,135 +0,0 @@
----
-title: client
-sidebarTitle: client
----
-
-# `fastmcp.cli.client`
-
-
-Client-side CLI commands for querying and invoking MCP servers.
-
-## Functions
-
-### `resolve_server_spec`
-
-```python
-resolve_server_spec(server_spec: str | None) -> str | dict[str, Any] | ClientTransport
-```
-
-
-Turn CLI inputs into something ``Client()`` accepts.
-
-Exactly one of ``server_spec`` or ``command`` should be provided.
-
-Resolution order for ``server_spec``:
-1. URLs (``http://``, ``https://``) — passed through as-is.
- If ``--transport`` is ``sse``, the URL is rewritten to end with ``/sse``
- so ``infer_transport`` picks the right transport.
-2. Existing file paths, or strings ending in ``.py``/``.js``/``.json``.
-3. Anything else — name-based resolution via ``resolve_name``.
-
-When ``command`` is provided, the string is shell-split into a
-``StdioTransport(command, args)``.
-
-
-### `coerce_value`
-
-```python
-coerce_value(raw: str, schema: dict[str, Any]) -> Any
-```
-
-
-Coerce a string CLI value according to a JSON-Schema type hint.
-
-
-### `parse_tool_arguments`
-
-```python
-parse_tool_arguments(raw_args: tuple[str, ...], input_json: str | None, input_schema: dict[str, Any]) -> dict[str, Any]
-```
-
-
-Build a tool-call argument dict from CLI inputs.
-
-A single JSON object argument is treated as the full argument dict.
-``--input-json`` provides the base dict; ``key=value`` pairs override.
-Values are coerced using the tool's ``inputSchema``.
-
-
-### `format_tool_signature`
-
-```python
-format_tool_signature(tool: mcp.types.Tool) -> str
-```
-
-
-Build ``name(param: type, ...) -> return_type`` from a tool's JSON schemas.
-
-
-### `list_command`
-
-```python
-list_command(server_spec: Annotated[str | None, cyclopts.Parameter(help='Server URL, Python file, MCPConfig JSON, or .js file')] = None) -> None
-```
-
-
-List tools available on an MCP server.
-
-**Examples:**
-
-fastmcp list http://localhost:8000/mcp
-fastmcp list server.py
-fastmcp list mcp.json --json
-fastmcp list --command 'npx -y @mcp/server' --resources
-fastmcp list http://server/mcp --transport sse
-
-
-### `call_command`
-
-```python
-call_command(server_spec: Annotated[str | None, cyclopts.Parameter(help='Server URL, Python file, MCPConfig JSON, or .js file')] = None, target: Annotated[str, cyclopts.Parameter(help='Tool name, resource URI, or prompt name (with --prompt)')] = '', *arguments: str) -> None
-```
-
-
-Call a tool, read a resource, or get a prompt on an MCP server.
-
-By default the target is treated as a tool name. If the target
-contains ``://`` it is treated as a resource URI. Pass ``--prompt``
-to treat it as a prompt name.
-
-Arguments are passed as key=value pairs. Use --input-json for complex
-or nested arguments.
-
-**Examples:**
-
-```
-fastmcp call server.py greet name=World
-fastmcp call server.py resource://docs/readme
-fastmcp call server.py analyze --prompt data='[1,2,3]'
-fastmcp call http://server/mcp create --input-json '{"tags": ["a","b"]}'
-```
-
-
-### `discover_command`
-
-```python
-discover_command() -> None
-```
-
-
-Discover MCP servers configured in editor and project configs.
-
-Scans Claude Desktop, Claude Code, Cursor, Gemini CLI, Goose, and
-project-level mcp.json files for MCP server definitions.
-
-Discovered server names can be used directly with ``fastmcp list``
-and ``fastmcp call`` instead of specifying a URL or file path.
-
-**Examples:**
-
-fastmcp discover
-fastmcp discover --source claude-code
-fastmcp discover --source cursor --source gemini --json
-fastmcp list weather
-fastmcp call cursor:weather get_forecast city=London
-
diff --git a/docs/python-sdk/fastmcp-cli-discovery.mdx b/docs/python-sdk/fastmcp-cli-discovery.mdx
deleted file mode 100644
index 5892df9af..000000000
--- a/docs/python-sdk/fastmcp-cli-discovery.mdx
+++ /dev/null
@@ -1,71 +0,0 @@
----
-title: discovery
-sidebarTitle: discovery
----
-
-# `fastmcp.cli.discovery`
-
-
-Discover MCP servers configured in editor config files.
-
-Scans filesystem-readable config files from editors like Claude Desktop,
-Claude Code, Cursor, Gemini CLI, and Goose, as well as project-level
-``mcp.json`` files. Each discovered server can be resolved by name
-(or ``source:name``) so the CLI can connect without requiring a URL
-or file path.
-
-
-## Functions
-
-### `discover_servers`
-
-```python
-discover_servers(start_dir: Path | None = None) -> list[DiscoveredServer]
-```
-
-
-Run all scanners and return the combined results.
-
-Duplicate names across sources are preserved — callers can
-use :pyattr:`DiscoveredServer.qualified_name` to disambiguate.
-
-
-### `resolve_name`
-
-```python
-resolve_name(name: str, start_dir: Path | None = None) -> ClientTransport
-```
-
-
-Resolve a server name (or ``source:name``) to a transport.
-
-Raises :class:`ValueError` when the name is not found or is ambiguous.
-
-
-## Classes
-
-### `DiscoveredServer`
-
-
-A single MCP server found in an editor or project config.
-
-
-**Methods:**
-
-#### `qualified_name`
-
-```python
-qualified_name(self) -> str
-```
-
-Fully qualified ``source:name`` identifier.
-
-
-#### `transport_summary`
-
-```python
-transport_summary(self) -> str
-```
-
-Human-readable one-liner describing the transport.
-
diff --git a/docs/python-sdk/fastmcp-cli-generate.mdx b/docs/python-sdk/fastmcp-cli-generate.mdx
deleted file mode 100644
index ebedaa186..000000000
--- a/docs/python-sdk/fastmcp-cli-generate.mdx
+++ /dev/null
@@ -1,66 +0,0 @@
----
-title: generate
-sidebarTitle: generate
----
-
-# `fastmcp.cli.generate`
-
-
-Generate a standalone CLI script and agent skill from an MCP server.
-
-## Functions
-
-### `serialize_transport`
-
-```python
-serialize_transport(resolved: str | dict[str, Any] | ClientTransport) -> tuple[str, set[str]]
-```
-
-
-Serialize a resolved transport to a Python expression string.
-
-Returns ``(expression, extra_imports)`` where *extra_imports* is a set of
-import lines needed by the expression.
-
-
-### `generate_cli_script`
-
-```python
-generate_cli_script(server_name: str, server_spec: str, transport_code: str, extra_imports: set[str], tools: list[mcp.types.Tool]) -> str
-```
-
-
-Generate the full CLI script source code.
-
-
-### `generate_skill_content`
-
-```python
-generate_skill_content(server_name: str, cli_filename: str, tools: list[mcp.types.Tool]) -> str
-```
-
-
-Generate a SKILL.md file for a generated CLI script.
-
-
-### `generate_cli_command`
-
-```python
-generate_cli_command(server_spec: Annotated[str, cyclopts.Parameter(help='Server URL, Python file, MCPConfig JSON, discovered name, or .js file')], output: Annotated[str, cyclopts.Parameter(help='Output file path (default: cli.py)')] = 'cli.py') -> None
-```
-
-
-Generate a standalone CLI script from an MCP server.
-
-Connects to the server, reads its tools/resources/prompts, and writes
-a Python script that can invoke them directly. Also generates a SKILL.md
-agent skill file unless --no-skill is passed.
-
-**Examples:**
-
-fastmcp generate-cli weather
-fastmcp generate-cli weather my_cli.py
-fastmcp generate-cli http://localhost:8000/mcp
-fastmcp generate-cli server.py output.py -f
-fastmcp generate-cli weather --no-skill
-
diff --git a/docs/python-sdk/fastmcp-cli-install-__init__.mdx b/docs/python-sdk/fastmcp-cli-install-__init__.mdx
deleted file mode 100644
index 3909565f2..000000000
--- a/docs/python-sdk/fastmcp-cli-install-__init__.mdx
+++ /dev/null
@@ -1,9 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.cli.install`
-
-
-Install subcommands for FastMCP CLI using Cyclopts.
diff --git a/docs/python-sdk/fastmcp-cli-install-claude_code.mdx b/docs/python-sdk/fastmcp-cli-install-claude_code.mdx
deleted file mode 100644
index 0a3393077..000000000
--- a/docs/python-sdk/fastmcp-cli-install-claude_code.mdx
+++ /dev/null
@@ -1,71 +0,0 @@
----
-title: claude_code
-sidebarTitle: claude_code
----
-
-# `fastmcp.cli.install.claude_code`
-
-
-Claude Code integration for FastMCP install using Cyclopts.
-
-## Functions
-
-### `find_claude_command`
-
-```python
-find_claude_command() -> str | None
-```
-
-
-Find the Claude Code CLI command.
-
-Checks common installation locations since 'claude' is often a shell alias
-that doesn't work with subprocess calls.
-
-
-### `check_claude_code_available`
-
-```python
-check_claude_code_available() -> bool
-```
-
-
-Check if Claude Code CLI is available.
-
-
-### `install_claude_code`
-
-```python
-install_claude_code(file: Path, server_object: str | None, name: str) -> bool
-```
-
-
-Install FastMCP server in Claude Code.
-
-**Args:**
-- `file`: Path to the server file
-- `server_object`: Optional server object name (for \:object suffix)
-- `name`: Name for the server in Claude Code
-- `with_editable`: Optional list of directories to install in editable mode
-- `with_packages`: Optional list of additional packages to install
-- `env_vars`: Optional dictionary of environment variables
-- `python_version`: Optional Python version to use
-- `with_requirements`: Optional requirements file to install from
-- `project`: Optional project directory to run within
-
-**Returns:**
-- True if installation was successful, False otherwise
-
-
-### `claude_code_command`
-
-```python
-claude_code_command(server_spec: str) -> None
-```
-
-
-Install an MCP server in Claude Code.
-
-**Args:**
-- `server_spec`: Python file to install, optionally with \:object suffix
-
diff --git a/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx b/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx
deleted file mode 100644
index 23f7a1b27..000000000
--- a/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx
+++ /dev/null
@@ -1,58 +0,0 @@
----
-title: claude_desktop
-sidebarTitle: claude_desktop
----
-
-# `fastmcp.cli.install.claude_desktop`
-
-
-Claude Desktop integration for FastMCP install using Cyclopts.
-
-## Functions
-
-### `get_claude_config_path`
-
-```python
-get_claude_config_path() -> Path | None
-```
-
-
-Get the Claude config directory based on platform.
-
-
-### `install_claude_desktop`
-
-```python
-install_claude_desktop(file: Path, server_object: str | None, name: str) -> bool
-```
-
-
-Install FastMCP server in Claude Desktop.
-
-**Args:**
-- `file`: Path to the server file
-- `server_object`: Optional server object name (for \:object suffix)
-- `name`: Name for the server in Claude's config
-- `with_editable`: Optional list of directories to install in editable mode
-- `with_packages`: Optional list of additional packages to install
-- `env_vars`: Optional dictionary of environment variables
-- `python_version`: Optional Python version to use
-- `with_requirements`: Optional requirements file to install from
-- `project`: Optional project directory to run within
-
-**Returns:**
-- True if installation was successful, False otherwise
-
-
-### `claude_desktop_command`
-
-```python
-claude_desktop_command(server_spec: str) -> None
-```
-
-
-Install an MCP server in Claude Desktop.
-
-**Args:**
-- `server_spec`: Python file to install, optionally with \:object suffix
-
diff --git a/docs/python-sdk/fastmcp-cli-install-cursor.mdx b/docs/python-sdk/fastmcp-cli-install-cursor.mdx
deleted file mode 100644
index a61bca0ff..000000000
--- a/docs/python-sdk/fastmcp-cli-install-cursor.mdx
+++ /dev/null
@@ -1,107 +0,0 @@
----
-title: cursor
-sidebarTitle: cursor
----
-
-# `fastmcp.cli.install.cursor`
-
-
-Cursor integration for FastMCP install using Cyclopts.
-
-## Functions
-
-### `generate_cursor_deeplink`
-
-```python
-generate_cursor_deeplink(server_name: str, server_config: StdioMCPServer) -> str
-```
-
-
-Generate a Cursor deeplink for installing the MCP server.
-
-**Args:**
-- `server_name`: Name of the server
-- `server_config`: Server configuration
-
-**Returns:**
-- Deeplink URL that can be clicked to install the server
-
-
-### `open_deeplink`
-
-```python
-open_deeplink(deeplink: str) -> bool
-```
-
-
-Attempt to open a Cursor deeplink URL using the system's default handler.
-
-**Args:**
-- `deeplink`: The deeplink URL to open
-
-**Returns:**
-- True if the command succeeded, False otherwise
-
-
-### `install_cursor_workspace`
-
-```python
-install_cursor_workspace(file: Path, server_object: str | None, name: str, workspace_path: Path) -> bool
-```
-
-
-Install FastMCP server to workspace-specific Cursor configuration.
-
-**Args:**
-- `file`: Path to the server file
-- `server_object`: Optional server object name (for \:object suffix)
-- `name`: Name for the server in Cursor
-- `workspace_path`: Path to the workspace directory
-- `with_editable`: Optional list of directories to install in editable mode
-- `with_packages`: Optional list of additional packages to install
-- `env_vars`: Optional dictionary of environment variables
-- `python_version`: Optional Python version to use
-- `with_requirements`: Optional requirements file to install from
-- `project`: Optional project directory to run within
-
-**Returns:**
-- True if installation was successful, False otherwise
-
-
-### `install_cursor`
-
-```python
-install_cursor(file: Path, server_object: str | None, name: str) -> bool
-```
-
-
-Install FastMCP server in Cursor.
-
-**Args:**
-- `file`: Path to the server file
-- `server_object`: Optional server object name (for \:object suffix)
-- `name`: Name for the server in Cursor
-- `with_editable`: Optional list of directories to install in editable mode
-- `with_packages`: Optional list of additional packages to install
-- `env_vars`: Optional dictionary of environment variables
-- `python_version`: Optional Python version to use
-- `with_requirements`: Optional requirements file to install from
-- `project`: Optional project directory to run within
-- `workspace`: Optional workspace directory for project-specific installation
-
-**Returns:**
-- True if installation was successful, False otherwise
-
-
-### `cursor_command`
-
-```python
-cursor_command(server_spec: str) -> None
-```
-
-
-Install an MCP server in Cursor.
-
-**Args:**
-- `server_spec`: Python file to install, optionally with \:object suffix
-
diff --git a/docs/python-sdk/fastmcp-cli-install-gemini_cli.mdx b/docs/python-sdk/fastmcp-cli-install-gemini_cli.mdx
deleted file mode 100644
index 9cb51f0f4..000000000
--- a/docs/python-sdk/fastmcp-cli-install-gemini_cli.mdx
+++ /dev/null
@@ -1,68 +0,0 @@
----
-title: gemini_cli
-sidebarTitle: gemini_cli
----
-
-# `fastmcp.cli.install.gemini_cli`
-
-
-Gemini CLI integration for FastMCP install using Cyclopts.
-
-## Functions
-
-### `find_gemini_command`
-
-```python
-find_gemini_command() -> str | None
-```
-
-
-Find the Gemini CLI command.
-
-
-### `check_gemini_cli_available`
-
-```python
-check_gemini_cli_available() -> bool
-```
-
-
-Check if Gemini CLI is available.
-
-
-### `install_gemini_cli`
-
-```python
-install_gemini_cli(file: Path, server_object: str | None, name: str) -> bool
-```
-
-
-Install FastMCP server in Gemini CLI.
-
-**Args:**
-- `file`: Path to the server file
-- `server_object`: Optional server object name (for \:object suffix)
-- `name`: Name for the server in Gemini CLI
-- `with_editable`: Optional list of directories to install in editable mode
-- `with_packages`: Optional list of additional packages to install
-- `env_vars`: Optional dictionary of environment variables
-- `python_version`: Optional Python version to use
-- `with_requirements`: Optional requirements file to install from
-- `project`: Optional project directory to run within
-
-**Returns:**
-- True if installation was successful, False otherwise
-
-
-### `gemini_cli_command`
-
-```python
-gemini_cli_command(server_spec: str) -> None
-```
-
-
-Install an MCP server in Gemini CLI.
-
-**Args:**
-- `server_spec`: Python file to install, optionally with \:object suffix
-
diff --git a/docs/python-sdk/fastmcp-cli-install-goose.mdx b/docs/python-sdk/fastmcp-cli-install-goose.mdx
deleted file mode 100644
index cd2a8cc9a..000000000
--- a/docs/python-sdk/fastmcp-cli-install-goose.mdx
+++ /dev/null
@@ -1,67 +0,0 @@
----
-title: goose
-sidebarTitle: goose
----
-
-# `fastmcp.cli.install.goose`
-
-
-Goose integration for FastMCP install using Cyclopts.
-
-## Functions
-
-### `generate_goose_deeplink`
-
-```python
-generate_goose_deeplink(name: str, command: str, args: list[str]) -> str
-```
-
-
-Generate a Goose deeplink for installing an MCP extension.
-
-**Args:**
-- `name`: Human-readable display name for the extension.
-- `command`: The executable command (e.g. "uv").
-- `args`: Arguments to the command.
-- `description`: Short description shown in Goose.
-
-**Returns:**
-- A goose://extension?... deeplink URL.
-
-
-### `install_goose`
-
-```python
-install_goose(file: Path, server_object: str | None, name: str) -> bool
-```
-
-
-Install FastMCP server in Goose via deeplink.
-
-**Args:**
-- `file`: Path to the server file.
-- `server_object`: Optional server object name (for \:object suffix).
-- `name`: Name for the extension in Goose.
-- `with_packages`: Optional list of additional packages to install.
-- `python_version`: Optional Python version to use.
-
-**Returns:**
-- True if installation was successful, False otherwise.
-
-
-### `goose_command`
-
-```python
-goose_command(server_spec: str) -> None
-```
-
-
-Install an MCP server in Goose.
-
-Uses uvx to run the server. Environment variables are not included
-in the deeplink; use `fastmcp install mcp-json` to generate a full
-config for manual installation.
-
-**Args:**
-- `server_spec`: Python file to install, optionally with \:object suffix
-
diff --git a/docs/python-sdk/fastmcp-cli-install-mcp_json.mdx b/docs/python-sdk/fastmcp-cli-install-mcp_json.mdx
deleted file mode 100644
index a9eca1e51..000000000
--- a/docs/python-sdk/fastmcp-cli-install-mcp_json.mdx
+++ /dev/null
@@ -1,49 +0,0 @@
----
-title: mcp_json
-sidebarTitle: mcp_json
----
-
-# `fastmcp.cli.install.mcp_json`
-
-
-MCP configuration JSON generation for FastMCP install using Cyclopts.
-
-## Functions
-
-### `install_mcp_json`
-
-```python
-install_mcp_json(file: Path, server_object: str | None, name: str) -> bool
-```
-
-
-Generate MCP configuration JSON for manual installation.
-
-**Args:**
-- `file`: Path to the server file
-- `server_object`: Optional server object name (for \:object suffix)
-- `name`: Name for the server in MCP config
-- `with_editable`: Optional list of directories to install in editable mode
-- `with_packages`: Optional list of additional packages to install
-- `env_vars`: Optional dictionary of environment variables
-- `copy`: If True, copy to clipboard instead of printing to stdout
-- `python_version`: Optional Python version to use
-- `with_requirements`: Optional requirements file to install from
-- `project`: Optional project directory to run within
-
-**Returns:**
-- True if generation was successful, False otherwise
-
-
-### `mcp_json_command`
-
-```python
-mcp_json_command(server_spec: str) -> None
-```
-
-
-Generate MCP configuration JSON for manual installation.
-
-**Args:**
-- `server_spec`: Python file to install, optionally with \:object suffix
-
diff --git a/docs/python-sdk/fastmcp-cli-install-shared.mdx b/docs/python-sdk/fastmcp-cli-install-shared.mdx
deleted file mode 100644
index a1fe2119c..000000000
--- a/docs/python-sdk/fastmcp-cli-install-shared.mdx
+++ /dev/null
@@ -1,50 +0,0 @@
----
-title: shared
-sidebarTitle: shared
----
-
-# `fastmcp.cli.install.shared`
-
-
-Shared utilities for install commands.
-
-## Functions
-
-### `parse_env_var`
-
-```python
-parse_env_var(env_var: str) -> tuple[str, str]
-```
-
-
-Parse environment variable string in format KEY=VALUE.
-
-
-### `process_common_args`
-
-```python
-process_common_args(server_spec: str, server_name: str | None, with_packages: list[str] | None, env_vars: list[str] | None, env_file: Path | None) -> tuple[Path, str | None, str, list[str], dict[str, str] | None]
-```
-
-
-Process common arguments shared by all install commands.
-
-Handles both fastmcp.json config files and traditional file.py:object syntax.
-
-
-### `open_deeplink`
-
-```python
-open_deeplink(url: str) -> bool
-```
-
-
-Attempt to open a deeplink URL using the system's default handler.
-
-**Args:**
-- `url`: The deeplink URL to open.
-- `expected_scheme`: The URL scheme to validate (e.g. "cursor", "goose").
-
-**Returns:**
-- True if the command succeeded, False otherwise.
-
diff --git a/docs/python-sdk/fastmcp-cli-install-stdio.mdx b/docs/python-sdk/fastmcp-cli-install-stdio.mdx
deleted file mode 100644
index 62bdc8fde..000000000
--- a/docs/python-sdk/fastmcp-cli-install-stdio.mdx
+++ /dev/null
@@ -1,50 +0,0 @@
----
-title: stdio
-sidebarTitle: stdio
----
-
-# `fastmcp.cli.install.stdio`
-
-
-Stdio command generation for FastMCP install using Cyclopts.
-
-## Functions
-
-### `install_stdio`
-
-```python
-install_stdio(file: Path, server_object: str | None) -> bool
-```
-
-
-Generate the stdio command for running a FastMCP server.
-
-**Args:**
-- `file`: Path to the server file
-- `server_object`: Optional server object name (for \:object suffix)
-- `with_editable`: Optional list of directories to install in editable mode
-- `with_packages`: Optional list of additional packages to install
-- `copy`: If True, copy to clipboard instead of printing to stdout
-- `python_version`: Optional Python version to use
-- `with_requirements`: Optional requirements file to install from
-- `project`: Optional project directory to run within
-
-**Returns:**
-- True if generation was successful, False otherwise
-
-
-### `stdio_command`
-
-```python
-stdio_command(server_spec: str) -> None
-```
-
-
-Generate the stdio command for running a FastMCP server.
-
-Outputs the shell command that an MCP host would use to start this server
-over stdio transport. Useful for manual configuration or debugging.
-
-**Args:**
-- `server_spec`: Python file to run, optionally with \:object suffix
-
diff --git a/docs/python-sdk/fastmcp-cli-run.mdx b/docs/python-sdk/fastmcp-cli-run.mdx
deleted file mode 100644
index 0b341c459..000000000
--- a/docs/python-sdk/fastmcp-cli-run.mdx
+++ /dev/null
@@ -1,136 +0,0 @@
----
-title: run
-sidebarTitle: run
----
-
-# `fastmcp.cli.run`
-
-
-FastMCP run command implementation with enhanced type hints.
-
-## Functions
-
-### `is_url`
-
-```python
-is_url(path: str) -> bool
-```
-
-
-Check if a string is a URL.
-
-
-### `create_client_server`
-
-```python
-create_client_server(url: str) -> Any
-```
-
-
-Create a FastMCP server from a client URL.
-
-**Args:**
-- `url`: The URL to connect to
-
-**Returns:**
-- A FastMCP server instance
-
-
-### `create_mcp_config_server`
-
-```python
-create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]
-```
-
-
-Create a FastMCP server from a MCPConfig.
-
-
-### `load_mcp_server_config`
-
-```python
-load_mcp_server_config(config_path: Path) -> MCPServerConfig
-```
-
-
-Load a FastMCP configuration from a fastmcp.json file.
-
-**Args:**
-- `config_path`: Path to fastmcp.json file
-
-**Returns:**
-- MCPServerConfig object
-
-
-### `run_command`
-
-```python
-run_command(server_spec: str, transport: TransportType | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: LogLevelType | None = None, server_args: list[str] | None = None, show_banner: bool = True, use_direct_import: bool = False, skip_source: bool = False, stateless: bool = False) -> None
-```
-
-
-Run a MCP server or connect to a remote one.
-
-**Args:**
-- `server_spec`: Python file, object specification (file\:obj), config file, or URL
-- `transport`: Transport protocol to use
-- `host`: Host to bind to when using http transport
-- `port`: Port to bind to when using http transport
-- `path`: Path to bind to when using http transport
-- `log_level`: Log level
-- `server_args`: Additional arguments to pass to the server
-- `show_banner`: Whether to show the server banner
-- `use_direct_import`: Whether to use direct import instead of subprocess
-- `skip_source`: Whether to skip source preparation step
-- `stateless`: Whether to run in stateless mode (no session)
-
-
-### `run_module_command`
-
-```python
-run_module_command(module_name: str) -> None
-```
-
-
-Run a Python module directly using ``python -m ``.
-
-When ``-m`` is used, the module manages its own server startup.
-No server-object discovery or transport overrides are applied.
-
-**Args:**
-- `module_name`: Dotted module name (e.g. ``my_package``).
-- `env_command_builder`: An optional callable that wraps a command list
-with environment setup (e.g. ``UVEnvironment.build_command``).
-- `extra_args`: Extra arguments forwarded after the module name.
-
-
-### `run_v1_server_async`
-
-```python
-run_v1_server_async(server: FastMCP1x, host: str | None = None, port: int | None = None, transport: TransportType | None = None) -> None
-```
-
-
-Run a FastMCP 1.x server using async methods.
-
-**Args:**
-- `server`: FastMCP 1.x server instance
-- `host`: Host to bind to
-- `port`: Port to bind to
-- `transport`: Transport protocol to use
-
-
-### `run_with_reload`
-
-```python
-run_with_reload(cmd: list[str], reload_dirs: list[Path] | None = None, is_stdio: bool = False) -> None
-```
-
-
-Run a command with file watching and auto-reload.
-
-**Args:**
-- `cmd`: Command to run as subprocess (should include --no-reload)
-- `reload_dirs`: Directories to watch for changes (default\: cwd)
-- `is_stdio`: Whether this is stdio transport
-
diff --git a/docs/python-sdk/fastmcp-cli-tasks.mdx b/docs/python-sdk/fastmcp-cli-tasks.mdx
deleted file mode 100644
index 99f5dac1f..000000000
--- a/docs/python-sdk/fastmcp-cli-tasks.mdx
+++ /dev/null
@@ -1,41 +0,0 @@
----
-title: tasks
-sidebarTitle: tasks
----
-
-# `fastmcp.cli.tasks`
-
-
-FastMCP tasks CLI for Docket task management.
-
-## Functions
-
-### `check_distributed_backend`
-
-```python
-check_distributed_backend() -> None
-```
-
-
-Check if Docket is configured with a distributed backend.
-
-The CLI worker runs as a separate process, so it needs Redis/Valkey
-to coordinate with the main server process.
-
-**Raises:**
-- `SystemExit`: If using memory\:// URL
-
-
-### `worker`
-
-```python
-worker(server_spec: Annotated[str | None, cyclopts.Parameter(help='Python file to run, optionally with :object suffix, or None to auto-detect fastmcp.json')] = None) -> None
-```
-
-
-Start an additional worker to process background tasks.
-
-Connects to your Docket backend and processes tasks in parallel with
-any other running workers. Configure via environment variables
-(FASTMCP_DOCKET_*).
-
diff --git a/docs/python-sdk/fastmcp-cli-__init__.mdx b/docs/python-sdk/fastmcp-cli.mdx
similarity index 55%
rename from docs/python-sdk/fastmcp-cli-__init__.mdx
rename to docs/python-sdk/fastmcp-cli.mdx
index d2873740a..4cc13272a 100644
--- a/docs/python-sdk/fastmcp-cli-__init__.mdx
+++ b/docs/python-sdk/fastmcp-cli.mdx
@@ -1,6 +1,6 @@
---
-title: __init__
-sidebarTitle: __init__
+title: cli
+sidebarTitle: cli
---
# `fastmcp.cli`
diff --git a/docs/python-sdk/fastmcp-client-__init__.mdx b/docs/python-sdk/fastmcp-client-__init__.mdx
deleted file mode 100644
index bc145d4b7..000000000
--- a/docs/python-sdk/fastmcp-client-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.client`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-client-auth-__init__.mdx b/docs/python-sdk/fastmcp-client-auth-__init__.mdx
deleted file mode 100644
index 28242780d..000000000
--- a/docs/python-sdk/fastmcp-client-auth-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.client.auth`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-client-auth-bearer.mdx b/docs/python-sdk/fastmcp-client-auth-bearer.mdx
deleted file mode 100644
index a6a53a3a6..000000000
--- a/docs/python-sdk/fastmcp-client-auth-bearer.mdx
+++ /dev/null
@@ -1,18 +0,0 @@
----
-title: bearer
-sidebarTitle: bearer
----
-
-# `fastmcp.client.auth.bearer`
-
-## Classes
-
-### `BearerAuth`
-
-**Methods:**
-
-#### `auth_flow`
-
-```python
-auth_flow(self, request)
-```
diff --git a/docs/python-sdk/fastmcp-client-auth-oauth.mdx b/docs/python-sdk/fastmcp-client-auth-oauth.mdx
deleted file mode 100644
index 455ea1337..000000000
--- a/docs/python-sdk/fastmcp-client-auth-oauth.mdx
+++ /dev/null
@@ -1,104 +0,0 @@
----
-title: oauth
-sidebarTitle: oauth
----
-
-# `fastmcp.client.auth.oauth`
-
-## Functions
-
-### `check_if_auth_required`
-
-```python
-check_if_auth_required(mcp_url: str, httpx_kwargs: dict[str, Any] | None = None) -> bool
-```
-
-
-Check if the MCP endpoint requires authentication by making a test request.
-
-**Returns:**
-- True if auth appears to be required, False otherwise
-
-
-## Classes
-
-### `ClientNotFoundError`
-
-
-Raised when OAuth client credentials are not found on the server.
-
-
-### `TokenStorageAdapter`
-
-**Methods:**
-
-#### `clear`
-
-```python
-clear(self) -> None
-```
-
-#### `get_tokens`
-
-```python
-get_tokens(self) -> OAuthToken | None
-```
-
-#### `set_tokens`
-
-```python
-set_tokens(self, tokens: OAuthToken) -> None
-```
-
-#### `get_client_info`
-
-```python
-get_client_info(self) -> OAuthClientInformationFull | None
-```
-
-#### `set_client_info`
-
-```python
-set_client_info(self, client_info: OAuthClientInformationFull) -> None
-```
-
-### `OAuth`
-
-
-OAuth client provider for MCP servers with browser-based authentication.
-
-This class provides OAuth authentication for FastMCP clients by opening
-a browser for user authorization and running a local callback server.
-
-
-**Methods:**
-
-#### `redirect_handler`
-
-```python
-redirect_handler(self, authorization_url: str) -> None
-```
-
-Open browser for authorization, with pre-flight check for invalid client.
-
-
-#### `callback_handler`
-
-```python
-callback_handler(self) -> tuple[str, str | None]
-```
-
-Handle OAuth callback and return (auth_code, state).
-
-
-#### `async_auth_flow`
-
-```python
-async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]
-```
-
-HTTPX auth flow with automatic retry on stale cached credentials.
-
-If the OAuth flow fails due to invalid/stale client credentials,
-clears the cache and retries once with fresh registration.
-
diff --git a/docs/python-sdk/fastmcp-client-client.mdx b/docs/python-sdk/fastmcp-client-client.mdx
deleted file mode 100644
index 6c3ac689a..000000000
--- a/docs/python-sdk/fastmcp-client-client.mdx
+++ /dev/null
@@ -1,286 +0,0 @@
----
-title: client
-sidebarTitle: client
----
-
-# `fastmcp.client.client`
-
-## Classes
-
-### `ClientSessionState`
-
-
-Holds all session-related state for a Client instance.
-
-This allows clean separation of configuration (which is copied) from
-session state (which should be fresh for each new client instance).
-
-
-### `CallToolResult`
-
-
-Parsed result from a tool call.
-
-
-### `Client`
-
-
-MCP client that delegates connection management to a Transport instance.
-
-The Client class is responsible for MCP protocol logic, while the Transport
-handles connection establishment and management. Client provides methods for
-working with resources, prompts, tools and other MCP capabilities.
-
-This client supports reentrant context managers (multiple concurrent
-`async with client:` blocks) using reference counting and background session
-management. This allows efficient session reuse in any scenario with
-nested or concurrent client usage.
-
-MCP SDK 1.10 introduced automatic list_tools() calls during call_tool()
-execution. This created a race condition where events could be reset while
-other tasks were waiting on them, causing deadlocks. The issue was exposed
-in proxy scenarios but affects any reentrant usage.
-
-The solution uses reference counting to track active context managers,
-a background task to manage the session lifecycle, events to coordinate
-between tasks, and ensures all session state changes happen within a lock.
-Events are only created when needed, never reset outside locks.
-
-This design prevents race conditions where tasks wait on events that get
-replaced by other tasks, ensuring reliable coordination in concurrent scenarios.
-
-**Args:**
-- `transport`:
-Connection source specification, which can be\:
-
- - ClientTransport\: Direct transport instance
- - FastMCP\: In-process FastMCP server
- - AnyUrl or str\: URL to connect to
- - Path\: File path for local socket
- - MCPConfig\: MCP server configuration
- - dict\: Transport configuration
-- `roots`: Optional RootsList or RootsHandler for filesystem access
-- `sampling_handler`: Optional handler for sampling requests
-- `log_handler`: Optional handler for log messages
-- `message_handler`: Optional handler for protocol messages
-- `progress_handler`: Optional handler for progress notifications
-- `timeout`: Optional timeout for requests (seconds or timedelta)
-- `init_timeout`: Optional timeout for initial connection (seconds or timedelta).
-Set to 0 to disable. If None, uses the value in the FastMCP global settings.
-
-**Examples:**
-
-```python
-# Connect to FastMCP server
-client = Client("http://localhost:8080")
-
-async with client:
- # List available resources
- resources = await client.list_resources()
-
- # Call a tool
- result = await client.call_tool("my_tool", {"param": "value"})
-```
-
-
-**Methods:**
-
-#### `session`
-
-```python
-session(self) -> ClientSession
-```
-
-Get the current active session. Raises RuntimeError if not connected.
-
-
-#### `initialize_result`
-
-```python
-initialize_result(self) -> mcp.types.InitializeResult | None
-```
-
-Get the result of the initialization request.
-
-
-#### `set_roots`
-
-```python
-set_roots(self, roots: RootsList | RootsHandler) -> None
-```
-
-Set the roots for the client. This does not automatically call `send_roots_list_changed`.
-
-
-#### `set_sampling_callback`
-
-```python
-set_sampling_callback(self, sampling_callback: SamplingHandler, sampling_capabilities: mcp.types.SamplingCapability | None = None) -> None
-```
-
-Set the sampling callback for the client.
-
-
-#### `set_elicitation_callback`
-
-```python
-set_elicitation_callback(self, elicitation_callback: ElicitationHandler) -> None
-```
-
-Set the elicitation callback for the client.
-
-
-#### `is_connected`
-
-```python
-is_connected(self) -> bool
-```
-
-Check if the client is currently connected.
-
-
-#### `new`
-
-```python
-new(self) -> Client[ClientTransportT]
-```
-
-Create a new client instance with the same configuration but fresh session state.
-
-This creates a new client with the same transport, handlers, and configuration,
-but with no active session. Useful for creating independent sessions that don't
-share state with the original client.
-
-**Returns:**
-- A new Client instance with the same configuration but disconnected state.
-
-
-#### `initialize`
-
-```python
-initialize(self, timeout: datetime.timedelta | float | int | None = None) -> mcp.types.InitializeResult
-```
-
-Send an initialize request to the server.
-
-This method performs the MCP initialization handshake with the server,
-exchanging capabilities and server information. It is idempotent - calling
-it multiple times returns the cached result from the first call.
-
-The initialization happens automatically when entering the client context
-manager unless `auto_initialize=False` was set during client construction.
-Manual calls to this method are only needed when auto-initialization is disabled.
-
-**Args:**
-- `timeout`: Optional timeout for the initialization request (seconds or timedelta).
-If None, uses the client's init_timeout setting.
-
-**Returns:**
-- The server's initialization response containing server info,
-capabilities, protocol version, and optional instructions.
-
-**Raises:**
-- `RuntimeError`: If the client is not connected or initialization times out.
-
-
-#### `close`
-
-```python
-close(self)
-```
-
-#### `ping`
-
-```python
-ping(self) -> bool
-```
-
-Send a ping request.
-
-
-#### `cancel`
-
-```python
-cancel(self, request_id: str | int, reason: str | None = None) -> None
-```
-
-Send a cancellation notification for an in-progress request.
-
-
-#### `progress`
-
-```python
-progress(self, progress_token: str | int, progress: float, total: float | None = None, message: str | None = None) -> None
-```
-
-Send a progress notification.
-
-
-#### `set_logging_level`
-
-```python
-set_logging_level(self, level: mcp.types.LoggingLevel) -> None
-```
-
-Send a logging/setLevel request.
-
-
-#### `send_roots_list_changed`
-
-```python
-send_roots_list_changed(self) -> None
-```
-
-Send a roots/list_changed notification.
-
-
-#### `complete_mcp`
-
-```python
-complete_mcp(self, ref: mcp.types.ResourceTemplateReference | mcp.types.PromptReference, argument: dict[str, str], context_arguments: dict[str, Any] | None = None) -> mcp.types.CompleteResult
-```
-
-Send a completion request and return the complete MCP protocol result.
-
-**Args:**
-- `ref`: The reference to complete.
-- `argument`: Arguments to pass to the completion request.
-- `context_arguments`: Optional context arguments to
-include with the completion request. Defaults to None.
-
-**Returns:**
-- mcp.types.CompleteResult: The complete response object from the protocol,
-containing the completion and any additional metadata.
-
-**Raises:**
-- `RuntimeError`: If called while the client is not connected.
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
-
-#### `complete`
-
-```python
-complete(self, ref: mcp.types.ResourceTemplateReference | mcp.types.PromptReference, argument: dict[str, str], context_arguments: dict[str, Any] | None = None) -> mcp.types.Completion
-```
-
-Send a completion request to the server.
-
-**Args:**
-- `ref`: The reference to complete.
-- `argument`: Arguments to pass to the completion request.
-- `context_arguments`: Optional context arguments to
-include with the completion request. Defaults to None.
-
-**Returns:**
-- mcp.types.Completion: The completion object.
-
-**Raises:**
-- `RuntimeError`: If called while the client is not connected.
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
-
-#### `generate_name`
-
-```python
-generate_name(cls, name: str | None = None) -> str
-```
diff --git a/docs/python-sdk/fastmcp-client-elicitation.mdx b/docs/python-sdk/fastmcp-client-elicitation.mdx
deleted file mode 100644
index c9db14702..000000000
--- a/docs/python-sdk/fastmcp-client-elicitation.mdx
+++ /dev/null
@@ -1,18 +0,0 @@
----
-title: elicitation
-sidebarTitle: elicitation
----
-
-# `fastmcp.client.elicitation`
-
-## Functions
-
-### `create_elicitation_callback`
-
-```python
-create_elicitation_callback(elicitation_handler: ElicitationHandler) -> ElicitationFnT
-```
-
-## Classes
-
-### `ElicitResult`
diff --git a/docs/python-sdk/fastmcp-client-logging.mdx b/docs/python-sdk/fastmcp-client-logging.mdx
deleted file mode 100644
index b9453a0be..000000000
--- a/docs/python-sdk/fastmcp-client-logging.mdx
+++ /dev/null
@@ -1,24 +0,0 @@
----
-title: logging
-sidebarTitle: logging
----
-
-# `fastmcp.client.logging`
-
-## Functions
-
-### `default_log_handler`
-
-```python
-default_log_handler(message: LogMessage) -> None
-```
-
-
-Default handler that properly routes server log messages to appropriate log levels.
-
-
-### `create_log_callback`
-
-```python
-create_log_callback(handler: LogHandler | None = None) -> LoggingFnT
-```
diff --git a/docs/python-sdk/fastmcp-client-messages.mdx b/docs/python-sdk/fastmcp-client-messages.mdx
deleted file mode 100644
index fde8f0cde..000000000
--- a/docs/python-sdk/fastmcp-client-messages.mdx
+++ /dev/null
@@ -1,107 +0,0 @@
----
-title: messages
-sidebarTitle: messages
----
-
-# `fastmcp.client.messages`
-
-## Classes
-
-### `MessageHandler`
-
-
-This class is used to handle MCP messages sent to the client. It is used to handle all messages,
-requests, notifications, and exceptions. Users can override any of the hooks
-
-
-**Methods:**
-
-#### `dispatch`
-
-```python
-dispatch(self, message: Message) -> None
-```
-
-#### `on_message`
-
-```python
-on_message(self, message: Message) -> None
-```
-
-#### `on_request`
-
-```python
-on_request(self, message: RequestResponder[mcp.types.ServerRequest, mcp.types.ClientResult]) -> None
-```
-
-#### `on_ping`
-
-```python
-on_ping(self, message: mcp.types.PingRequest) -> None
-```
-
-#### `on_list_roots`
-
-```python
-on_list_roots(self, message: mcp.types.ListRootsRequest) -> None
-```
-
-#### `on_create_message`
-
-```python
-on_create_message(self, message: mcp.types.CreateMessageRequest) -> None
-```
-
-#### `on_notification`
-
-```python
-on_notification(self, message: mcp.types.ServerNotification) -> None
-```
-
-#### `on_exception`
-
-```python
-on_exception(self, message: Exception) -> None
-```
-
-#### `on_progress`
-
-```python
-on_progress(self, message: mcp.types.ProgressNotification) -> None
-```
-
-#### `on_logging_message`
-
-```python
-on_logging_message(self, message: mcp.types.LoggingMessageNotification) -> None
-```
-
-#### `on_tool_list_changed`
-
-```python
-on_tool_list_changed(self, message: mcp.types.ToolListChangedNotification) -> None
-```
-
-#### `on_resource_list_changed`
-
-```python
-on_resource_list_changed(self, message: mcp.types.ResourceListChangedNotification) -> None
-```
-
-#### `on_prompt_list_changed`
-
-```python
-on_prompt_list_changed(self, message: mcp.types.PromptListChangedNotification) -> None
-```
-
-#### `on_resource_updated`
-
-```python
-on_resource_updated(self, message: mcp.types.ResourceUpdatedNotification) -> None
-```
-
-#### `on_cancelled`
-
-```python
-on_cancelled(self, message: mcp.types.CancelledNotification) -> None
-```
diff --git a/docs/python-sdk/fastmcp-client-mixins-__init__.mdx b/docs/python-sdk/fastmcp-client-mixins-__init__.mdx
deleted file mode 100644
index bc0e32a4c..000000000
--- a/docs/python-sdk/fastmcp-client-mixins-__init__.mdx
+++ /dev/null
@@ -1,9 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.client.mixins`
-
-
-Client mixins for FastMCP.
diff --git a/docs/python-sdk/fastmcp-client-mixins-prompts.mdx b/docs/python-sdk/fastmcp-client-mixins-prompts.mdx
deleted file mode 100644
index 3931c03db..000000000
--- a/docs/python-sdk/fastmcp-client-mixins-prompts.mdx
+++ /dev/null
@@ -1,119 +0,0 @@
----
-title: prompts
-sidebarTitle: prompts
----
-
-# `fastmcp.client.mixins.prompts`
-
-
-Prompt-related methods for FastMCP Client.
-
-## Classes
-
-### `ClientPromptsMixin`
-
-
-Mixin providing prompt-related methods for Client.
-
-
-**Methods:**
-
-#### `list_prompts_mcp`
-
-```python
-list_prompts_mcp(self: Client) -> mcp.types.ListPromptsResult
-```
-
-Send a prompts/list request and return the complete MCP protocol result.
-
-**Args:**
-- `cursor`: Optional pagination cursor from a previous request's nextCursor.
-
-**Returns:**
-- mcp.types.ListPromptsResult: The complete response object from the protocol,
-containing the list of prompts and any additional metadata.
-
-**Raises:**
-- `RuntimeError`: If called while the client is not connected.
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
-
-#### `list_prompts`
-
-```python
-list_prompts(self: Client) -> list[mcp.types.Prompt]
-```
-
-Retrieve all prompts available on the server.
-
-This method automatically fetches all pages if the server paginates results,
-returning the complete list. For manual pagination control (e.g., to handle
-large result sets incrementally), use list_prompts_mcp() with the cursor parameter.
-
-**Returns:**
-- list\[mcp.types.Prompt]: A list of all Prompt objects.
-
-**Raises:**
-- `RuntimeError`: If called while the client is not connected.
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
-
-#### `get_prompt_mcp`
-
-```python
-get_prompt_mcp(self: Client, name: str, arguments: dict[str, Any] | None = None, meta: dict[str, Any] | None = None) -> mcp.types.GetPromptResult
-```
-
-Send a prompts/get request and return the complete MCP protocol result.
-
-**Args:**
-- `name`: The name of the prompt to retrieve.
-- `arguments`: Arguments to pass to the prompt. Defaults to None.
-- `meta`: Request metadata (e.g., for SEP-1686 tasks). Defaults to None.
-
-**Returns:**
-- mcp.types.GetPromptResult: The complete response object from the protocol,
-containing the prompt messages and any additional metadata.
-
-**Raises:**
-- `RuntimeError`: If called while the client is not connected.
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
-
-#### `get_prompt`
-
-```python
-get_prompt(self: Client, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult
-```
-
-#### `get_prompt`
-
-```python
-get_prompt(self: Client, name: str, arguments: dict[str, Any] | None = None) -> PromptTask
-```
-
-#### `get_prompt`
-
-```python
-get_prompt(self: Client, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult | PromptTask
-```
-
-Retrieve a rendered prompt message list from the server.
-
-**Args:**
-- `name`: The name of the prompt to retrieve.
-- `arguments`: Arguments to pass to the prompt. Defaults to None.
-- `version`: Specific prompt version to get. If None, gets highest version.
-- `meta`: Optional request-level metadata.
-- `task`: If True, execute as background task (SEP-1686). Defaults to False.
-- `task_id`: Optional client-provided task ID (auto-generated if not provided).
-- `ttl`: Time to keep results available in milliseconds (default 60s).
-
-**Returns:**
-- mcp.types.GetPromptResult | PromptTask: The complete response object if task=False,
-or a PromptTask object if task=True.
-
-**Raises:**
-- `RuntimeError`: If called while the client is not connected.
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
diff --git a/docs/python-sdk/fastmcp-client-mixins-resources.mdx b/docs/python-sdk/fastmcp-client-mixins-resources.mdx
deleted file mode 100644
index 655101ac3..000000000
--- a/docs/python-sdk/fastmcp-client-mixins-resources.mdx
+++ /dev/null
@@ -1,158 +0,0 @@
----
-title: resources
-sidebarTitle: resources
----
-
-# `fastmcp.client.mixins.resources`
-
-
-Resource-related methods for FastMCP Client.
-
-## Classes
-
-### `ClientResourcesMixin`
-
-
-Mixin providing resource-related methods for Client.
-
-
-**Methods:**
-
-#### `list_resources_mcp`
-
-```python
-list_resources_mcp(self: Client) -> mcp.types.ListResourcesResult
-```
-
-Send a resources/list request and return the complete MCP protocol result.
-
-**Args:**
-- `cursor`: Optional pagination cursor from a previous request's nextCursor.
-
-**Returns:**
-- mcp.types.ListResourcesResult: The complete response object from the protocol,
-containing the list of resources and any additional metadata.
-
-**Raises:**
-- `RuntimeError`: If called while the client is not connected.
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
-
-#### `list_resources`
-
-```python
-list_resources(self: Client) -> list[mcp.types.Resource]
-```
-
-Retrieve all resources available on the server.
-
-This method automatically fetches all pages if the server paginates results,
-returning the complete list. For manual pagination control (e.g., to handle
-large result sets incrementally), use list_resources_mcp() with the cursor parameter.
-
-**Returns:**
-- list\[mcp.types.Resource]: A list of all Resource objects.
-
-**Raises:**
-- `RuntimeError`: If called while the client is not connected.
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
-
-#### `list_resource_templates_mcp`
-
-```python
-list_resource_templates_mcp(self: Client) -> mcp.types.ListResourceTemplatesResult
-```
-
-Send a resources/listResourceTemplates request and return the complete MCP protocol result.
-
-**Args:**
-- `cursor`: Optional pagination cursor from a previous request's nextCursor.
-
-**Returns:**
-- mcp.types.ListResourceTemplatesResult: The complete response object from the protocol,
-containing the list of resource templates and any additional metadata.
-
-**Raises:**
-- `RuntimeError`: If called while the client is not connected.
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
-
-#### `list_resource_templates`
-
-```python
-list_resource_templates(self: Client) -> list[mcp.types.ResourceTemplate]
-```
-
-Retrieve all resource templates available on the server.
-
-This method automatically fetches all pages if the server paginates results,
-returning the complete list. For manual pagination control (e.g., to handle
-large result sets incrementally), use list_resource_templates_mcp() with the
-cursor parameter.
-
-**Returns:**
-- list\[mcp.types.ResourceTemplate]: A list of all ResourceTemplate objects.
-
-**Raises:**
-- `RuntimeError`: If called while the client is not connected.
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
-
-#### `read_resource_mcp`
-
-```python
-read_resource_mcp(self: Client, uri: AnyUrl | str, meta: dict[str, Any] | None = None) -> mcp.types.ReadResourceResult
-```
-
-Send a resources/read request and return the complete MCP protocol result.
-
-**Args:**
-- `uri`: The URI of the resource to read. Can be a string or an AnyUrl object.
-- `meta`: Request metadata (e.g., for SEP-1686 tasks). Defaults to None.
-
-**Returns:**
-- mcp.types.ReadResourceResult: The complete response object from the protocol,
-containing the resource contents and any additional metadata.
-
-**Raises:**
-- `RuntimeError`: If called while the client is not connected.
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
-
-#### `read_resource`
-
-```python
-read_resource(self: Client, uri: AnyUrl | str) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]
-```
-
-#### `read_resource`
-
-```python
-read_resource(self: Client, uri: AnyUrl | str) -> ResourceTask
-```
-
-#### `read_resource`
-
-```python
-read_resource(self: Client, uri: AnyUrl | str) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents] | ResourceTask
-```
-
-Read the contents of a resource or resolved template.
-
-**Args:**
-- `uri`: The URI of the resource to read. Can be a string or an AnyUrl object.
-- `version`: Specific version to read. If None, reads highest version.
-- `meta`: Optional request-level metadata.
-- `task`: If True, execute as background task (SEP-1686). Defaults to False.
-- `task_id`: Optional client-provided task ID (auto-generated if not provided).
-- `ttl`: Time to keep results available in milliseconds (default 60s).
-
-**Returns:**
-- list\[mcp.types.TextResourceContents | mcp.types.BlobResourceContents] | ResourceTask:
-A list of content objects if task=False, or a ResourceTask object if task=True.
-
-**Raises:**
-- `RuntimeError`: If called while the client is not connected.
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
diff --git a/docs/python-sdk/fastmcp-client-mixins-task_management.mdx b/docs/python-sdk/fastmcp-client-mixins-task_management.mdx
deleted file mode 100644
index 90d6d18b8..000000000
--- a/docs/python-sdk/fastmcp-client-mixins-task_management.mdx
+++ /dev/null
@@ -1,110 +0,0 @@
----
-title: task_management
-sidebarTitle: task_management
----
-
-# `fastmcp.client.mixins.task_management`
-
-
-Task management methods for FastMCP Client.
-
-## Classes
-
-### `ClientTaskManagementMixin`
-
-
-Mixin providing task management methods for Client.
-
-
-**Methods:**
-
-#### `get_task_status`
-
-```python
-get_task_status(self: Client, task_id: str) -> GetTaskResult
-```
-
-Query the status of a background task.
-
-Sends a 'tasks/get' MCP protocol request over the existing transport.
-
-**Args:**
-- `task_id`: The task ID returned from call_tool_as_task
-
-**Returns:**
-- Status information including taskId, status, pollInterval, etc.
-
-**Raises:**
-- `RuntimeError`: If client not connected
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
-
-#### `get_task_result`
-
-```python
-get_task_result(self: Client, task_id: str) -> Any
-```
-
-Retrieve the raw result of a completed background task.
-
-Sends a 'tasks/result' MCP protocol request over the existing transport.
-Returns the raw result - callers should parse it appropriately.
-
-**Args:**
-- `task_id`: The task ID returned from call_tool_as_task
-
-**Returns:**
-- The raw result (could be tool, prompt, or resource result)
-
-**Raises:**
-- `RuntimeError`: If client not connected, task not found, or task failed
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
-
-#### `list_tasks`
-
-```python
-list_tasks(self: Client, cursor: str | None = None, limit: int = 50) -> dict[str, Any]
-```
-
-List background tasks.
-
-Sends a 'tasks/list' MCP protocol request to the server. If the server
-returns an empty list (indicating client-side tracking), falls back to
-querying status for locally tracked task IDs.
-
-**Args:**
-- `cursor`: Optional pagination cursor
-- `limit`: Maximum number of tasks to return (default 50)
-
-**Returns:**
-- Response with structure:
-- tasks: List of task status dicts with taskId, status, etc.
-- nextCursor: Optional cursor for next page
-
-**Raises:**
-- `RuntimeError`: If client not connected
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
-
-#### `cancel_task`
-
-```python
-cancel_task(self: Client, task_id: str) -> mcp.types.CancelTaskResult
-```
-
-Cancel a task, transitioning it to cancelled state.
-
-Sends a 'tasks/cancel' MCP protocol request. Task will halt execution
-and transition to cancelled state.
-
-**Args:**
-- `task_id`: The task ID to cancel
-
-**Returns:**
-- The task status showing cancelled state
-
-**Raises:**
-- `RuntimeError`: If task doesn't exist
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
diff --git a/docs/python-sdk/fastmcp-client-mixins-tools.mdx b/docs/python-sdk/fastmcp-client-mixins-tools.mdx
deleted file mode 100644
index f048ed070..000000000
--- a/docs/python-sdk/fastmcp-client-mixins-tools.mdx
+++ /dev/null
@@ -1,141 +0,0 @@
----
-title: tools
-sidebarTitle: tools
----
-
-# `fastmcp.client.mixins.tools`
-
-
-Tool-related methods for FastMCP Client.
-
-## Classes
-
-### `ClientToolsMixin`
-
-
-Mixin providing tool-related methods for Client.
-
-
-**Methods:**
-
-#### `list_tools_mcp`
-
-```python
-list_tools_mcp(self: Client) -> mcp.types.ListToolsResult
-```
-
-Send a tools/list request and return the complete MCP protocol result.
-
-**Args:**
-- `cursor`: Optional pagination cursor from a previous request's nextCursor.
-
-**Returns:**
-- mcp.types.ListToolsResult: The complete response object from the protocol,
-containing the list of tools and any additional metadata.
-
-**Raises:**
-- `RuntimeError`: If called while the client is not connected.
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
-
-#### `list_tools`
-
-```python
-list_tools(self: Client) -> list[mcp.types.Tool]
-```
-
-Retrieve all tools available on the server.
-
-This method automatically fetches all pages if the server paginates results,
-returning the complete list. For manual pagination control (e.g., to handle
-large result sets incrementally), use list_tools_mcp() with the cursor parameter.
-
-**Returns:**
-- list\[mcp.types.Tool]: A list of all Tool objects.
-
-**Raises:**
-- `RuntimeError`: If called while the client is not connected.
-- `McpError`: If the request results in a TimeoutError | JSONRPCError
-
-
-#### `call_tool_mcp`
-
-```python
-call_tool_mcp(self: Client, name: str, arguments: dict[str, Any], progress_handler: ProgressHandler | None = None, timeout: datetime.timedelta | float | int | None = None, meta: dict[str, Any] | None = None) -> mcp.types.CallToolResult
-```
-
-Send a tools/call request and return the complete MCP protocol result.
-
-This method returns the raw CallToolResult object, which includes an isError flag
-and other metadata. It does not raise an exception if the tool call results in an error.
-
-**Args:**
-- `name`: The name of the tool to call.
-- `arguments`: Arguments to pass to the tool.
-- `timeout`: The timeout for the tool call. Defaults to None.
-- `progress_handler`: The progress handler to use for the tool call. Defaults to None.
-- `meta`: Additional metadata to include with the request.
-This is useful for passing contextual information (like user IDs, trace IDs, or preferences)
-that shouldn't be tool arguments but may influence server-side processing. The server
-can access this via `context.request_context.meta`. Defaults to None.
-
-**Returns:**
-- mcp.types.CallToolResult: The complete response object from the protocol,
-containing the tool result and any additional metadata.
-
-**Raises:**
-- `RuntimeError`: If called while the client is not connected.
-- `McpError`: If the tool call requests results in a TimeoutError | JSONRPCError
-
-
-#### `call_tool`
-
-```python
-call_tool(self: Client, name: str, arguments: dict[str, Any] | None = None) -> CallToolResult
-```
-
-#### `call_tool`
-
-```python
-call_tool(self: Client, name: str, arguments: dict[str, Any] | None = None) -> ToolTask
-```
-
-#### `call_tool`
-
-```python
-call_tool(self: Client, name: str, arguments: dict[str, Any] | None = None) -> CallToolResult | ToolTask
-```
-
-Call a tool on the server.
-
-Unlike call_tool_mcp, this method raises a ToolError if the tool call results in an error.
-
-**Args:**
-- `name`: The name of the tool to call.
-- `arguments`: Arguments to pass to the tool. Defaults to None.
-- `version`: Specific tool version to call. If None, calls highest version.
-- `timeout`: The timeout for the tool call. Defaults to None.
-- `progress_handler`: The progress handler to use for the tool call. Defaults to None.
-- `raise_on_error`: Whether to raise an exception if the tool call results in an error. Defaults to True.
-- `meta`: Additional metadata to include with the request.
-This is useful for passing contextual information (like user IDs, trace IDs, or preferences)
-that shouldn't be tool arguments but may influence server-side processing. The server
-can access this via `context.request_context.meta`. Defaults to None.
-- `task`: If True, execute as background task (SEP-1686). Defaults to False.
-- `task_id`: Optional client-provided task ID (auto-generated if not provided).
-- `ttl`: Time to keep results available in milliseconds (default 60s).
-
-**Returns:**
-- CallToolResult | ToolTask: The content returned by the tool if task=False,
-or a ToolTask object if task=True. If the tool returns structured
-outputs, they are returned as a dataclass (if an output schema
-is available) or a dictionary; otherwise, a list of content
-blocks is returned. Note: to receive both structured and
-unstructured outputs, use call_tool_mcp instead and access the
-raw result object.
-
-**Raises:**
-- `ToolError`: If the tool call results in an error.
-- `McpError`: If the tool call request results in a TimeoutError | JSONRPCError
-- `RuntimeError`: If called while the client is not connected.
-
diff --git a/docs/python-sdk/fastmcp-client-oauth_callback.mdx b/docs/python-sdk/fastmcp-client-oauth_callback.mdx
deleted file mode 100644
index c3e3e84fb..000000000
--- a/docs/python-sdk/fastmcp-client-oauth_callback.mdx
+++ /dev/null
@@ -1,70 +0,0 @@
----
-title: oauth_callback
-sidebarTitle: oauth_callback
----
-
-# `fastmcp.client.oauth_callback`
-
-
-
-OAuth callback server for handling authorization code flows.
-
-This module provides a reusable callback server that can handle OAuth redirects
-and display styled responses to users.
-
-
-## Functions
-
-### `create_callback_html`
-
-```python
-create_callback_html(message: str, is_success: bool = True, title: str = 'FastMCP OAuth', server_url: str | None = None) -> str
-```
-
-
-Create a styled HTML response for OAuth callbacks.
-
-
-### `create_oauth_callback_server`
-
-```python
-create_oauth_callback_server(port: int, callback_path: str = '/callback', server_url: str | None = None, result_container: OAuthCallbackResult | None = None, result_ready: anyio.Event | None = None) -> Server
-```
-
-
-Create an OAuth callback server.
-
-**Args:**
-- `port`: The port to run the server on
-- `callback_path`: The path to listen for OAuth redirects on
-- `server_url`: Optional server URL to display in success messages
-- `result_container`: Optional container to store callback results
-- `result_ready`: Optional event to signal when callback is received
-
-**Returns:**
-- Configured uvicorn Server instance (not yet running)
-
-
-## Classes
-
-### `CallbackResponse`
-
-**Methods:**
-
-#### `from_dict`
-
-```python
-from_dict(cls, data: dict[str, str]) -> CallbackResponse
-```
-
-#### `to_dict`
-
-```python
-to_dict(self) -> dict[str, str]
-```
-
-### `OAuthCallbackResult`
-
-
-Container for OAuth callback results, used with anyio.Event for async coordination.
-
diff --git a/docs/python-sdk/fastmcp-client-progress.mdx b/docs/python-sdk/fastmcp-client-progress.mdx
deleted file mode 100644
index 884dab165..000000000
--- a/docs/python-sdk/fastmcp-client-progress.mdx
+++ /dev/null
@@ -1,25 +0,0 @@
----
-title: progress
-sidebarTitle: progress
----
-
-# `fastmcp.client.progress`
-
-## Functions
-
-### `default_progress_handler`
-
-```python
-default_progress_handler(progress: float, total: float | None, message: str | None) -> None
-```
-
-
-Default handler for progress notifications.
-
-Logs progress updates at debug level, properly handling missing total or message values.
-
-**Args:**
-- `progress`: Current progress value
-- `total`: Optional total expected value
-- `message`: Optional status message
-
diff --git a/docs/python-sdk/fastmcp-client-roots.mdx b/docs/python-sdk/fastmcp-client-roots.mdx
deleted file mode 100644
index 8b429856a..000000000
--- a/docs/python-sdk/fastmcp-client-roots.mdx
+++ /dev/null
@@ -1,20 +0,0 @@
----
-title: roots
-sidebarTitle: roots
----
-
-# `fastmcp.client.roots`
-
-## Functions
-
-### `convert_roots_list`
-
-```python
-convert_roots_list(roots: RootsList) -> list[mcp.types.Root]
-```
-
-### `create_roots_callback`
-
-```python
-create_roots_callback(handler: RootsList | RootsHandler) -> ListRootsFnT
-```
diff --git a/docs/python-sdk/fastmcp-client-sampling-__init__.mdx b/docs/python-sdk/fastmcp-client-sampling-__init__.mdx
deleted file mode 100644
index 609853b1f..000000000
--- a/docs/python-sdk/fastmcp-client-sampling-__init__.mdx
+++ /dev/null
@@ -1,14 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.client.sampling`
-
-## Functions
-
-### `create_sampling_callback`
-
-```python
-create_sampling_callback(sampling_handler: SamplingHandler) -> SamplingFnT
-```
diff --git a/docs/python-sdk/fastmcp-client-sampling-handlers-__init__.mdx b/docs/python-sdk/fastmcp-client-sampling-handlers-__init__.mdx
deleted file mode 100644
index 43329b5e6..000000000
--- a/docs/python-sdk/fastmcp-client-sampling-handlers-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.client.sampling.handlers`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-client-sampling-handlers-anthropic.mdx b/docs/python-sdk/fastmcp-client-sampling-handlers-anthropic.mdx
deleted file mode 100644
index 976367c28..000000000
--- a/docs/python-sdk/fastmcp-client-sampling-handlers-anthropic.mdx
+++ /dev/null
@@ -1,17 +0,0 @@
----
-title: anthropic
-sidebarTitle: anthropic
----
-
-# `fastmcp.client.sampling.handlers.anthropic`
-
-
-Anthropic sampling handler for FastMCP.
-
-## Classes
-
-### `AnthropicSamplingHandler`
-
-
-Sampling handler that uses the Anthropic API.
-
diff --git a/docs/python-sdk/fastmcp-client-sampling-handlers-google_genai.mdx b/docs/python-sdk/fastmcp-client-sampling-handlers-google_genai.mdx
deleted file mode 100644
index 9681c3a4a..000000000
--- a/docs/python-sdk/fastmcp-client-sampling-handlers-google_genai.mdx
+++ /dev/null
@@ -1,17 +0,0 @@
----
-title: google_genai
-sidebarTitle: google_genai
----
-
-# `fastmcp.client.sampling.handlers.google_genai`
-
-
-Google GenAI sampling handler with tool support for FastMCP 3.0.
-
-## Classes
-
-### `GoogleGenaiSamplingHandler`
-
-
-Sampling handler that uses the Google GenAI API with tool support.
-
diff --git a/docs/python-sdk/fastmcp-client-sampling-handlers-openai.mdx b/docs/python-sdk/fastmcp-client-sampling-handlers-openai.mdx
deleted file mode 100644
index b291bbaa5..000000000
--- a/docs/python-sdk/fastmcp-client-sampling-handlers-openai.mdx
+++ /dev/null
@@ -1,17 +0,0 @@
----
-title: openai
-sidebarTitle: openai
----
-
-# `fastmcp.client.sampling.handlers.openai`
-
-
-OpenAI sampling handler for FastMCP.
-
-## Classes
-
-### `OpenAISamplingHandler`
-
-
-Sampling handler that uses the OpenAI API.
-
diff --git a/docs/python-sdk/fastmcp-client-tasks.mdx b/docs/python-sdk/fastmcp-client-tasks.mdx
deleted file mode 100644
index 547d9bc92..000000000
--- a/docs/python-sdk/fastmcp-client-tasks.mdx
+++ /dev/null
@@ -1,219 +0,0 @@
----
-title: tasks
-sidebarTitle: tasks
----
-
-# `fastmcp.client.tasks`
-
-
-SEP-1686 client Task classes.
-
-## Classes
-
-### `TaskNotificationHandler`
-
-
-MessageHandler that routes task status notifications to Task objects.
-
-
-**Methods:**
-
-#### `dispatch`
-
-```python
-dispatch(self, message: Message) -> None
-```
-
-Dispatch messages, including task status notifications.
-
-
-### `Task`
-
-
-Abstract base class for MCP background tasks (SEP-1686).
-
-Provides a uniform API whether the server accepts background execution
-or executes synchronously (graceful degradation per SEP-1686).
-
-
-**Methods:**
-
-#### `task_id`
-
-```python
-task_id(self) -> str
-```
-
-Get the task ID.
-
-
-#### `returned_immediately`
-
-```python
-returned_immediately(self) -> bool
-```
-
-Check if server executed the task immediately.
-
-**Returns:**
-- True if server executed synchronously (graceful degradation or no task support)
-- False if server accepted background execution
-
-
-#### `on_status_change`
-
-```python
-on_status_change(self, callback: Callable[[GetTaskResult], None | Awaitable[None]]) -> None
-```
-
-Register callback for status change notifications.
-
-The callback will be invoked when a notifications/tasks/status is received
-for this task (optional server feature per SEP-1686 lines 436-444).
-
-Supports both sync and async callbacks (auto-detected).
-
-**Args:**
-- `callback`: Function to call with GetTaskResult when status changes.
- Can return None (sync) or Awaitable[None] (async).
-
-
-#### `status`
-
-```python
-status(self) -> GetTaskResult
-```
-
-Get current task status.
-
-If server executed immediately, returns synthetic completed status.
-Otherwise queries the server for current status.
-
-
-#### `result`
-
-```python
-result(self) -> TaskResultT
-```
-
-Wait for and return the task result.
-
-Must be implemented by subclasses to return the appropriate result type.
-
-
-#### `wait`
-
-```python
-wait(self) -> GetTaskResult
-```
-
-Wait for task to reach a specific state or complete.
-
-Uses event-based waiting when notifications are available (fast),
-with fallback to polling (reliable). Optimally wakes up immediately
-on status changes when server sends notifications/tasks/status.
-
-**Args:**
-- `state`: Desired state ('submitted', 'working', 'completed', 'failed').
- If None, waits for any terminal state (completed/failed)
-- `timeout`: Maximum time to wait in seconds
-
-**Returns:**
-- Final task status
-
-**Raises:**
-- `TimeoutError`: If desired state not reached within timeout
-
-
-#### `cancel`
-
-```python
-cancel(self) -> None
-```
-
-Cancel this task, transitioning it to cancelled state.
-
-Sends a tasks/cancel protocol request. The server will attempt to halt
-execution and move the task to cancelled state.
-
-Note: If server executed immediately (graceful degradation), this is a no-op
-as there's no server-side task to cancel.
-
-
-### `ToolTask`
-
-
-Represents a tool call that may execute in background or immediately.
-
-Provides a uniform API whether the server accepts background execution
-or executes synchronously (graceful degradation per SEP-1686).
-
-
-**Methods:**
-
-#### `result`
-
-```python
-result(self) -> CallToolResult
-```
-
-Wait for and return the tool result.
-
-If server executed immediately, returns the immediate result.
-Otherwise waits for background task to complete and retrieves result.
-
-**Returns:**
-- The parsed tool result (same as call_tool returns)
-
-
-### `PromptTask`
-
-
-Represents a prompt call that may execute in background or immediately.
-
-Provides a uniform API whether the server accepts background execution
-or executes synchronously (graceful degradation per SEP-1686).
-
-
-**Methods:**
-
-#### `result`
-
-```python
-result(self) -> mcp.types.GetPromptResult
-```
-
-Wait for and return the prompt result.
-
-If server executed immediately, returns the immediate result.
-Otherwise waits for background task to complete and retrieves result.
-
-**Returns:**
-- The prompt result with messages and description
-
-
-### `ResourceTask`
-
-
-Represents a resource read that may execute in background or immediately.
-
-Provides a uniform API whether the server accepts background execution
-or executes synchronously (graceful degradation per SEP-1686).
-
-
-**Methods:**
-
-#### `result`
-
-```python
-result(self) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]
-```
-
-Wait for and return the resource contents.
-
-If server executed immediately, returns the immediate result.
-Otherwise waits for background task to complete and retrieves result.
-
-**Returns:**
-- list\[ReadResourceContents]: The resource contents
-
diff --git a/docs/python-sdk/fastmcp-client-telemetry.mdx b/docs/python-sdk/fastmcp-client-telemetry.mdx
deleted file mode 100644
index 9a1f8d260..000000000
--- a/docs/python-sdk/fastmcp-client-telemetry.mdx
+++ /dev/null
@@ -1,23 +0,0 @@
----
-title: telemetry
-sidebarTitle: telemetry
----
-
-# `fastmcp.client.telemetry`
-
-
-Client-side telemetry helpers.
-
-## Functions
-
-### `client_span`
-
-```python
-client_span(name: str, method: str, component_key: str, session_id: str | None = None, resource_uri: str | None = None) -> Generator[Span, None, None]
-```
-
-
-Create a CLIENT span with standard MCP attributes.
-
-Automatically records any exception on the span and sets error status.
-
diff --git a/docs/python-sdk/fastmcp-client-transports-__init__.mdx b/docs/python-sdk/fastmcp-client-transports-__init__.mdx
deleted file mode 100644
index 1f9b02d38..000000000
--- a/docs/python-sdk/fastmcp-client-transports-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.client.transports`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-client-transports-base.mdx b/docs/python-sdk/fastmcp-client-transports-base.mdx
deleted file mode 100644
index 12c1848d2..000000000
--- a/docs/python-sdk/fastmcp-client-transports-base.mdx
+++ /dev/null
@@ -1,62 +0,0 @@
----
-title: base
-sidebarTitle: base
----
-
-# `fastmcp.client.transports.base`
-
-## Classes
-
-### `SessionKwargs`
-
-
-Keyword arguments for the MCP ClientSession constructor.
-
-
-### `ClientTransport`
-
-
-Abstract base class for different MCP client transport mechanisms.
-
-A Transport is responsible for establishing and managing connections
-to an MCP server, and providing a ClientSession within an async context.
-
-
-**Methods:**
-
-#### `connect_session`
-
-```python
-connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
-```
-
-Establishes a connection and yields an active ClientSession.
-
-The ClientSession is *not* expected to be initialized in this context manager.
-
-The session is guaranteed to be valid only within the scope of the
-async context manager. Connection setup and teardown are handled
-within this context.
-
-**Args:**
-- `**session_kwargs`: Keyword arguments to pass to the ClientSession
- constructor (e.g., callbacks, timeouts).
-
-
-#### `close`
-
-```python
-close(self)
-```
-
-Close the transport.
-
-
-#### `get_session_id`
-
-```python
-get_session_id(self) -> str | None
-```
-
-Get the session ID for this transport, if available.
-
diff --git a/docs/python-sdk/fastmcp-client-transports-config.mdx b/docs/python-sdk/fastmcp-client-transports-config.mdx
deleted file mode 100644
index 7ad10e0df..000000000
--- a/docs/python-sdk/fastmcp-client-transports-config.mdx
+++ /dev/null
@@ -1,72 +0,0 @@
----
-title: config
-sidebarTitle: config
----
-
-# `fastmcp.client.transports.config`
-
-## Classes
-
-### `MCPConfigTransport`
-
-
-Transport for connecting to one or more MCP servers defined in an MCPConfig.
-
-This transport provides a unified interface to multiple MCP servers defined in an MCPConfig
-object or dictionary matching the MCPConfig schema. It supports two key scenarios:
-
-1. If the MCPConfig contains exactly one server, it creates a direct transport to that server.
-2. If the MCPConfig contains multiple servers, it creates a composite client by mounting
- all servers on a single FastMCP instance, with each server's name, by default, used as its mounting prefix.
-
-In the multiserver case, tools are accessible with the prefix pattern `{server_name}_{tool_name}`
-and resources with the pattern `protocol://{server_name}/path/to/resource`.
-
-This is particularly useful for creating clients that need to interact with multiple specialized
-MCP servers through a single interface, simplifying client code.
-
-**Examples:**
-
-```python
-from fastmcp import Client
-
-# Create a config with multiple servers
-config = {
- "mcpServers": {
- "weather": {
- "url": "https://weather-api.example.com/mcp",
- "transport": "http"
- },
- "calendar": {
- "url": "https://calendar-api.example.com/mcp",
- "transport": "http"
- }
- }
-}
-
-# Create a client with the config
-client = Client(config)
-
-async with client:
- # Access tools with prefixes
- weather = await client.call_tool("weather_get_forecast", {"city": "London"})
- events = await client.call_tool("calendar_list_events", {"date": "2023-06-01"})
-
- # Access resources with prefixed URIs
- icons = await client.read_resource("weather://weather/icons/sunny")
-```
-
-
-**Methods:**
-
-#### `connect_session`
-
-```python
-connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
-```
-
-#### `close`
-
-```python
-close(self)
-```
diff --git a/docs/python-sdk/fastmcp-client-transports-http.mdx b/docs/python-sdk/fastmcp-client-transports-http.mdx
deleted file mode 100644
index a3375240e..000000000
--- a/docs/python-sdk/fastmcp-client-transports-http.mdx
+++ /dev/null
@@ -1,37 +0,0 @@
----
-title: http
-sidebarTitle: http
----
-
-# `fastmcp.client.transports.http`
-
-
-Streamable HTTP transport for FastMCP Client.
-
-## Classes
-
-### `StreamableHttpTransport`
-
-
-Transport implementation that connects to an MCP server via Streamable HTTP Requests.
-
-
-**Methods:**
-
-#### `connect_session`
-
-```python
-connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
-```
-
-#### `get_session_id`
-
-```python
-get_session_id(self) -> str | None
-```
-
-#### `close`
-
-```python
-close(self)
-```
diff --git a/docs/python-sdk/fastmcp-client-transports-inference.mdx b/docs/python-sdk/fastmcp-client-transports-inference.mdx
deleted file mode 100644
index 730f56845..000000000
--- a/docs/python-sdk/fastmcp-client-transports-inference.mdx
+++ /dev/null
@@ -1,56 +0,0 @@
----
-title: inference
-sidebarTitle: inference
----
-
-# `fastmcp.client.transports.inference`
-
-## Functions
-
-### `infer_transport`
-
-```python
-infer_transport(transport: ClientTransport | FastMCP | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str) -> ClientTransport
-```
-
-
-Infer the appropriate transport type from the given transport argument.
-
-This function attempts to infer the correct transport type from the provided
-argument, handling various input types and converting them to the appropriate
-ClientTransport subclass.
-
-The function supports these input types:
-- ClientTransport: Used directly without modification
-- FastMCP or FastMCP1Server: Creates an in-memory FastMCPTransport
-- Path or str (file path): Creates PythonStdioTransport (.py) or NodeStdioTransport (.js)
-- AnyUrl or str (URL): Creates StreamableHttpTransport (default) or SSETransport (for /sse endpoints)
-- MCPConfig or dict: Creates MCPConfigTransport, potentially connecting to multiple servers
-
-For HTTP URLs, they are assumed to be Streamable HTTP URLs unless they end in `/sse`.
-
-For MCPConfig with multiple servers, a composite client is created where each server
-is mounted with its name as prefix. This allows accessing tools and resources from multiple
-servers through a single unified client interface, using naming patterns like
-`servername_toolname` for tools and `protocol://servername/path` for resources.
-If the MCPConfig contains only one server, a direct connection is established without prefixing.
-
-**Examples:**
-
-```python
-# Connect to a local Python script
-transport = infer_transport("my_script.py")
-
-# Connect to a remote server via HTTP
-transport = infer_transport("http://example.com/mcp")
-
-# Connect to multiple servers using MCPConfig
-config = {
- "mcpServers": {
- "weather": {"url": "http://weather.example.com/mcp"},
- "calendar": {"url": "http://calendar.example.com/mcp"}
- }
-}
-transport = infer_transport(config)
-```
-
diff --git a/docs/python-sdk/fastmcp-client-transports-memory.mdx b/docs/python-sdk/fastmcp-client-transports-memory.mdx
deleted file mode 100644
index b5887b8ca..000000000
--- a/docs/python-sdk/fastmcp-client-transports-memory.mdx
+++ /dev/null
@@ -1,27 +0,0 @@
----
-title: memory
-sidebarTitle: memory
----
-
-# `fastmcp.client.transports.memory`
-
-## Classes
-
-### `FastMCPTransport`
-
-
-In-memory transport for FastMCP servers.
-
-This transport connects directly to a FastMCP server instance in the same
-Python process. It works with both FastMCP 2.x servers and FastMCP 1.0
-servers from the low-level MCP SDK. This is particularly useful for unit
-tests or scenarios where client and server run in the same runtime.
-
-
-**Methods:**
-
-#### `connect_session`
-
-```python
-connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
-```
diff --git a/docs/python-sdk/fastmcp-client-transports-sse.mdx b/docs/python-sdk/fastmcp-client-transports-sse.mdx
deleted file mode 100644
index 59c145401..000000000
--- a/docs/python-sdk/fastmcp-client-transports-sse.mdx
+++ /dev/null
@@ -1,25 +0,0 @@
----
-title: sse
-sidebarTitle: sse
----
-
-# `fastmcp.client.transports.sse`
-
-
-Server-Sent Events (SSE) transport for FastMCP Client.
-
-## Classes
-
-### `SSETransport`
-
-
-Transport implementation that connects to an MCP server via Server-Sent Events.
-
-
-**Methods:**
-
-#### `connect_session`
-
-```python
-connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
-```
diff --git a/docs/python-sdk/fastmcp-client-transports-stdio.mdx b/docs/python-sdk/fastmcp-client-transports-stdio.mdx
deleted file mode 100644
index eb7d98eb2..000000000
--- a/docs/python-sdk/fastmcp-client-transports-stdio.mdx
+++ /dev/null
@@ -1,79 +0,0 @@
----
-title: stdio
-sidebarTitle: stdio
----
-
-# `fastmcp.client.transports.stdio`
-
-## Classes
-
-### `StdioTransport`
-
-
-Base transport for connecting to an MCP server via subprocess with stdio.
-
-This is a base class that can be subclassed for specific command-based
-transports like Python, Node, Uvx, etc.
-
-
-**Methods:**
-
-#### `connect_session`
-
-```python
-connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
-```
-
-#### `connect`
-
-```python
-connect(self, **session_kwargs: Unpack[SessionKwargs]) -> ClientSession | None
-```
-
-#### `disconnect`
-
-```python
-disconnect(self)
-```
-
-#### `close`
-
-```python
-close(self)
-```
-
-### `PythonStdioTransport`
-
-
-Transport for running Python scripts.
-
-
-### `FastMCPStdioTransport`
-
-
-Transport for running FastMCP servers using the FastMCP CLI.
-
-
-### `NodeStdioTransport`
-
-
-Transport for running Node.js scripts.
-
-
-### `UvStdioTransport`
-
-
-Transport for running commands via the uv tool.
-
-
-### `UvxStdioTransport`
-
-
-Transport for running commands via the uvx tool.
-
-
-### `NpxStdioTransport`
-
-
-Transport for running commands via the npx tool.
-
diff --git a/docs/python-sdk/fastmcp-decorators.mdx b/docs/python-sdk/fastmcp-decorators.mdx
index c2cc11dbb..0eb68b25a 100644
--- a/docs/python-sdk/fastmcp-decorators.mdx
+++ b/docs/python-sdk/fastmcp-decorators.mdx
@@ -10,7 +10,7 @@ Shared decorator utilities for FastMCP.
## Functions
-### `resolve_task_config`
+### `resolve_task_config`
```python
resolve_task_config(task: bool | TaskConfig | None) -> bool | TaskConfig
@@ -20,7 +20,7 @@ resolve_task_config(task: bool | TaskConfig | None) -> bool | TaskConfig
Resolve task config, defaulting None to False.
-### `get_fastmcp_meta`
+### `get_fastmcp_meta`
```python
get_fastmcp_meta(fn: Any) -> Any | None
@@ -32,7 +32,7 @@ Extract FastMCP metadata from a function, handling bound methods and wrappers.
## Classes
-### `HasFastMCPMeta`
+### `HasFastMCPMeta`
Protocol for callables decorated with FastMCP metadata.
diff --git a/docs/python-sdk/fastmcp-dependencies.mdx b/docs/python-sdk/fastmcp-dependencies.mdx
index f27566ef3..310511e56 100644
--- a/docs/python-sdk/fastmcp-dependencies.mdx
+++ b/docs/python-sdk/fastmcp-dependencies.mdx
@@ -12,6 +12,7 @@ This module re-exports dependency injection symbols to provide a clean,
centralized import location for all dependency-related functionality.
DI features (Depends, CurrentContext, CurrentFastMCP) work without pydocket
-using the uncalled-for DI engine. Only task-related dependencies (CurrentDocket,
-CurrentWorker) and background task execution require fastmcp[tasks].
+using the uncalled-for DI engine. The docket-specific dependencies
+(``CurrentDocket``, ``CurrentWorker``) live in the ``fastmcp-tasks`` package
+(``fastmcp_tasks.dependencies``).
diff --git a/docs/python-sdk/fastmcp-exceptions.mdx b/docs/python-sdk/fastmcp-exceptions.mdx
index d8f5a6871..151f0f10a 100644
--- a/docs/python-sdk/fastmcp-exceptions.mdx
+++ b/docs/python-sdk/fastmcp-exceptions.mdx
@@ -8,64 +8,117 @@ sidebarTitle: exceptions
Custom exceptions for FastMCP.
+## Functions
+
+### `to_mcp_error`
+
+```python
+to_mcp_error(exc: Exception) -> MCPError
+```
+
+
+Translate a FastMCP exception into a wire-format ``MCPError``.
+
+Central mapping from FastMCP's public exception types to the JSON-RPC error
+codes defined by the MCP spec (imported from ``mcp_types``). Request-handler
+adapters call this instead of hand-rolling ``MCPError(code=..., ...)`` per
+call site, so the wire codes stay spec-correct and consistent across
+resources, prompts, and tools.
+
+``NotFoundError`` and ``DisabledError`` map to ``INVALID_PARAMS`` (-32602):
+per SEP-2164 a request naming a component that does not exist (or is
+disabled) is an invalid-params error, which matches the SDK's own
+``ResourceNotFoundError -> INVALID_PARAMS`` mapping in ``mcp.server.mcpserver``.
+``ValidationError`` is also an invalid-params error. Everything else falls
+back to ``default_code`` (``INTERNAL_ERROR`` by default).
+
+If ``exc`` is already an ``MCPError``, it is returned unchanged so an
+explicit code chosen upstream survives translation.
+
+
## Classes
-### `FastMCPError`
+### `FastMCPError`
Base error for FastMCP.
-### `ValidationError`
+### `ValidationError`
Error in validating parameters or return values.
-### `ResourceError`
+### `ResourceError`
Error in resource operations.
-### `ToolError`
+### `ToolError`
Error in tool operations.
-### `PromptError`
+### `PromptError`
Error in prompt operations.
-### `InvalidSignature`
+### `InvalidSignature`
Invalid signature for use with FastMCP.
-### `ClientError`
+### `ClientError`
Error in client operations.
-### `NotFoundError`
+### `NotFoundError`
Object not found.
-### `DisabledError`
+### `DisabledError`
Object is disabled.
-### `AuthorizationError`
+### `ResourceSecurityError`
+
+
+A templated resource parameter failed path-security screening.
+
+Subclasses ``NotFoundError`` so the read handler surfaces a
+non-leaky ``INVALID_PARAMS`` (-32602) "resource not found" error to
+the client — a traversal attempt is indistinguishable from a request
+for a resource that does not exist, and never reveals which parameter
+or policy tripped.
+
+
+### `AuthorizationError`
Error when authorization check fails.
+
+### `InsufficientScopeError`
+
+
+Authorization failed because the token is missing required OAuth scopes.
+
+Unlike a bare ``AuthorizationError``, this carries the specific scopes the
+caller must obtain. A component-level scope shortfall can then be signalled
+as a spec-correct ``insufficient_scope`` step-up (SEP-2350 / RFC 6750 §3),
+naming exactly what to re-authorize for instead of an opaque denial. The
+named scopes are only the *unmet* ones, so an existing grant is accumulated
+rather than replaced when the caller re-authorizes.
+
diff --git a/docs/python-sdk/fastmcp-experimental-__init__.mdx b/docs/python-sdk/fastmcp-experimental-__init__.mdx
deleted file mode 100644
index 04ef31f33..000000000
--- a/docs/python-sdk/fastmcp-experimental-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.experimental`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-experimental-sampling-__init__.mdx b/docs/python-sdk/fastmcp-experimental-sampling-__init__.mdx
deleted file mode 100644
index f37691267..000000000
--- a/docs/python-sdk/fastmcp-experimental-sampling-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.experimental.sampling`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-experimental-sampling-handlers.mdx b/docs/python-sdk/fastmcp-experimental-sampling-handlers.mdx
deleted file mode 100644
index 9220684bb..000000000
--- a/docs/python-sdk/fastmcp-experimental-sampling-handlers.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: handlers
-sidebarTitle: handlers
----
-
-# `fastmcp.experimental.sampling.handlers`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-experimental-transforms-__init__.mdx b/docs/python-sdk/fastmcp-experimental-transforms-__init__.mdx
deleted file mode 100644
index a33a00679..000000000
--- a/docs/python-sdk/fastmcp-experimental-transforms-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.experimental.transforms`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-experimental-transforms-code_mode.mdx b/docs/python-sdk/fastmcp-experimental-transforms-code_mode.mdx
index 0553029eb..6d634b6ea 100644
--- a/docs/python-sdk/fastmcp-experimental-transforms-code_mode.mdx
+++ b/docs/python-sdk/fastmcp-experimental-transforms-code_mode.mdx
@@ -7,7 +7,7 @@ sidebarTitle: code_mode
## Classes
-### `SandboxProvider`
+### `SandboxProvider`
Interface for executing LLM-generated Python code in a sandbox.
@@ -20,13 +20,13 @@ sandbox — never with plain ``exec()``. Use ``MontySandboxProvider``
**Methods:**
-#### `run`
+#### `run`
```python
run(self, code: str) -> Any
```
-### `MontySandboxProvider`
+### `MontySandboxProvider`
Sandbox provider backed by `pydantic-monty`.
@@ -38,16 +38,22 @@ Sandbox provider backed by `pydantic-monty`.
``gc_interval`` (int). All are optional; omit a key to
leave that limit uncapped.
+When the argument is omitted entirely, a conservative baseline
+is applied (``max_duration_secs=30``, ``max_memory=100 MB``) so
+the out-of-box configuration is not unbounded. Pass
+``limits=None`` to explicitly run without any limits, or a dict
+to set your own.
+
**Methods:**
-#### `run`
+#### `run`
```python
run(self, code: str) -> Any
```
-### `Search`
+### `Search`
Discovery tool factory that searches the catalog by query.
@@ -64,7 +70,7 @@ Defaults to BM25 ranking.
The LLM can override this per call. ``None`` means no limit.
-### `GetSchemas`
+### `GetSchemas`
Discovery tool factory that returns schemas for tools by name.
@@ -78,7 +84,7 @@ types, and required markers.
``"full"`` returns the complete JSON schema.
-### `GetTags`
+### `GetTags`
Discovery tool factory that lists tool tags from the catalog.
@@ -93,7 +99,7 @@ without tags appear under ``"untagged"``.
``"full"`` lists all tools under each tag.
-### `ListTools`
+### `ListTools`
Discovery tool factory that lists all tools in the catalog.
@@ -106,7 +112,7 @@ Discovery tool factory that lists all tools in the catalog.
``"full"`` returns the complete JSON schema.
-### `CodeMode`
+### `CodeMode`
Transform that collapses all tools into discovery + execute meta-tools.
@@ -123,13 +129,13 @@ environment with ``call_tool(name, params)`` in scope.
**Methods:**
-#### `transform_tools`
+#### `transform_tools`
```python
transform_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
```
-#### `get_tool`
+#### `get_tool`
```python
get_tool(self, name: str, call_next: GetToolNext) -> Tool | None
diff --git a/docs/python-sdk/fastmcp-mcp_config.mdx b/docs/python-sdk/fastmcp-mcp_config.mdx
index f7d8f6e26..70f0978ff 100644
--- a/docs/python-sdk/fastmcp-mcp_config.mdx
+++ b/docs/python-sdk/fastmcp-mcp_config.mdx
@@ -32,7 +32,7 @@ Example configuration:
## Functions
-### `infer_transport_type_from_url`
+### `infer_transport_type_from_url`
```python
infer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse']
@@ -42,7 +42,7 @@ infer_transport_type_from_url(url: str | AnyUrl) -> Literal['http', 'sse']
Infer the appropriate transport type from the given URL.
-### `update_config_file`
+### `update_config_file`
```python
update_config_file(file_path: Path, server_name: str, server_config: CanonicalMCPServerTypes) -> None
@@ -57,7 +57,7 @@ worry about transforming server objects here.
## Classes
-### `StdioMCPServer`
+### `StdioMCPServer`
MCP server configuration for stdio transport.
@@ -67,19 +67,19 @@ This is the canonical configuration format for MCP servers using stdio transport
**Methods:**
-#### `to_transport`
+#### `to_transport`
```python
-to_transport(self) -> StdioTransport
+to_transport(self) -> StdioTransport | FastMCPTransport
```
-### `TransformingStdioMCPServer`
+### `TransformingStdioMCPServer`
A Stdio server with tool transforms.
-### `RemoteMCPServer`
+### `RemoteMCPServer`
MCP server configuration for HTTP/SSE transport.
@@ -89,19 +89,19 @@ This is the canonical configuration format for MCP servers using remote transpor
**Methods:**
-#### `to_transport`
+#### `to_transport`
```python
-to_transport(self) -> StreamableHttpTransport | SSETransport
+to_transport(self) -> StreamableHttpTransport | SSETransport | FastMCPTransport
```
-### `TransformingRemoteMCPServer`
+### `TransformingRemoteMCPServer`
A Remote server with tool transforms.
-### `MCPConfig`
+### `MCPConfig`
A configuration object for MCP Servers that conforms to the canonical MCP configuration format
@@ -113,7 +113,7 @@ For an MCPConfig that is strictly canonical, see the `CanonicalMCPConfig` class.
**Methods:**
-#### `wrap_servers_at_root`
+#### `wrap_servers_at_root`
```python
wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any]
@@ -122,7 +122,7 @@ wrap_servers_at_root(cls, values: dict[str, Any]) -> dict[str, Any]
If there's no mcpServers key but there are server configs at root, wrap them.
-#### `add_server`
+#### `add_server`
```python
add_server(self, name: str, server: MCPServerTypes) -> None
@@ -131,7 +131,7 @@ add_server(self, name: str, server: MCPServerTypes) -> None
Add or update a server in the configuration.
-#### `from_dict`
+#### `from_dict`
```python
from_dict(cls, config: dict[str, Any]) -> Self
@@ -140,7 +140,7 @@ from_dict(cls, config: dict[str, Any]) -> Self
Parse MCP configuration from dictionary format.
-#### `to_dict`
+#### `to_dict`
```python
to_dict(self) -> dict[str, Any]
@@ -149,7 +149,7 @@ to_dict(self) -> dict[str, Any]
Convert MCPConfig to dictionary format, preserving all fields.
-#### `write_to_file`
+#### `write_to_file`
```python
write_to_file(self, file_path: Path) -> None
@@ -158,7 +158,7 @@ write_to_file(self, file_path: Path) -> None
Write configuration to JSON file.
-#### `from_file`
+#### `from_file`
```python
from_file(cls, file_path: Path) -> Self
@@ -167,7 +167,7 @@ from_file(cls, file_path: Path) -> Self
Load configuration from JSON file.
-### `CanonicalMCPConfig`
+### `CanonicalMCPConfig`
Canonical MCP configuration format.
@@ -178,7 +178,7 @@ The format is designed to be client-agnostic and extensible for future use cases
**Methods:**
-#### `add_server`
+#### `add_server`
```python
add_server(self, name: str, server: CanonicalMCPServerTypes) -> None
diff --git a/docs/python-sdk/fastmcp-prompts-__init__.mdx b/docs/python-sdk/fastmcp-prompts-__init__.mdx
deleted file mode 100644
index 8ef80b59e..000000000
--- a/docs/python-sdk/fastmcp-prompts-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.prompts`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-prompts-function_prompt.mdx b/docs/python-sdk/fastmcp-prompts-function_prompt.mdx
deleted file mode 100644
index a3222afc0..000000000
--- a/docs/python-sdk/fastmcp-prompts-function_prompt.mdx
+++ /dev/null
@@ -1,106 +0,0 @@
----
-title: function_prompt
-sidebarTitle: function_prompt
----
-
-# `fastmcp.prompts.function_prompt`
-
-
-Standalone @prompt decorator for FastMCP.
-
-## Functions
-
-### `prompt`
-
-```python
-prompt(name_or_fn: str | Callable[..., Any] | None = None) -> Any
-```
-
-
-Standalone decorator to mark a function as an MCP prompt.
-
-Returns the original function with metadata attached. Register with a server
-using mcp.add_prompt().
-
-
-## Classes
-
-### `DecoratedPrompt`
-
-
-Protocol for functions decorated with @prompt.
-
-
-### `PromptMeta`
-
-
-Metadata attached to functions by the @prompt decorator.
-
-
-### `FunctionPrompt`
-
-
-A prompt that is a function.
-
-
-**Methods:**
-
-#### `from_function`
-
-```python
-from_function(cls, fn: Callable[..., Any]) -> FunctionPrompt
-```
-
-Create a Prompt from a function.
-
-**Args:**
-- `fn`: The function to wrap
-- `metadata`: PromptMeta object with all configuration. If provided,
-individual parameters must not be passed.
-- `name, title, etc.`: Individual parameters for backwards compatibility.
-Cannot be used together with metadata parameter.
-
-The function can return:
-- str: wrapped as single user Message
-- list\[Message | str]: converted to list\[Message]
-- PromptResult: used directly
-
-
-#### `render`
-
-```python
-render(self, arguments: dict[str, Any] | None = None) -> PromptResult
-```
-
-Render the prompt with arguments.
-
-
-#### `register_with_docket`
-
-```python
-register_with_docket(self, docket: Docket) -> None
-```
-
-Register this prompt with docket for background execution.
-
-FunctionPrompt registers the underlying function, which has the user's
-Depends parameters for docket to resolve.
-
-
-#### `add_to_docket`
-
-```python
-add_to_docket(self, docket: Docket, arguments: dict[str, Any] | None, **kwargs: Any) -> Execution
-```
-
-Schedule this prompt for background execution via docket.
-
-FunctionPrompt splats the arguments dict since .fn expects **kwargs.
-
-**Args:**
-- `docket`: The Docket instance
-- `arguments`: Prompt arguments
-- `fn_key`: Function lookup key in Docket registry (defaults to self.key)
-- `task_key`: Redis storage key for the result
-- `**kwargs`: Additional kwargs passed to docket.add()
-
diff --git a/docs/python-sdk/fastmcp-prompts-prompt.mdx b/docs/python-sdk/fastmcp-prompts-prompt.mdx
deleted file mode 100644
index 2fe759342..000000000
--- a/docs/python-sdk/fastmcp-prompts-prompt.mdx
+++ /dev/null
@@ -1,145 +0,0 @@
----
-title: prompt
-sidebarTitle: prompt
----
-
-# `fastmcp.prompts.prompt`
-
-
-Base classes for FastMCP prompts.
-
-## Classes
-
-### `Message`
-
-
-Wrapper for prompt message with auto-serialization.
-
-Accepts any content - strings pass through, other types
-(dict, list, BaseModel) are JSON-serialized to text.
-
-
-**Methods:**
-
-#### `to_mcp_prompt_message`
-
-```python
-to_mcp_prompt_message(self) -> PromptMessage
-```
-
-Convert to MCP PromptMessage.
-
-
-### `PromptArgument`
-
-
-An argument that can be passed to a prompt.
-
-
-### `PromptResult`
-
-
-Canonical result type for prompt rendering.
-
-Provides explicit control over prompt responses: multiple messages,
-roles, and metadata at both the message and result level.
-
-
-**Methods:**
-
-#### `to_mcp_prompt_result`
-
-```python
-to_mcp_prompt_result(self) -> GetPromptResult
-```
-
-Convert to MCP GetPromptResult.
-
-
-### `Prompt`
-
-
-A prompt template that can be rendered with parameters.
-
-
-**Methods:**
-
-#### `to_mcp_prompt`
-
-```python
-to_mcp_prompt(self, **overrides: Any) -> SDKPrompt
-```
-
-Convert the prompt to an MCP prompt.
-
-
-#### `from_function`
-
-```python
-from_function(cls, fn: Callable[..., Any]) -> FunctionPrompt
-```
-
-Create a Prompt from a function.
-
-The function can return:
-- str: wrapped as single user Message
-- list\[Message | str]: converted to list\[Message]
-- PromptResult: used directly
-
-
-#### `render`
-
-```python
-render(self, arguments: dict[str, Any] | None = None) -> str | list[Message | str] | PromptResult
-```
-
-Render the prompt with arguments.
-
-Subclasses must implement this method. Return one of:
-- str: Wrapped as single user Message
-- list\[Message | str]: Converted to list\[Message]
-- PromptResult: Used directly
-
-
-#### `convert_result`
-
-```python
-convert_result(self, raw_value: Any) -> PromptResult
-```
-
-Convert a raw return value to PromptResult.
-
-**Raises:**
-- `TypeError`: for unsupported types
-
-
-#### `register_with_docket`
-
-```python
-register_with_docket(self, docket: Docket) -> None
-```
-
-Register this prompt with docket for background execution.
-
-
-#### `add_to_docket`
-
-```python
-add_to_docket(self, docket: Docket, arguments: dict[str, Any] | None, **kwargs: Any) -> Execution
-```
-
-Schedule this prompt for background execution via docket.
-
-**Args:**
-- `docket`: The Docket instance
-- `arguments`: Prompt arguments
-- `fn_key`: Function lookup key in Docket registry (defaults to self.key)
-- `task_key`: Redis storage key for the result
-- `**kwargs`: Additional kwargs passed to docket.add()
-
-
-#### `get_span_attributes`
-
-```python
-get_span_attributes(self) -> dict[str, Any]
-```
diff --git a/docs/python-sdk/fastmcp-resources-__init__.mdx b/docs/python-sdk/fastmcp-resources-__init__.mdx
deleted file mode 100644
index cc5fd2786..000000000
--- a/docs/python-sdk/fastmcp-resources-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.resources`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-resources-function_resource.mdx b/docs/python-sdk/fastmcp-resources-function_resource.mdx
deleted file mode 100644
index 3a7d346e1..000000000
--- a/docs/python-sdk/fastmcp-resources-function_resource.mdx
+++ /dev/null
@@ -1,93 +0,0 @@
----
-title: function_resource
-sidebarTitle: function_resource
----
-
-# `fastmcp.resources.function_resource`
-
-
-Standalone @resource decorator for FastMCP.
-
-## Functions
-
-### `resource`
-
-```python
-resource(uri: str) -> Callable[[F], F]
-```
-
-
-Standalone decorator to mark a function as an MCP resource.
-
-Returns the original function with metadata attached. Register with a server
-using mcp.add_resource().
-
-
-## Classes
-
-### `DecoratedResource`
-
-
-Protocol for functions decorated with @resource.
-
-
-### `ResourceMeta`
-
-
-Metadata attached to functions by the @resource decorator.
-
-
-### `FunctionResource`
-
-
-A resource that defers data loading by wrapping a function.
-
-The function is only called when the resource is read, allowing for lazy loading
-of potentially expensive data. This is particularly useful when listing resources,
-as the function won't be called until the resource is actually accessed.
-
-The function can return:
-- str for text content (default)
-- bytes for binary content
-- other types will be converted to JSON
-
-
-**Methods:**
-
-#### `from_function`
-
-```python
-from_function(cls, fn: Callable[..., Any], uri: str | AnyUrl | None = None) -> FunctionResource
-```
-
-Create a FunctionResource from a function.
-
-**Args:**
-- `fn`: The function to wrap
-- `uri`: The URI for the resource (required if metadata not provided)
-- `metadata`: ResourceMeta object with all configuration. If provided,
-individual parameters must not be passed.
-- `name, title, etc.`: Individual parameters for backwards compatibility.
-Cannot be used together with metadata parameter.
-
-
-#### `read`
-
-```python
-read(self) -> str | bytes | ResourceResult
-```
-
-Read the resource by calling the wrapped function.
-
-
-#### `register_with_docket`
-
-```python
-register_with_docket(self, docket: Docket) -> None
-```
-
-Register this resource with docket for background execution.
-
-FunctionResource registers the underlying function, which has the user's
-Depends parameters for docket to resolve.
-
diff --git a/docs/python-sdk/fastmcp-resources-resource.mdx b/docs/python-sdk/fastmcp-resources-resource.mdx
deleted file mode 100644
index 029b48700..000000000
--- a/docs/python-sdk/fastmcp-resources-resource.mdx
+++ /dev/null
@@ -1,180 +0,0 @@
----
-title: resource
-sidebarTitle: resource
----
-
-# `fastmcp.resources.resource`
-
-
-Base classes and interfaces for FastMCP resources.
-
-## Classes
-
-### `ResourceContent`
-
-
-Wrapper for resource content with optional MIME type and metadata.
-
-Accepts any value for content - strings and bytes pass through directly,
-other types (dict, list, BaseModel, etc.) are automatically JSON-serialized.
-
-
-**Methods:**
-
-#### `to_mcp_resource_contents`
-
-```python
-to_mcp_resource_contents(self, uri: AnyUrl | str) -> mcp.types.TextResourceContents | mcp.types.BlobResourceContents
-```
-
-Convert to MCP resource contents type.
-
-**Args:**
-- `uri`: The URI of the resource (required by MCP types)
-
-**Returns:**
-- TextResourceContents for str content, BlobResourceContents for bytes
-
-
-### `ResourceResult`
-
-
-Canonical result type for resource reads.
-
-Provides explicit control over resource responses: multiple content items,
-per-item MIME types, and metadata at both the item and result level.
-
-
-**Methods:**
-
-#### `to_mcp_result`
-
-```python
-to_mcp_result(self, uri: AnyUrl | str) -> mcp.types.ReadResourceResult
-```
-
-Convert to MCP ReadResourceResult.
-
-**Args:**
-- `uri`: The URI of the resource (required by MCP types)
-
-**Returns:**
-- MCP ReadResourceResult with converted contents
-
-
-### `Resource`
-
-
-Base class for all resources.
-
-
-**Methods:**
-
-#### `from_function`
-
-```python
-from_function(cls, fn: Callable[..., Any], uri: str | AnyUrl) -> FunctionResource
-```
-
-#### `set_default_mime_type`
-
-```python
-set_default_mime_type(cls, mime_type: str | None) -> str
-```
-
-Set default MIME type if not provided.
-
-
-#### `set_default_name`
-
-```python
-set_default_name(self) -> Self
-```
-
-Set default name from URI if not provided.
-
-
-#### `read`
-
-```python
-read(self) -> str | bytes | ResourceResult
-```
-
-Read the resource content.
-
-Subclasses implement this to return resource data. Supported return types:
- - str: Text content
- - bytes: Binary content
- - ResourceResult: Full control over contents and result-level meta
-
-
-#### `convert_result`
-
-```python
-convert_result(self, raw_value: Any) -> ResourceResult
-```
-
-Convert a raw result to ResourceResult.
-
-This is used in two contexts:
-1. In _read() to convert user function return values to ResourceResult
-2. In tasks_result_handler() to convert Docket task results to ResourceResult
-
-Handles ResourceResult passthrough and converts raw values using
-ResourceResult's normalization. When the raw value is a plain
-string or bytes, the resource's own ``mime_type`` is forwarded so
-that ``ui://`` resources (and others with non-default MIME types)
-don't fall back to ``text/plain``.
-
-The resource's component-level ``meta`` (e.g. ``ui`` metadata for
-MCP Apps CSP/permissions) is propagated to each content item so
-that hosts can read it from the ``resources/read`` response.
-
-
-#### `to_mcp_resource`
-
-```python
-to_mcp_resource(self, **overrides: Any) -> SDKResource
-```
-
-Convert the resource to an SDKResource.
-
-
-#### `key`
-
-```python
-key(self) -> str
-```
-
-The globally unique lookup key for this resource.
-
-
-#### `register_with_docket`
-
-```python
-register_with_docket(self, docket: Docket) -> None
-```
-
-Register this resource with docket for background execution.
-
-
-#### `add_to_docket`
-
-```python
-add_to_docket(self, docket: Docket, **kwargs: Any) -> Execution
-```
-
-Schedule this resource for background execution via docket.
-
-**Args:**
-- `docket`: The Docket instance
-- `fn_key`: Function lookup key in Docket registry (defaults to self.key)
-- `task_key`: Redis storage key for the result
-- `**kwargs`: Additional kwargs passed to docket.add()
-
-
-#### `get_span_attributes`
-
-```python
-get_span_attributes(self) -> dict[str, Any]
-```
diff --git a/docs/python-sdk/fastmcp-resources-template.mdx b/docs/python-sdk/fastmcp-resources-template.mdx
deleted file mode 100644
index 89e51c22f..000000000
--- a/docs/python-sdk/fastmcp-resources-template.mdx
+++ /dev/null
@@ -1,244 +0,0 @@
----
-title: template
-sidebarTitle: template
----
-
-# `fastmcp.resources.template`
-
-
-Resource template functionality.
-
-## Functions
-
-### `extract_query_params`
-
-```python
-extract_query_params(uri_template: str) -> set[str]
-```
-
-
-Extract query parameter names from RFC 6570 `{?param1,param2}` syntax.
-
-
-### `build_regex`
-
-```python
-build_regex(template: str) -> re.Pattern
-```
-
-
-Build regex pattern for URI template, handling RFC 6570 syntax.
-
-Supports:
-- `{var}` - simple path parameter
-- `{var*}` - wildcard path parameter (captures multiple segments)
-- `{?var1,var2}` - query parameters (ignored in path matching)
-
-
-### `match_uri_template`
-
-```python
-match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None
-```
-
-
-Match URI against template and extract both path and query parameters.
-
-Supports RFC 6570 URI templates:
-- Path params: `{var}`, `{var*}`
-- Query params: `{?var1,var2}`
-
-
-## Classes
-
-### `ResourceTemplate`
-
-
-A template for dynamically creating resources.
-
-
-**Methods:**
-
-#### `from_function`
-
-```python
-from_function(fn: Callable[..., Any], uri_template: str, name: str | None = None, version: str | int | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, mime_type: str | None = None, tags: set[str] | None = None, annotations: Annotations | None = None, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, auth: AuthCheck | list[AuthCheck] | None = None) -> FunctionResourceTemplate
-```
-
-#### `set_default_mime_type`
-
-```python
-set_default_mime_type(cls, mime_type: str | None) -> str
-```
-
-Set default MIME type if not provided.
-
-
-#### `matches`
-
-```python
-matches(self, uri: str) -> dict[str, Any] | None
-```
-
-Check if URI matches template and extract parameters.
-
-
-#### `read`
-
-```python
-read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult
-```
-
-Read the resource content.
-
-
-#### `convert_result`
-
-```python
-convert_result(self, raw_value: Any) -> ResourceResult
-```
-
-Convert a raw result to ResourceResult.
-
-This is used in two contexts:
-1. In _read() to convert user function return values to ResourceResult
-2. In tasks_result_handler() to convert Docket task results to ResourceResult
-
-Handles ResourceResult passthrough and converts raw values using
-ResourceResult's normalization.
-
-
-#### `create_resource`
-
-```python
-create_resource(self, uri: str, params: dict[str, Any]) -> Resource
-```
-
-Create a resource from the template with the given parameters.
-
-The base implementation does not support background tasks.
-Use FunctionResourceTemplate for task support.
-
-
-#### `to_mcp_template`
-
-```python
-to_mcp_template(self, **overrides: Any) -> SDKResourceTemplate
-```
-
-Convert the resource template to an SDKResourceTemplate.
-
-
-#### `from_mcp_template`
-
-```python
-from_mcp_template(cls, mcp_template: SDKResourceTemplate) -> ResourceTemplate
-```
-
-Creates a FastMCP ResourceTemplate from a raw MCP ResourceTemplate object.
-
-
-#### `key`
-
-```python
-key(self) -> str
-```
-
-The globally unique lookup key for this template.
-
-
-#### `register_with_docket`
-
-```python
-register_with_docket(self, docket: Docket) -> None
-```
-
-Register this template with docket for background execution.
-
-
-#### `add_to_docket`
-
-```python
-add_to_docket(self, docket: Docket, params: dict[str, Any], **kwargs: Any) -> Execution
-```
-
-Schedule this template for background execution via docket.
-
-**Args:**
-- `docket`: The Docket instance
-- `params`: Template parameters
-- `fn_key`: Function lookup key in Docket registry (defaults to self.key)
-- `task_key`: Redis storage key for the result
-- `**kwargs`: Additional kwargs passed to docket.add()
-
-
-#### `get_span_attributes`
-
-```python
-get_span_attributes(self) -> dict[str, Any]
-```
-
-### `FunctionResourceTemplate`
-
-
-A template for dynamically creating resources.
-
-
-**Methods:**
-
-#### `create_resource`
-
-```python
-create_resource(self, uri: str, params: dict[str, Any]) -> Resource
-```
-
-Create a resource from the template with the given parameters.
-
-
-#### `read`
-
-```python
-read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult
-```
-
-Read the resource content.
-
-
-#### `register_with_docket`
-
-```python
-register_with_docket(self, docket: Docket) -> None
-```
-
-Register this template with docket for background execution.
-
-FunctionResourceTemplate registers the underlying function, which has the
-user's Depends parameters for docket to resolve.
-
-
-#### `add_to_docket`
-
-```python
-add_to_docket(self, docket: Docket, params: dict[str, Any], **kwargs: Any) -> Execution
-```
-
-Schedule this template for background execution via docket.
-
-FunctionResourceTemplate splats the params dict since .fn expects **kwargs.
-
-**Args:**
-- `docket`: The Docket instance
-- `params`: Template parameters
-- `fn_key`: Function lookup key in Docket registry (defaults to self.key)
-- `task_key`: Redis storage key for the result
-- `**kwargs`: Additional kwargs passed to docket.add()
-
-
-#### `from_function`
-
-```python
-from_function(cls, fn: Callable[..., Any], uri_template: str, name: str | None = None, version: str | int | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, mime_type: str | None = None, tags: set[str] | None = None, annotations: Annotations | None = None, meta: dict[str, Any] | None = None, task: bool | TaskConfig | None = None, auth: AuthCheck | list[AuthCheck] | None = None) -> FunctionResourceTemplate
-```
-
-Create a template from a function.
-
diff --git a/docs/python-sdk/fastmcp-resources-types.mdx b/docs/python-sdk/fastmcp-resources-types.mdx
deleted file mode 100644
index d19b24eb2..000000000
--- a/docs/python-sdk/fastmcp-resources-types.mdx
+++ /dev/null
@@ -1,134 +0,0 @@
----
-title: types
-sidebarTitle: types
----
-
-# `fastmcp.resources.types`
-
-
-Concrete resource implementations.
-
-## Classes
-
-### `TextResource`
-
-
-A resource that reads from a string.
-
-
-**Methods:**
-
-#### `read`
-
-```python
-read(self) -> ResourceResult
-```
-
-Read the text content.
-
-
-### `BinaryResource`
-
-
-A resource that reads from bytes.
-
-
-**Methods:**
-
-#### `read`
-
-```python
-read(self) -> ResourceResult
-```
-
-Read the binary content.
-
-
-### `FileResource`
-
-
-A resource that reads from a file.
-
-Set is_binary=True to read file as binary data instead of text.
-
-
-**Methods:**
-
-#### `validate_absolute_path`
-
-```python
-validate_absolute_path(cls, path: Path) -> Path
-```
-
-Ensure path is absolute.
-
-
-#### `set_binary_from_mime_type`
-
-```python
-set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool
-```
-
-Set is_binary based on mime_type if not explicitly set.
-
-
-#### `read`
-
-```python
-read(self) -> ResourceResult
-```
-
-Read the file content.
-
-
-### `HttpResource`
-
-
-A resource that reads from an HTTP endpoint.
-
-
-**Methods:**
-
-#### `read`
-
-```python
-read(self) -> ResourceResult
-```
-
-Read the HTTP content.
-
-
-### `DirectoryResource`
-
-
-A resource that lists files in a directory.
-
-
-**Methods:**
-
-#### `validate_absolute_path`
-
-```python
-validate_absolute_path(cls, path: Path) -> Path
-```
-
-Ensure path is absolute.
-
-
-#### `list_files`
-
-```python
-list_files(self) -> list[Path]
-```
-
-List files in the directory.
-
-
-#### `read`
-
-```python
-read(self) -> ResourceResult
-```
-
-Read the directory listing.
-
diff --git a/docs/python-sdk/fastmcp-server-__init__.mdx b/docs/python-sdk/fastmcp-server-__init__.mdx
deleted file mode 100644
index 157a018ce..000000000
--- a/docs/python-sdk/fastmcp-server-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.server`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-server-apps.mdx b/docs/python-sdk/fastmcp-server-apps.mdx
deleted file mode 100644
index 00e53eafe..000000000
--- a/docs/python-sdk/fastmcp-server-apps.mdx
+++ /dev/null
@@ -1,86 +0,0 @@
----
-title: apps
-sidebarTitle: apps
----
-
-# `fastmcp.server.apps`
-
-
-MCP Apps support — extension negotiation and typed UI metadata models.
-
-Provides constants and Pydantic models for the MCP Apps extension
-(io.modelcontextprotocol/ui), enabling tools and resources to carry
-UI metadata for clients that support interactive app rendering.
-
-
-## Functions
-
-### `app_config_to_meta_dict`
-
-```python
-app_config_to_meta_dict(app: AppConfig | dict[str, Any]) -> dict[str, Any]
-```
-
-
-Convert an AppConfig or dict to the wire-format dict for ``meta["ui"]``.
-
-
-### `resolve_ui_mime_type`
-
-```python
-resolve_ui_mime_type(uri: str, explicit_mime_type: str | None) -> str | None
-```
-
-
-Return the appropriate MIME type for a resource URI.
-
-For ``ui://`` scheme resources, defaults to ``UI_MIME_TYPE`` when no
-explicit MIME type is provided. This ensures UI resources are correctly
-identified regardless of how they're registered (via FastMCP.resource,
-the standalone @resource decorator, or resource templates).
-
-**Args:**
-- `uri`: The resource URI string
-- `explicit_mime_type`: The MIME type explicitly provided by the user
-
-**Returns:**
-- The resolved MIME type (explicit value, UI default, or None)
-
-
-## Classes
-
-### `ResourceCSP`
-
-
-Content Security Policy for MCP App resources.
-
-Declares which external origins the app is allowed to connect to or
-load resources from. Hosts use these declarations to build the
-``Content-Security-Policy`` header for the sandboxed iframe.
-
-
-### `ResourcePermissions`
-
-
-Iframe sandbox permissions for MCP App resources.
-
-Each field, when set (typically to ``{}``), requests that the host
-grant the corresponding Permission Policy feature to the sandboxed
-iframe. Hosts MAY honour these; apps should use JS feature detection
-as a fallback.
-
-
-### `AppConfig`
-
-
-Configuration for MCP App tools and resources.
-
-Controls how a tool or resource participates in the MCP Apps extension.
-On tools, ``resource_uri`` and ``visibility`` specify which UI resource
-to render and where the tool appears. On resources, those fields must
-be left unset (the resource itself is the UI).
-
-All fields use ``exclude_none`` serialization so only explicitly-set
-values appear on the wire. Aliases match the MCP Apps wire format
-(camelCase).
-
diff --git a/docs/python-sdk/fastmcp-server-auth-__init__.mdx b/docs/python-sdk/fastmcp-server-auth-__init__.mdx
deleted file mode 100644
index c86f07005..000000000
--- a/docs/python-sdk/fastmcp-server-auth-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.server.auth`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-server-auth-auth.mdx b/docs/python-sdk/fastmcp-server-auth-auth.mdx
deleted file mode 100644
index 2186df875..000000000
--- a/docs/python-sdk/fastmcp-server-auth-auth.mdx
+++ /dev/null
@@ -1,382 +0,0 @@
----
-title: auth
-sidebarTitle: auth
----
-
-# `fastmcp.server.auth.auth`
-
-## Classes
-
-### `AccessToken`
-
-
-AccessToken that includes all JWT claims.
-
-
-### `TokenHandler`
-
-
-TokenHandler that returns MCP-compliant error responses.
-
-This handler addresses two SDK issues:
-
-1. Error code: The SDK returns `unauthorized_client` for client authentication
- failures, but RFC 6749 Section 5.2 requires `invalid_client` with HTTP 401.
- This distinction matters for client re-registration behavior.
-
-2. Status code: The SDK returns HTTP 400 for all token errors including
- `invalid_grant` (expired/invalid tokens). However, the MCP spec requires:
- "Invalid or expired tokens MUST receive a HTTP 401 response."
-
-This handler transforms responses to be compliant with both OAuth 2.1 and MCP specs.
-
-
-**Methods:**
-
-#### `handle`
-
-```python
-handle(self, request: Any)
-```
-
-Wrap SDK handle() and transform auth error responses.
-
-
-### `PrivateKeyJWTClientAuthenticator`
-
-
-Client authenticator with private_key_jwt support for CIMD clients.
-
-Extends the SDK's ClientAuthenticator to add support for the `private_key_jwt`
-authentication method per RFC 7523. This is required for CIMD (Client ID Metadata
-Document) clients that use asymmetric keys for authentication.
-
-The authenticator:
-1. Delegates to SDK for standard methods (client_secret_basic, client_secret_post, none)
-2. Adds private_key_jwt handling for CIMD clients
-3. Validates JWT assertions against client's JWKS
-
-
-**Methods:**
-
-#### `authenticate_request`
-
-```python
-authenticate_request(self, request: Request) -> OAuthClientInformationFull
-```
-
-Authenticate a client from an HTTP request.
-
-Extends SDK authentication to support private_key_jwt for CIMD clients.
-Delegates to SDK for client_secret_basic (Authorization header) and
-client_secret_post (form body) authentication.
-
-
-### `AuthProvider`
-
-
-Base class for all FastMCP authentication providers.
-
-This class provides a unified interface for all authentication providers,
-whether they are simple token verifiers or full OAuth authorization servers.
-All providers must be able to verify tokens and can optionally provide
-custom authentication routes.
-
-
-**Methods:**
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify a bearer token and return access info if valid.
-
-All auth providers must implement token verification.
-
-**Args:**
-- `token`: The token string to validate
-
-**Returns:**
-- AccessToken object if valid, None if invalid or expired
-
-
-#### `set_mcp_path`
-
-```python
-set_mcp_path(self, mcp_path: str | None) -> None
-```
-
-Set the MCP endpoint path and compute resource URL.
-
-This method is called by get_routes() to configure the expected
-resource URL before route creation. Subclasses can override to
-perform additional initialization that depends on knowing the
-MCP endpoint path.
-
-**Args:**
-- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
-
-
-#### `get_routes`
-
-```python
-get_routes(self, mcp_path: str | None = None) -> list[Route]
-```
-
-Get all routes for this authentication provider.
-
-This includes both well-known discovery routes and operational routes.
-Each provider is responsible for creating whatever routes it needs:
-- TokenVerifier: typically no routes (default implementation)
-- RemoteAuthProvider: protected resource metadata routes
-- OAuthProvider: full OAuth authorization server routes
-- Custom providers: whatever routes they need
-
-**Args:**
-- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
-This is used to advertise the resource URL in metadata, but the
-provider does not create the actual MCP endpoint route.
-
-**Returns:**
-- List of all routes for this provider (excluding the MCP endpoint itself)
-
-
-#### `get_well_known_routes`
-
-```python
-get_well_known_routes(self, mcp_path: str | None = None) -> list[Route]
-```
-
-Get well-known discovery routes for this authentication provider.
-
-This is a utility method that filters get_routes() to return only
-well-known discovery routes (those starting with /.well-known/).
-
-Well-known routes provide OAuth metadata and discovery endpoints that
-clients use to discover authentication capabilities. These routes should
-be mounted at the root level of the application to comply with RFC 8414
-and RFC 9728.
-
-Common well-known routes:
-- /.well-known/oauth-authorization-server (authorization server metadata)
-- /.well-known/oauth-protected-resource/* (protected resource metadata)
-
-**Args:**
-- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
-This is used to construct path-scoped well-known URLs.
-
-**Returns:**
-- List of well-known discovery routes (typically mounted at root level)
-
-
-#### `get_middleware`
-
-```python
-get_middleware(self) -> list
-```
-
-Get HTTP application-level middleware for this auth provider.
-
-**Returns:**
-- List of Starlette Middleware instances to apply to the HTTP app
-
-
-### `TokenVerifier`
-
-
-Base class for token verifiers (Resource Servers).
-
-This class provides token verification capability without OAuth server functionality.
-Token verifiers typically don't provide authentication routes by default.
-
-
-**Methods:**
-
-#### `scopes_supported`
-
-```python
-scopes_supported(self) -> list[str]
-```
-
-Scopes to advertise in OAuth metadata.
-
-Defaults to required_scopes. Override in subclasses when the
-advertised scopes differ from the validation scopes (e.g., Azure AD
-where tokens contain short-form scopes but clients request full URI
-scopes).
-
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify a bearer token and return access info if valid.
-
-
-### `RemoteAuthProvider`
-
-
-Authentication provider for resource servers that verify tokens from known authorization servers.
-
-This provider composes a TokenVerifier with authorization server metadata to create
-standardized OAuth 2.0 Protected Resource endpoints (RFC 9728). Perfect for:
-- JWT verification with known issuers
-- Remote token introspection services
-- Any resource server that knows where its tokens come from
-
-Use this when you have token verification logic and want to advertise
-the authorization servers that issue valid tokens.
-
-
-**Methods:**
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify token using the configured token verifier.
-
-
-#### `get_routes`
-
-```python
-get_routes(self, mcp_path: str | None = None) -> list[Route]
-```
-
-Get routes for this provider.
-
-Creates protected resource metadata routes (RFC 9728).
-
-
-### `MultiAuth`
-
-
-Composes an optional auth server with additional token verifiers.
-
-Use this when a single server needs to accept tokens from multiple sources.
-For example, an OAuth proxy for interactive clients combined with a JWT
-verifier for machine-to-machine tokens.
-
-Token verification tries the server first (if present), then each verifier
-in order, returning the first successful result. Routes and OAuth metadata
-come from the server; verifiers contribute only token verification.
-
-
-**Methods:**
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify a token by trying the server, then each verifier in order.
-
-Each source is tried independently. If a source raises an exception,
-it is logged and treated as a non-match so that remaining sources
-still get a chance to verify the token.
-
-
-#### `set_mcp_path`
-
-```python
-set_mcp_path(self, mcp_path: str | None) -> None
-```
-
-Propagate MCP path to the server and all verifiers.
-
-
-#### `get_routes`
-
-```python
-get_routes(self, mcp_path: str | None = None) -> list[Route]
-```
-
-Delegate route creation to the server.
-
-
-#### `get_well_known_routes`
-
-```python
-get_well_known_routes(self, mcp_path: str | None = None) -> list[Route]
-```
-
-Delegate well-known route creation to the server.
-
-This ensures that server-specific well-known route logic (e.g.,
-OAuthProvider's RFC 8414 path-aware discovery) is preserved.
-
-
-### `OAuthProvider`
-
-
-OAuth Authorization Server provider.
-
-This class provides full OAuth server functionality including client registration,
-authorization flows, token issuance, and token verification.
-
-
-**Methods:**
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify a bearer token and return access info if valid.
-
-This method implements the TokenVerifier protocol by delegating
-to our existing load_access_token method.
-
-**Args:**
-- `token`: The token string to validate
-
-**Returns:**
-- AccessToken object if valid, None if invalid or expired
-
-
-#### `get_routes`
-
-```python
-get_routes(self, mcp_path: str | None = None) -> list[Route]
-```
-
-Get OAuth authorization server routes and optional protected resource routes.
-
-This method creates the full set of OAuth routes including:
-- Standard OAuth authorization server routes (/.well-known/oauth-authorization-server, /authorize, /token, etc.)
-- Optional protected resource routes
-
-**Returns:**
-- List of OAuth routes
-
-
-#### `get_well_known_routes`
-
-```python
-get_well_known_routes(self, mcp_path: str | None = None) -> list[Route]
-```
-
-Get well-known discovery routes with RFC 8414 path-aware support.
-
-Overrides the base implementation to support path-aware authorization
-server metadata discovery per RFC 8414. If issuer_url has a path component,
-the authorization server metadata route is adjusted to include that path.
-
-For example, if issuer_url is "http://example.com/api", the discovery
-endpoint will be at "/.well-known/oauth-authorization-server/api" instead
-of just "/.well-known/oauth-authorization-server".
-
-**Args:**
-- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
-
-**Returns:**
-- List of well-known discovery routes
-
diff --git a/docs/python-sdk/fastmcp-server-auth-authorization.mdx b/docs/python-sdk/fastmcp-server-auth-authorization.mdx
deleted file mode 100644
index 7465c43dc..000000000
--- a/docs/python-sdk/fastmcp-server-auth-authorization.mdx
+++ /dev/null
@@ -1,129 +0,0 @@
----
-title: authorization
-sidebarTitle: authorization
----
-
-# `fastmcp.server.auth.authorization`
-
-
-Authorization checks for FastMCP components.
-
-This module provides callable-based authorization for tools, resources, and prompts.
-Auth checks are functions that receive an AuthContext and return True to allow access
-or False to deny.
-
-Auth checks can also raise exceptions:
-- AuthorizationError: Propagates with the custom message for explicit denial
-- Other exceptions: Masked for security (logged, treated as auth failure)
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth import require_scopes
-
- mcp = FastMCP()
-
- @mcp.tool(auth=require_scopes("write"))
- def protected_tool(): ...
-
- @mcp.resource("data://secret", auth=require_scopes("read"))
- def secret_data(): ...
-
- @mcp.prompt(auth=require_scopes("admin"))
- def admin_prompt(): ...
- ```
-
-
-## Functions
-
-### `require_scopes`
-
-```python
-require_scopes(*scopes: str) -> AuthCheck
-```
-
-
-Require specific OAuth scopes.
-
-Returns an auth check that requires ALL specified scopes to be present
-in the token (AND logic).
-
-**Args:**
-- `*scopes`: One or more scope strings that must all be present.
-
-
-### `restrict_tag`
-
-```python
-restrict_tag(tag: str) -> AuthCheck
-```
-
-
-Restrict components with a specific tag to require certain scopes.
-
-If the component has the specified tag, the token must have ALL the
-required scopes. If the component doesn't have the tag, access is allowed.
-
-**Args:**
-- `tag`: The tag that triggers the scope requirement.
-- `scopes`: List of scopes required when the tag is present.
-
-
-### `run_auth_checks`
-
-```python
-run_auth_checks(checks: AuthCheck | list[AuthCheck], ctx: AuthContext) -> bool
-```
-
-
-Run auth checks with AND logic.
-
-All checks must pass for authorization to succeed. Checks can be
-synchronous or asynchronous functions.
-
-Auth checks can:
-- Return True to allow access
-- Return False to deny access
-- Raise AuthorizationError to deny with a custom message (propagates)
-- Raise other exceptions (masked for security, treated as denial)
-
-**Args:**
-- `checks`: A single check function or list of check functions.
-Each check can be sync (returns bool) or async (returns Awaitable[bool]).
-- `ctx`: The auth context to pass to each check.
-
-**Returns:**
-- True if all checks pass, False if any check fails.
-
-**Raises:**
-- `AuthorizationError`: If an auth check explicitly raises it.
-
-
-## Classes
-
-### `AuthContext`
-
-
-Context passed to auth check callables.
-
-This object is passed to each auth check function and provides
-access to the current authentication token and the component being accessed.
-
-**Attributes:**
-- `token`: The current access token, or None if unauthenticated.
-- `component`: The component (tool, resource, or prompt) being accessed.
-- `tool`: Backwards-compatible alias for component when it's a Tool.
-
-
-**Methods:**
-
-#### `tool`
-
-```python
-tool(self) -> Tool | None
-```
-
-Backwards-compatible access to the component as a Tool.
-
-Returns the component if it's a Tool, None otherwise.
-
diff --git a/docs/python-sdk/fastmcp-server-auth-cimd.mdx b/docs/python-sdk/fastmcp-server-auth-cimd.mdx
deleted file mode 100644
index dda5d28c6..000000000
--- a/docs/python-sdk/fastmcp-server-auth-cimd.mdx
+++ /dev/null
@@ -1,242 +0,0 @@
----
-title: cimd
-sidebarTitle: cimd
----
-
-# `fastmcp.server.auth.cimd`
-
-
-CIMD (Client ID Metadata Document) support for FastMCP.
-
-.. warning::
- **Beta Feature**: CIMD support is currently in beta. The API may change
- in future releases. Please report any issues you encounter.
-
-CIMD is a simpler alternative to Dynamic Client Registration where clients
-host a static JSON document at an HTTPS URL, and that URL becomes their
-client_id. See the IETF draft: draft-parecki-oauth-client-id-metadata-document
-
-This module provides:
-- CIMDDocument: Pydantic model for CIMD document validation
-- CIMDFetcher: Fetch and validate CIMD documents with SSRF protection
-- CIMDClientManager: Manages CIMD client operations
-
-
-## Classes
-
-### `CIMDDocument`
-
-
-CIMD document per draft-parecki-oauth-client-id-metadata-document.
-
-The client metadata document is a JSON document containing OAuth client
-metadata. The client_id property MUST match the URL where this document
-is hosted.
-
-Key constraint: token_endpoint_auth_method MUST NOT use shared secrets
-(client_secret_post, client_secret_basic, client_secret_jwt).
-
-redirect_uris is required and must contain at least one entry.
-
-
-**Methods:**
-
-#### `validate_auth_method`
-
-```python
-validate_auth_method(cls, v: str) -> str
-```
-
-Ensure no shared-secret auth methods are used.
-
-
-#### `validate_redirect_uris`
-
-```python
-validate_redirect_uris(cls, v: list[str]) -> list[str]
-```
-
-Ensure redirect_uris is non-empty and each entry is a valid URI.
-
-
-### `CIMDValidationError`
-
-
-Raised when CIMD document validation fails.
-
-
-### `CIMDFetchError`
-
-
-Raised when CIMD document fetching fails.
-
-
-### `CIMDFetcher`
-
-
-Fetch and validate CIMD documents with SSRF protection.
-
-Delegates HTTP fetching to ssrf_safe_fetch_response, which provides DNS
-pinning, IP validation, size limits, and timeout enforcement. Documents are
-cached using HTTP caching semantics (Cache-Control/ETag/Last-Modified), with
-a TTL fallback when response headers do not define caching behavior.
-
-
-**Methods:**
-
-#### `is_cimd_client_id`
-
-```python
-is_cimd_client_id(self, client_id: str) -> bool
-```
-
-Check if a client_id looks like a CIMD URL.
-
-CIMD URLs must be HTTPS with a host and non-root path.
-
-
-#### `fetch`
-
-```python
-fetch(self, client_id_url: str) -> CIMDDocument
-```
-
-Fetch and validate a CIMD document with SSRF protection.
-
-Uses ssrf_safe_fetch_response for the HTTP layer, which provides:
-- HTTPS only, DNS resolution with IP validation
-- DNS pinning (connects to validated IP directly)
-- Blocks private/loopback/link-local/multicast IPs
-- Response size limit and timeout enforcement
-- Redirects disabled
-
-**Args:**
-- `client_id_url`: The URL to fetch (also the expected client_id)
-
-**Returns:**
-- Validated CIMDDocument
-
-**Raises:**
-- `CIMDValidationError`: If document is invalid or URL blocked
-- `CIMDFetchError`: If document cannot be fetched
-
-
-#### `validate_redirect_uri`
-
-```python
-validate_redirect_uri(self, doc: CIMDDocument, redirect_uri: str) -> bool
-```
-
-Validate that a redirect_uri is allowed by the CIMD document.
-
-**Args:**
-- `doc`: The CIMD document
-- `redirect_uri`: The redirect URI to validate
-
-**Returns:**
-- True if valid, False otherwise
-
-
-### `CIMDAssertionValidator`
-
-
-Validates JWT assertions for private_key_jwt CIMD clients.
-
-Implements RFC 7523 (JSON Web Token (JWT) Profile for OAuth 2.0 Client
-Authentication and Authorization Grants) for CIMD client authentication.
-
-JTI replay protection uses TTL-based caching to ensure proper security:
-- JTIs are cached with expiration matching the JWT's exp claim
-- Expired JTIs are automatically cleaned up
-- Maximum assertion lifetime is enforced (5 minutes)
-
-
-**Methods:**
-
-#### `validate_assertion`
-
-```python
-validate_assertion(self, assertion: str, client_id: str, token_endpoint: str, cimd_doc: CIMDDocument) -> bool
-```
-
-Validate JWT assertion from client.
-
-**Args:**
-- `assertion`: The JWT assertion string
-- `client_id`: Expected client_id (must match iss and sub claims)
-- `token_endpoint`: Token endpoint URL (must match aud claim)
-- `cimd_doc`: CIMD document containing JWKS for key verification
-
-**Returns:**
-- True if valid
-
-**Raises:**
-- `ValueError`: If validation fails
-
-
-### `CIMDClientManager`
-
-
-Manages all CIMD client operations for OAuth proxy.
-
-This class encapsulates:
-- CIMD client detection
-- Document fetching and validation
-- Synthetic OAuth client creation
-- Private key JWT assertion validation
-
-This allows the OAuth proxy to delegate all CIMD-specific logic to a
-single, focused manager class.
-
-
-**Methods:**
-
-#### `is_cimd_client_id`
-
-```python
-is_cimd_client_id(self, client_id: str) -> bool
-```
-
-Check if client_id is a CIMD URL.
-
-**Args:**
-- `client_id`: Client ID to check
-
-**Returns:**
-- True if client_id is an HTTPS URL (CIMD format)
-
-
-#### `get_client`
-
-```python
-get_client(self, client_id_url: str)
-```
-
-Fetch CIMD document and create synthetic OAuth client.
-
-**Args:**
-- `client_id_url`: HTTPS URL pointing to CIMD document
-
-**Returns:**
-- OAuthProxyClient with CIMD document attached, or None if fetch fails
-
-
-#### `validate_private_key_jwt`
-
-```python
-validate_private_key_jwt(self, assertion: str, client, token_endpoint: str) -> bool
-```
-
-Validate JWT assertion for private_key_jwt auth.
-
-**Args:**
-- `assertion`: JWT assertion string from client
-- `client`: OAuth proxy client (must have cimd_document)
-- `token_endpoint`: Token endpoint URL for aud validation
-
-**Returns:**
-- True if assertion is valid
-
-**Raises:**
-- `ValueError`: If client doesn't have CIMD document or validation fails
-
diff --git a/docs/python-sdk/fastmcp-server-auth-jwt_issuer.mdx b/docs/python-sdk/fastmcp-server-auth-jwt_issuer.mdx
deleted file mode 100644
index b30f3090b..000000000
--- a/docs/python-sdk/fastmcp-server-auth-jwt_issuer.mdx
+++ /dev/null
@@ -1,106 +0,0 @@
----
-title: jwt_issuer
-sidebarTitle: jwt_issuer
----
-
-# `fastmcp.server.auth.jwt_issuer`
-
-
-JWT token issuance and verification for FastMCP OAuth Proxy.
-
-This module implements the token factory pattern for OAuth proxies, where the proxy
-issues its own JWT tokens to clients instead of forwarding upstream provider tokens.
-This maintains proper OAuth 2.0 token audience boundaries.
-
-
-## Functions
-
-### `derive_jwt_key`
-
-```python
-derive_jwt_key() -> bytes
-```
-
-
-Derive JWT signing key from a high-entropy or low-entropy key material and server salt.
-
-
-## Classes
-
-### `JWTIssuer`
-
-
-Issues and validates FastMCP-signed JWT tokens using HS256.
-
-This issuer creates JWT tokens for MCP clients with proper audience claims,
-maintaining OAuth 2.0 token boundaries. Tokens are signed with HS256 using
-a key derived from the upstream client secret.
-
-
-**Methods:**
-
-#### `issue_access_token`
-
-```python
-issue_access_token(self, client_id: str, scopes: list[str], jti: str, expires_in: int = 3600, upstream_claims: dict[str, Any] | None = None) -> str
-```
-
-Issue a minimal FastMCP access token.
-
-FastMCP tokens are reference tokens containing only the minimal claims
-needed for validation and lookup. The JTI maps to the upstream token
-which contains actual user identity and authorization data.
-
-**Args:**
-- `client_id`: MCP client ID
-- `scopes`: Token scopes
-- `jti`: Unique token identifier (maps to upstream token)
-- `expires_in`: Token lifetime in seconds
-- `upstream_claims`: Optional claims from upstream IdP token to include
-
-**Returns:**
-- Signed JWT token
-
-
-#### `issue_refresh_token`
-
-```python
-issue_refresh_token(self, client_id: str, scopes: list[str], jti: str, expires_in: int, upstream_claims: dict[str, Any] | None = None) -> str
-```
-
-Issue a minimal FastMCP refresh token.
-
-FastMCP refresh tokens are reference tokens containing only the minimal
-claims needed for validation and lookup. The JTI maps to the upstream
-token which contains actual user identity and authorization data.
-
-**Args:**
-- `client_id`: MCP client ID
-- `scopes`: Token scopes
-- `jti`: Unique token identifier (maps to upstream token)
-- `expires_in`: Token lifetime in seconds (should match upstream refresh expiry)
-- `upstream_claims`: Optional claims from upstream IdP token to include
-
-**Returns:**
-- Signed JWT token
-
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> dict[str, Any]
-```
-
-Verify and decode a FastMCP token.
-
-Validates JWT signature, expiration, issuer, and audience.
-
-**Args:**
-- `token`: JWT token to verify
-
-**Returns:**
-- Decoded token payload
-
-**Raises:**
-- `JoseError`: If token is invalid, expired, or has wrong claims
-
diff --git a/docs/python-sdk/fastmcp-server-auth-middleware.mdx b/docs/python-sdk/fastmcp-server-auth-middleware.mdx
deleted file mode 100644
index 29217a9fe..000000000
--- a/docs/python-sdk/fastmcp-server-auth-middleware.mdx
+++ /dev/null
@@ -1,26 +0,0 @@
----
-title: middleware
-sidebarTitle: middleware
----
-
-# `fastmcp.server.auth.middleware`
-
-
-Enhanced authentication middleware with better error messages.
-
-This module provides enhanced versions of MCP SDK authentication middleware
-that return more helpful error messages for developers troubleshooting
-authentication issues.
-
-
-## Classes
-
-### `RequireAuthMiddleware`
-
-
-Enhanced authentication middleware with detailed error messages.
-
-Extends the SDK's RequireAuthMiddleware to provide more actionable
-error messages when authentication fails. This helps developers
-understand what went wrong and how to fix it.
-
diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-__init__.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy-__init__.mdx
deleted file mode 100644
index 7c4665aa8..000000000
--- a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-__init__.mdx
+++ /dev/null
@@ -1,16 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.server.auth.oauth_proxy`
-
-
-OAuth Proxy Provider for FastMCP.
-
-This package provides OAuth proxy functionality split across multiple modules:
-- models: Pydantic models and constants
-- ui: HTML generation functions
-- consent: Consent management mixin
-- proxy: Main OAuthProxy class
-
diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-consent.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy-consent.mdx
deleted file mode 100644
index 67c514ee7..000000000
--- a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-consent.mdx
+++ /dev/null
@@ -1,28 +0,0 @@
----
-title: consent
-sidebarTitle: consent
----
-
-# `fastmcp.server.auth.oauth_proxy.consent`
-
-
-OAuth Proxy Consent Management.
-
-This module contains consent management functionality for the OAuth proxy.
-The ConsentMixin class provides methods for handling user consent flows,
-cookie management, and consent page rendering.
-
-
-## Classes
-
-### `ConsentMixin`
-
-
-Mixin class providing consent management functionality for OAuthProxy.
-
-This mixin contains all methods related to:
-- Cookie signing and verification
-- Consent page rendering
-- Consent approval/denial handling
-- URI normalization for consent tracking
-
diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-models.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy-models.mdx
deleted file mode 100644
index a096e9e45..000000000
--- a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-models.mdx
+++ /dev/null
@@ -1,105 +0,0 @@
----
-title: models
-sidebarTitle: models
----
-
-# `fastmcp.server.auth.oauth_proxy.models`
-
-
-OAuth Proxy Models and Constants.
-
-This module contains all Pydantic models and constants used by the OAuth proxy.
-
-
-## Classes
-
-### `OAuthTransaction`
-
-
-OAuth transaction state for consent flow.
-
-Stored server-side to track active authorization flows with client context.
-Includes CSRF tokens for consent protection per MCP security best practices.
-
-
-### `ClientCode`
-
-
-Client authorization code with PKCE and upstream tokens.
-
-Stored server-side after upstream IdP callback. Contains the upstream
-tokens bound to the client's PKCE challenge for secure token exchange.
-
-
-### `UpstreamTokenSet`
-
-
-Stored upstream OAuth tokens from identity provider.
-
-These tokens are obtained from the upstream provider (Google, GitHub, etc.)
-and stored in plaintext within this model. Encryption is handled transparently
-at the storage layer via FernetEncryptionWrapper. Tokens are never exposed to MCP clients.
-
-
-### `JTIMapping`
-
-
-Maps FastMCP token JTI to upstream token ID.
-
-This allows stateless JWT validation while still being able to look up
-the corresponding upstream token when tools need to access upstream APIs.
-
-
-### `RefreshTokenMetadata`
-
-
-Metadata for a refresh token, stored keyed by token hash.
-
-We store only metadata (not the token itself) for security - if storage
-is compromised, attackers get hashes they can't reverse into usable tokens.
-
-
-### `ProxyDCRClient`
-
-
-Client for DCR proxy with configurable redirect URI validation.
-
-This special client class is critical for the OAuth proxy to work correctly
-with Dynamic Client Registration (DCR). Here's why it exists:
-
-Problem:
---------
-When MCP clients use OAuth, they dynamically register with random localhost
-ports (e.g., http://localhost:55454/callback). The OAuth proxy needs to:
-1. Accept these dynamic redirect URIs from clients based on configured patterns
-2. Use its own fixed redirect URI with the upstream provider (Google, GitHub, etc.)
-3. Forward the authorization code back to the client's dynamic URI
-
-Solution:
----------
-This class validates redirect URIs against configurable patterns,
-while the proxy internally uses its own fixed redirect URI with the upstream
-provider. This allows the flow to work even when clients reconnect with
-different ports or when tokens are cached.
-
-Without proper validation, clients could get "Redirect URI not registered" errors
-when trying to authenticate with cached tokens, or security vulnerabilities could
-arise from accepting arbitrary redirect URIs.
-
-
-**Methods:**
-
-#### `validate_redirect_uri`
-
-```python
-validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl
-```
-
-Validate redirect URI against proxy patterns and optionally CIMD redirect_uris.
-
-For CIMD clients: validates against BOTH the CIMD document's redirect_uris
-AND the proxy's allowed patterns (if configured). Both must pass.
-
-For DCR clients: validates against proxy patterns first, falling back to
-base validation (registered redirect_uris) if patterns don't match.
-
diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-proxy.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy-proxy.mdx
deleted file mode 100644
index dd1400086..000000000
--- a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-proxy.mdx
+++ /dev/null
@@ -1,323 +0,0 @@
----
-title: proxy
-sidebarTitle: proxy
----
-
-# `fastmcp.server.auth.oauth_proxy.proxy`
-
-
-OAuth Proxy Provider for FastMCP.
-
-This provider acts as a transparent proxy to an upstream OAuth Authorization Server,
-handling Dynamic Client Registration locally while forwarding all other OAuth flows.
-This enables authentication with upstream providers that don't support DCR or have
-restricted client registration policies.
-
-Key features:
-- Proxies authorization and token endpoints to upstream server
-- Implements local Dynamic Client Registration with fixed upstream credentials
-- Validates tokens using upstream JWKS
-- Maintains minimal local state for bookkeeping
-- Enhanced logging with request correlation
-
-This implementation is based on the OAuth 2.1 specification and is designed for
-production use with enterprise identity providers.
-
-
-## Classes
-
-### `OAuthProxy`
-
-
-OAuth provider that presents a DCR-compliant interface while proxying to non-DCR IDPs.
-
-Purpose
--------
-MCP clients expect OAuth providers to support Dynamic Client Registration (DCR),
-where clients can register themselves dynamically and receive unique credentials.
-Most enterprise IDPs (Google, GitHub, Azure AD, etc.) don't support DCR and require
-pre-registered OAuth applications with fixed credentials.
-
-This proxy bridges that gap by:
-- Presenting a full DCR-compliant OAuth interface to MCP clients
-- Translating DCR registration requests to use pre-configured upstream credentials
-- Proxying all OAuth flows to the upstream IDP with appropriate translations
-- Managing the state and security requirements of both protocols
-
-Architecture Overview
---------------------
-The proxy maintains a single OAuth app registration with the upstream provider
-while allowing unlimited MCP clients to register and authenticate dynamically.
-It implements the complete OAuth 2.1 + DCR specification for clients while
-translating to whatever OAuth variant the upstream provider requires.
-
-Key Translation Challenges Solved
----------------------------------
-1. Dynamic Client Registration:
- - MCP clients expect to register dynamically and get unique credentials
- - Upstream IDPs require pre-registered apps with fixed credentials
- - Solution: Accept DCR requests, return shared upstream credentials
-
-2. Dynamic Redirect URIs:
- - MCP clients use random localhost ports that change between sessions
- - Upstream IDPs require fixed, pre-registered redirect URIs
- - Solution: Use proxy's fixed callback URL with upstream, forward to client's dynamic URI
-
-3. Authorization Code Mapping:
- - Upstream returns codes for the proxy's redirect URI
- - Clients expect codes for their own redirect URIs
- - Solution: Exchange upstream code server-side, issue new code to client
-
-4. State Parameter Collision:
- - Both client and proxy need to maintain state through the flow
- - Only one state parameter available in OAuth
- - Solution: Use transaction ID as state with upstream, preserve client's state
-
-5. Token Management:
- - Clients may expect different token formats/claims than upstream provides
- - Need to track tokens for revocation and refresh
- - Solution: Store token relationships, forward upstream tokens transparently
-
-OAuth Flow Implementation
-------------------------
-1. Client Registration (DCR):
- - Accept any client registration request
- - Store ProxyDCRClient that accepts dynamic redirect URIs
-
-2. Authorization:
- - Store transaction mapping client details to proxy flow
- - Redirect to upstream with proxy's fixed redirect URI
- - Use transaction ID as state parameter with upstream
-
-3. Upstream Callback:
- - Exchange upstream authorization code for tokens (server-side)
- - Generate new authorization code bound to client's PKCE challenge
- - Redirect to client's original dynamic redirect URI
-
-4. Token Exchange:
- - Validate client's code and PKCE verifier
- - Return previously obtained upstream tokens
- - Clean up one-time use authorization code
-
-5. Token Refresh:
- - Forward refresh requests to upstream using authlib
- - Handle token rotation if upstream issues new refresh token
- - Update local token mappings
-
-State Management
----------------
-The proxy maintains minimal but crucial state via pluggable storage (client_storage):
-- _oauth_transactions: Active authorization flows with client context
-- _client_codes: Authorization codes with PKCE challenges and upstream tokens
-- _jti_mapping_store: Maps FastMCP token JTIs to upstream token IDs
-- _refresh_token_store: Refresh token metadata (keyed by token hash)
-
-All state is stored in the configured client_storage backend (Redis, disk, etc.)
-enabling horizontal scaling across multiple instances.
-
-Security Considerations
-----------------------
-- Refresh tokens stored by hash only (defense in depth if storage compromised)
-- PKCE enforced end-to-end (client to proxy, proxy to upstream)
-- Authorization codes are single-use with short expiry
-- Transaction IDs are cryptographically random
-- All state is cleaned up after use to prevent replay
-- Token validation delegates to upstream provider
-
-Provider Compatibility
----------------------
-Works with any OAuth 2.0 provider that supports:
-- Authorization code flow
-- Fixed redirect URI (configured in provider's app settings)
-- Standard token endpoint
-
-Handles provider-specific requirements:
-- Google: Ensures minimum scope requirements
-- GitHub: Compatible with OAuth Apps and GitHub Apps
-- Azure AD: Handles tenant-specific endpoints
-- Generic: Works with any spec-compliant provider
-
-
-**Methods:**
-
-#### `set_mcp_path`
-
-```python
-set_mcp_path(self, mcp_path: str | None) -> None
-```
-
-Set the MCP endpoint path and create JWTIssuer with correct audience.
-
-This method is called by get_routes() to configure the resource URL
-and create the JWTIssuer. The JWT audience is set to the full resource
-URL (e.g., http://localhost:8000/mcp) to ensure tokens are bound to
-this specific MCP endpoint.
-
-**Args:**
-- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
-
-
-#### `jwt_issuer`
-
-```python
-jwt_issuer(self) -> JWTIssuer
-```
-
-Get the JWT issuer, ensuring it has been initialized.
-
-The JWT issuer is created when set_mcp_path() is called (via get_routes()).
-This property ensures a clear error if used before initialization.
-
-
-#### `get_client`
-
-```python
-get_client(self, client_id: str) -> OAuthClientInformationFull | None
-```
-
-Get client information by ID. This is generally the random ID
-provided to the DCR client during registration, not the upstream client ID.
-
-For unregistered clients, returns None (which will raise an error in the SDK).
-CIMD clients (URL-based client IDs) are looked up and cached automatically.
-
-
-#### `register_client`
-
-```python
-register_client(self, client_info: OAuthClientInformationFull) -> None
-```
-
-Register a client locally
-
-When a client registers, we create a ProxyDCRClient that is more
-forgiving about validating redirect URIs, since the DCR client's
-redirect URI will likely be localhost or unknown to the proxied IDP. The
-proxied IDP only knows about this server's fixed redirect URI.
-
-
-#### `authorize`
-
-```python
-authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
-```
-
-Start OAuth transaction and route through consent interstitial.
-
-Flow:
-1. Validate client's resource matches server's resource URL (security check)
-2. Store transaction with client details and PKCE (if forwarding)
-3. Return local /consent URL; browser visits consent first
-4. Consent handler redirects to upstream IdP if approved/already approved
-
-If consent is disabled (require_authorization_consent=False), skip the consent screen
-and redirect directly to the upstream IdP.
-
-
-#### `load_authorization_code`
-
-```python
-load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None
-```
-
-Load authorization code for validation.
-
-Look up our client code and return authorization code object
-with PKCE challenge for validation.
-
-
-#### `exchange_authorization_code`
-
-```python
-exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken
-```
-
-Exchange authorization code for FastMCP-issued tokens.
-
-Implements the token factory pattern:
-1. Retrieves upstream tokens from stored authorization code
-2. Extracts user identity from upstream token
-3. Encrypts and stores upstream tokens
-4. Issues FastMCP-signed JWT tokens
-5. Returns FastMCP tokens (NOT upstream tokens)
-
-PKCE validation is handled by the MCP framework before this method is called.
-
-
-#### `load_refresh_token`
-
-```python
-load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None
-```
-
-Load refresh token metadata from distributed storage.
-
-Looks up by token hash and reconstructs the RefreshToken object.
-Validates that the token belongs to the requesting client.
-
-
-#### `exchange_refresh_token`
-
-```python
-exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken
-```
-
-Exchange FastMCP refresh token for new FastMCP access token.
-
-Implements two-tier refresh:
-1. Verify FastMCP refresh token
-2. Look up upstream token via JTI mapping
-3. Refresh upstream token with upstream provider
-4. Update stored upstream token
-5. Issue new FastMCP access token
-6. Keep same FastMCP refresh token (unless upstream rotates)
-
-
-#### `load_access_token`
-
-```python
-load_access_token(self, token: str) -> AccessToken | None
-```
-
-Validate FastMCP JWT by swapping for upstream token.
-
-This implements the token swap pattern:
-1. Verify FastMCP JWT signature (proves it's our token)
-2. Look up upstream token via JTI mapping
-3. Decrypt upstream token
-4. Validate upstream token with provider (GitHub API, JWT validation, etc.)
-5. Return upstream validation result
-
-The FastMCP JWT is a reference token - all authorization data comes
-from validating the upstream token via the TokenVerifier.
-
-
-#### `revoke_token`
-
-```python
-revoke_token(self, token: AccessToken | RefreshToken) -> None
-```
-
-Revoke token locally and with upstream server if supported.
-
-For refresh tokens, removes from local storage by hash.
-For all tokens, attempts upstream revocation if endpoint is configured.
-Access token JTI mappings expire via TTL.
-
-
-#### `get_routes`
-
-```python
-get_routes(self, mcp_path: str | None = None) -> list[Route]
-```
-
-Get OAuth routes with custom handlers for better error UX.
-
-This method creates standard OAuth routes and replaces:
-- /authorize endpoint: Enhanced error responses for unregistered clients
-- /token endpoint: OAuth 2.1 compliant error codes
-
-**Args:**
-- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
-This is used to advertise the resource URL in metadata.
-
diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-ui.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy-ui.mdx
deleted file mode 100644
index fdbd2eb50..000000000
--- a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-ui.mdx
+++ /dev/null
@@ -1,50 +0,0 @@
----
-title: ui
-sidebarTitle: ui
----
-
-# `fastmcp.server.auth.oauth_proxy.ui`
-
-
-OAuth Proxy UI Generation Functions.
-
-This module contains HTML generation functions for consent and error pages.
-
-
-## Functions
-
-### `create_consent_html`
-
-```python
-create_consent_html(client_id: str, redirect_uri: str, scopes: list[str], txn_id: str, csrf_token: str, client_name: str | None = None, title: str = 'Application Access Request', server_name: str | None = None, server_icon_url: str | None = None, server_website_url: str | None = None, client_website_url: str | None = None, csp_policy: str | None = None, is_cimd_client: bool = False, cimd_domain: str | None = None) -> str
-```
-
-
-Create a styled HTML consent page for OAuth authorization requests.
-
-**Args:**
-- `csp_policy`: Content Security Policy override.
-If None, uses the built-in CSP policy with appropriate directives.
-If empty string "", disables CSP entirely (no meta tag is rendered).
-If a non-empty string, uses that as the CSP policy value.
-
-
-### `create_error_html`
-
-```python
-create_error_html(error_title: str, error_message: str, error_details: dict[str, str] | None = None, server_name: str | None = None, server_icon_url: str | None = None) -> str
-```
-
-
-Create a styled HTML error page for OAuth errors.
-
-**Args:**
-- `error_title`: The error title (e.g., "OAuth Error", "Authorization Failed")
-- `error_message`: The main error message to display
-- `error_details`: Optional dictionary of error details to show (e.g., `{"Error Code"\: "invalid_client"}`)
-- `server_name`: Optional server name to display
-- `server_icon_url`: Optional URL to server icon/logo
-
-**Returns:**
-- Complete HTML page as a string
-
diff --git a/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx b/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
deleted file mode 100644
index 183380ed9..000000000
--- a/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
+++ /dev/null
@@ -1,82 +0,0 @@
----
-title: oidc_proxy
-sidebarTitle: oidc_proxy
----
-
-# `fastmcp.server.auth.oidc_proxy`
-
-
-OIDC Proxy Provider for FastMCP.
-
-This provider acts as a transparent proxy to an upstream OIDC compliant Authorization
-Server. It leverages the OAuthProxy class to handle Dynamic Client Registration and
-forwarding of all OAuth flows.
-
-This implementation is based on:
- OpenID Connect Discovery 1.0 - https://openid.net/specs/openid-connect-discovery-1_0.html
- OAuth 2.0 Authorization Server Metadata - https://datatracker.ietf.org/doc/html/rfc8414
-
-
-## Classes
-
-### `OIDCConfiguration`
-
-
-OIDC Configuration.
-
-
-**Methods:**
-
-#### `get_oidc_configuration`
-
-```python
-get_oidc_configuration(cls, config_url: AnyHttpUrl) -> Self
-```
-
-Get the OIDC configuration for the specified config URL.
-
-**Args:**
-- `config_url`: The OIDC config URL
-- `strict`: The strict flag for the configuration
-- `timeout_seconds`: HTTP request timeout in seconds
-
-
-### `OIDCProxy`
-
-
-OAuth provider that wraps OAuthProxy to provide configuration via an OIDC configuration URL.
-
-This provider makes it easier to add OAuth protection for any upstream provider
-that is OIDC compliant.
-
-
-**Methods:**
-
-#### `get_oidc_configuration`
-
-```python
-get_oidc_configuration(self, config_url: AnyHttpUrl, strict: bool | None, timeout_seconds: int | None) -> OIDCConfiguration
-```
-
-Gets the OIDC configuration for the specified configuration URL.
-
-**Args:**
-- `config_url`: The OIDC configuration URL
-- `strict`: The strict flag for the configuration
-- `timeout_seconds`: HTTP request timeout in seconds
-
-
-#### `get_token_verifier`
-
-```python
-get_token_verifier(self) -> TokenVerifier
-```
-
-Creates the token verifier for the specified OIDC configuration and arguments.
-
-**Args:**
-- `algorithm`: Optional token verifier algorithm
-- `audience`: Optional token verifier audience
-- `required_scopes`: Optional token verifier required_scopes
-- `timeout_seconds`: HTTP request timeout in seconds
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-__init__.mdx b/docs/python-sdk/fastmcp-server-auth-providers-__init__.mdx
deleted file mode 100644
index 9de7cce8a..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.server.auth.providers`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-auth0.mdx b/docs/python-sdk/fastmcp-server-auth-providers-auth0.mdx
deleted file mode 100644
index 140ddd193..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-auth0.mdx
+++ /dev/null
@@ -1,41 +0,0 @@
----
-title: auth0
-sidebarTitle: auth0
----
-
-# `fastmcp.server.auth.providers.auth0`
-
-
-Auth0 OAuth provider for FastMCP.
-
-This module provides a complete Auth0 integration that's ready to use with
-just the configuration URL, client ID, client secret, audience, and base URL.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.auth0 import Auth0Provider
-
- # Simple Auth0 OAuth protection
- auth = Auth0Provider(
- config_url="https://auth0.config.url",
- client_id="your-auth0-client-id",
- client_secret="your-auth0-client-secret",
- audience="your-auth0-api-audience",
- base_url="http://localhost:8000",
- )
-
- mcp = FastMCP("My Protected Server", auth=auth)
- ```
-
-
-## Classes
-
-### `Auth0Provider`
-
-
-An Auth0 provider implementation for FastMCP.
-
-This provider is a complete Auth0 integration that's ready to use with
-just the configuration URL, client ID, client secret, audience, and base URL.
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-aws.mdx b/docs/python-sdk/fastmcp-server-auth-providers-aws.mdx
deleted file mode 100644
index 5803d4f62..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-aws.mdx
+++ /dev/null
@@ -1,82 +0,0 @@
----
-title: aws
-sidebarTitle: aws
----
-
-# `fastmcp.server.auth.providers.aws`
-
-
-AWS Cognito OAuth provider for FastMCP.
-
-This module provides a complete AWS Cognito OAuth integration that's ready to use
-with a user pool ID, domain prefix, client ID and client secret. It handles all
-the complexity of AWS Cognito's OAuth flow, token validation, and user management.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.aws_cognito import AWSCognitoProvider
-
- # Simple AWS Cognito OAuth protection
- auth = AWSCognitoProvider(
- user_pool_id="your-user-pool-id",
- aws_region="eu-central-1",
- client_id="your-cognito-client-id",
- client_secret="your-cognito-client-secret"
- )
-
- mcp = FastMCP("My Protected Server", auth=auth)
- ```
-
-
-## Classes
-
-### `AWSCognitoTokenVerifier`
-
-
-Token verifier that filters claims to Cognito-specific subset.
-
-
-**Methods:**
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify token and filter claims to Cognito-specific subset.
-
-
-### `AWSCognitoProvider`
-
-
-Complete AWS Cognito OAuth provider for FastMCP.
-
-This provider makes it trivial to add AWS Cognito OAuth protection to any
-FastMCP server using OIDC Discovery. Just provide your Cognito User Pool details,
-client credentials, and a base URL, and you're ready to go.
-
-Features:
-- Automatic OIDC Discovery from AWS Cognito User Pool
-- Automatic JWT token validation via Cognito's public keys
-- Cognito-specific claim filtering (sub, username, cognito:groups)
-- Support for Cognito User Pools
-
-
-**Methods:**
-
-#### `get_token_verifier`
-
-```python
-get_token_verifier(self) -> TokenVerifier
-```
-
-Creates a Cognito-specific token verifier with claim filtering.
-
-**Args:**
-- `algorithm`: Optional token verifier algorithm
-- `audience`: Optional token verifier audience
-- `required_scopes`: Optional token verifier required_scopes
-- `timeout_seconds`: HTTP request timeout in seconds
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
deleted file mode 100644
index 4d3140c4a..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
+++ /dev/null
@@ -1,182 +0,0 @@
----
-title: azure
-sidebarTitle: azure
----
-
-# `fastmcp.server.auth.providers.azure`
-
-
-Azure (Microsoft Entra) OAuth provider for FastMCP.
-
-This provider implements Azure/Microsoft Entra ID OAuth authentication
-using the OAuth Proxy pattern for non-DCR OAuth flows.
-
-
-## Functions
-
-### `EntraOBOToken`
-
-```python
-EntraOBOToken(scopes: list[str]) -> str
-```
-
-
-Exchange the user's Entra token for a downstream API token via OBO.
-
-This dependency performs a Microsoft Entra On-Behalf-Of (OBO) token exchange,
-allowing your MCP server to call downstream APIs (like Microsoft Graph) on
-behalf of the authenticated user.
-
-**Args:**
-- `scopes`: The scopes to request for the downstream API. For Microsoft Graph,
-use scopes like ["https\://graph.microsoft.com/Mail.Read"] or
-["https\://graph.microsoft.com/.default"].
-
-**Returns:**
-- A dependency that resolves to the downstream API access token string
-
-**Raises:**
-- `ImportError`: If fastmcp[azure] is not installed
-- `RuntimeError`: If no access token is available, provider is not Azure,
-or OBO exchange fails
-
-
-## Classes
-
-### `AzureProvider`
-
-
-Azure (Microsoft Entra) OAuth provider for FastMCP.
-
-This provider implements Azure/Microsoft Entra ID authentication using the
-OAuth Proxy pattern. It supports both organizational accounts and personal
-Microsoft accounts depending on the tenant configuration.
-
-Scope Handling:
-- required_scopes: Provide unprefixed scope names (e.g., ["read", "write"])
- → Automatically prefixed with identifier_uri during initialization
- → Validated on all tokens and advertised to MCP clients
-- additional_authorize_scopes: Provide full format (e.g., ["User.Read"])
- → NOT prefixed, NOT validated, NOT advertised to clients
- → Used to request Microsoft Graph or other upstream API permissions
-
-Features:
-- OAuth proxy to Azure/Microsoft identity platform
-- JWT validation using tenant issuer and JWKS
-- Supports tenant configurations: specific tenant ID, "organizations", or "consumers"
-- Custom API scopes and Microsoft Graph scopes in a single provider
-
-Setup:
-1. Create an App registration in Azure Portal
-2. Configure Web platform redirect URI: http://localhost:8000/auth/callback (or your custom path)
-3. Add an Application ID URI under "Expose an API" (defaults to api://{client_id})
-4. Add custom scopes (e.g., "read", "write") under "Expose an API"
-5. Set access token version to 2 in the App manifest: "requestedAccessTokenVersion": 2
-6. Create a client secret
-7. Get Application (client) ID, Directory (tenant) ID, and client secret
-
-
-**Methods:**
-
-#### `authorize`
-
-```python
-authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
-```
-
-Start OAuth transaction and redirect to Azure AD.
-
-Override parent's authorize method to filter out the 'resource' parameter
-which is not supported by Azure AD v2.0 endpoints. The v2.0 endpoints use
-scopes to determine the resource/audience instead of a separate parameter.
-
-**Args:**
-- `client`: OAuth client information
-- `params`: Authorization parameters from the client
-
-**Returns:**
-- Authorization URL to redirect the user to Azure AD
-
-
-#### `get_obo_credential`
-
-```python
-get_obo_credential(self, user_assertion: str) -> OnBehalfOfCredential
-```
-
-Get a cached or new OnBehalfOfCredential for OBO token exchange.
-
-Credentials are cached by user assertion so the Azure SDK's internal
-token cache can avoid redundant OBO exchanges when the same user
-calls multiple tools with the same scopes.
-
-**Args:**
-- `user_assertion`: The user's access token to exchange via OBO.
-
-**Returns:**
-- A configured OnBehalfOfCredential ready for get_token() calls.
-
-**Raises:**
-- `ImportError`: If azure-identity is not installed (requires fastmcp[azure]).
-
-
-#### `close_obo_credentials`
-
-```python
-close_obo_credentials(self) -> None
-```
-
-Close all cached OBO credentials.
-
-
-### `AzureJWTVerifier`
-
-
-JWT verifier pre-configured for Azure AD / Microsoft Entra ID.
-
-Auto-configures JWKS URI, issuer, audience, and scope handling from your
-Azure app registration details. Designed for Managed Identity and other
-token-verification-only scenarios where AzureProvider's full OAuth proxy
-isn't needed.
-
-Handles Azure's scope format automatically:
-- Validates tokens using short-form scopes (what Azure puts in ``scp`` claims)
-- Advertises full-URI scopes in OAuth metadata (what clients need to request)
-
-Example::
-
- from fastmcp.server.auth import RemoteAuthProvider
- from fastmcp.server.auth.providers.azure import AzureJWTVerifier
- from pydantic import AnyHttpUrl
-
- verifier = AzureJWTVerifier(
- client_id="your-client-id",
- tenant_id="your-tenant-id",
- required_scopes=["access_as_user"],
- )
-
- auth = RemoteAuthProvider(
- token_verifier=verifier,
- authorization_servers=[
- AnyHttpUrl("https://login.microsoftonline.com/your-tenant-id/v2.0")
- ],
- base_url="https://my-server.com",
- )
-
-
-**Methods:**
-
-#### `scopes_supported`
-
-```python
-scopes_supported(self) -> list[str]
-```
-
-Return scopes with Azure URI prefix for OAuth metadata.
-
-Azure tokens contain short-form scopes (e.g., ``read``) in the ``scp``
-claim, but clients must request full URI scopes (e.g.,
-``api://client-id/read``) from the Azure authorization endpoint. This
-property returns the full-URI form for OAuth metadata while
-``required_scopes`` retains the short form for token validation.
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-debug.mdx b/docs/python-sdk/fastmcp-server-auth-providers-debug.mdx
deleted file mode 100644
index 8ed3e6b87..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-debug.mdx
+++ /dev/null
@@ -1,70 +0,0 @@
----
-title: debug
-sidebarTitle: debug
----
-
-# `fastmcp.server.auth.providers.debug`
-
-
-Debug token verifier for testing and special cases.
-
-This module provides a flexible token verifier that delegates validation
-to a custom callable. Useful for testing, development, or scenarios where
-standard verification isn't possible (like opaque tokens without introspection).
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.debug import DebugTokenVerifier
-
- # Accept all tokens (default - useful for testing)
- auth = DebugTokenVerifier()
-
- # Custom sync validation logic
- auth = DebugTokenVerifier(validate=lambda token: token.startswith("valid-"))
-
- # Custom async validation logic
- async def check_cache(token: str) -> bool:
- return await redis.exists(f"token:{token}")
-
- auth = DebugTokenVerifier(validate=check_cache)
-
- mcp = FastMCP("My Server", auth=auth)
- ```
-
-
-## Classes
-
-### `DebugTokenVerifier`
-
-
-Token verifier with custom validation logic.
-
-This verifier delegates token validation to a user-provided callable.
-By default, it accepts all non-empty tokens (useful for testing).
-
-Use cases:
-- Testing: Accept any token without real verification
-- Development: Custom validation logic for prototyping
-- Opaque tokens: When you have tokens with no introspection endpoint
-
-WARNING: This bypasses standard security checks. Only use in controlled
-environments or when you understand the security implications.
-
-
-**Methods:**
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify token using custom validation logic.
-
-**Args:**
-- `token`: The token string to validate
-
-**Returns:**
-- AccessToken if validation succeeds, None otherwise
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-descope.mdx b/docs/python-sdk/fastmcp-server-auth-providers-descope.mdx
deleted file mode 100644
index 3436aa5c3..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-descope.mdx
+++ /dev/null
@@ -1,60 +0,0 @@
----
-title: descope
-sidebarTitle: descope
----
-
-# `fastmcp.server.auth.providers.descope`
-
-
-Descope authentication provider for FastMCP.
-
-This module provides DescopeProvider - a complete authentication solution that integrates
-with Descope's OAuth 2.1 and OpenID Connect services, supporting Dynamic Client Registration (DCR)
-for seamless MCP client authentication.
-
-
-## Classes
-
-### `DescopeProvider`
-
-
-Descope metadata provider for DCR (Dynamic Client Registration).
-
-This provider implements Descope integration using metadata forwarding.
-This is the recommended approach for Descope DCR
-as it allows Descope to handle the OAuth flow directly while FastMCP acts
-as a resource server.
-
-IMPORTANT SETUP REQUIREMENTS:
-
-1. Create an MCP Server in Descope Console:
- - Go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console
- - Create a new MCP Server
- - Ensure that **Dynamic Client Registration (DCR)** is enabled
- - Note your Well-Known URL
-
-2. Note your Well-Known URL:
- - Save your Well-Known URL from [MCP Server Settings](https://app.descope.com/mcp-servers)
- - Format: ``https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration``
-
-For detailed setup instructions, see:
-https://docs.descope.com/identity-federation/inbound-apps/creating-inbound-apps#method-2-dynamic-client-registration-dcr
-
-
-**Methods:**
-
-#### `get_routes`
-
-```python
-get_routes(self, mcp_path: str | None = None) -> list[Route]
-```
-
-Get OAuth routes including Descope authorization server metadata forwarding.
-
-This returns the standard protected resource routes plus an authorization server
-metadata endpoint that forwards Descope's OAuth metadata to clients.
-
-**Args:**
-- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
-This is used to advertise the resource URL in metadata.
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-discord.mdx b/docs/python-sdk/fastmcp-server-auth-providers-discord.mdx
deleted file mode 100644
index 61b024b63..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-discord.mdx
+++ /dev/null
@@ -1,66 +0,0 @@
----
-title: discord
-sidebarTitle: discord
----
-
-# `fastmcp.server.auth.providers.discord`
-
-
-Discord OAuth provider for FastMCP.
-
-This module provides a complete Discord OAuth integration that's ready to use
-with just a client ID and client secret. It handles all the complexity of
-Discord's OAuth flow, token validation, and user management.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.discord import DiscordProvider
-
- # Simple Discord OAuth protection
- auth = DiscordProvider(
- client_id="your-discord-client-id",
- client_secret="your-discord-client-secret"
- )
-
- mcp = FastMCP("My Protected Server", auth=auth)
- ```
-
-
-## Classes
-
-### `DiscordTokenVerifier`
-
-
-Token verifier for Discord OAuth tokens.
-
-Discord OAuth tokens are opaque (not JWTs), so we verify them
-by calling Discord's tokeninfo API to check if they're valid and get user info.
-
-
-**Methods:**
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify Discord OAuth token by calling Discord's tokeninfo API.
-
-
-### `DiscordProvider`
-
-
-Complete Discord OAuth provider for FastMCP.
-
-This provider makes it trivial to add Discord OAuth protection to any
-FastMCP server. Just provide your Discord OAuth app credentials and
-a base URL, and you're ready to go.
-
-Features:
-- Transparent OAuth proxy to Discord
-- Automatic token validation via Discord's API
-- User information extraction from Discord APIs
-- Minimal configuration required
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-github.mdx b/docs/python-sdk/fastmcp-server-auth-providers-github.mdx
deleted file mode 100644
index 66a808136..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-github.mdx
+++ /dev/null
@@ -1,66 +0,0 @@
----
-title: github
-sidebarTitle: github
----
-
-# `fastmcp.server.auth.providers.github`
-
-
-GitHub OAuth provider for FastMCP.
-
-This module provides a complete GitHub OAuth integration that's ready to use
-with just a client ID and client secret. It handles all the complexity of
-GitHub's OAuth flow, token validation, and user management.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.github import GitHubProvider
-
- # Simple GitHub OAuth protection
- auth = GitHubProvider(
- client_id="your-github-client-id",
- client_secret="your-github-client-secret"
- )
-
- mcp = FastMCP("My Protected Server", auth=auth)
- ```
-
-
-## Classes
-
-### `GitHubTokenVerifier`
-
-
-Token verifier for GitHub OAuth tokens.
-
-GitHub OAuth tokens are opaque (not JWTs), so we verify them
-by calling GitHub's API to check if they're valid and get user info.
-
-
-**Methods:**
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify GitHub OAuth token by calling GitHub API.
-
-
-### `GitHubProvider`
-
-
-Complete GitHub OAuth provider for FastMCP.
-
-This provider makes it trivial to add GitHub OAuth protection to any
-FastMCP server. Just provide your GitHub OAuth app credentials and
-a base URL, and you're ready to go.
-
-Features:
-- Transparent OAuth proxy to GitHub
-- Automatic token validation via GitHub API
-- User information extraction
-- Minimal configuration required
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-google.mdx b/docs/python-sdk/fastmcp-server-auth-providers-google.mdx
deleted file mode 100644
index 880488438..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-google.mdx
+++ /dev/null
@@ -1,66 +0,0 @@
----
-title: google
-sidebarTitle: google
----
-
-# `fastmcp.server.auth.providers.google`
-
-
-Google OAuth provider for FastMCP.
-
-This module provides a complete Google OAuth integration that's ready to use
-with just a client ID and client secret. It handles all the complexity of
-Google's OAuth flow, token validation, and user management.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.google import GoogleProvider
-
- # Simple Google OAuth protection
- auth = GoogleProvider(
- client_id="your-google-client-id.apps.googleusercontent.com",
- client_secret="your-google-client-secret"
- )
-
- mcp = FastMCP("My Protected Server", auth=auth)
- ```
-
-
-## Classes
-
-### `GoogleTokenVerifier`
-
-
-Token verifier for Google OAuth tokens.
-
-Google OAuth tokens are opaque (not JWTs), so we verify them
-by calling Google's tokeninfo API to check if they're valid and get user info.
-
-
-**Methods:**
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify Google OAuth token by calling Google's tokeninfo API.
-
-
-### `GoogleProvider`
-
-
-Complete Google OAuth provider for FastMCP.
-
-This provider makes it trivial to add Google OAuth protection to any
-FastMCP server. Just provide your Google OAuth app credentials and
-a base URL, and you're ready to go.
-
-Features:
-- Transparent OAuth proxy to Google
-- Automatic token validation via Google's tokeninfo API
-- User information extraction from Google APIs
-- Minimal configuration required
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx b/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx
deleted file mode 100644
index c4ee4cc14..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx
+++ /dev/null
@@ -1,96 +0,0 @@
----
-title: in_memory
-sidebarTitle: in_memory
----
-
-# `fastmcp.server.auth.providers.in_memory`
-
-## Classes
-
-### `InMemoryOAuthProvider`
-
-
-An in-memory OAuth provider for testing purposes.
-It simulates the OAuth 2.1 flow locally without external calls.
-
-
-**Methods:**
-
-#### `get_client`
-
-```python
-get_client(self, client_id: str) -> OAuthClientInformationFull | None
-```
-
-#### `register_client`
-
-```python
-register_client(self, client_info: OAuthClientInformationFull) -> None
-```
-
-#### `authorize`
-
-```python
-authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
-```
-
-Simulates user authorization and generates an authorization code.
-Returns a redirect URI with the code and state.
-
-
-#### `load_authorization_code`
-
-```python
-load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None
-```
-
-#### `exchange_authorization_code`
-
-```python
-exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken
-```
-
-#### `load_refresh_token`
-
-```python
-load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None
-```
-
-#### `exchange_refresh_token`
-
-```python
-exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken
-```
-
-#### `load_access_token`
-
-```python
-load_access_token(self, token: str) -> AccessToken | None
-```
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify a bearer token and return access info if valid.
-
-This method implements the TokenVerifier protocol by delegating
-to our existing load_access_token method.
-
-**Args:**
-- `token`: The token string to validate
-
-**Returns:**
-- AccessToken object if valid, None if invalid or expired
-
-
-#### `revoke_token`
-
-```python
-revoke_token(self, token: AccessToken | RefreshToken) -> None
-```
-
-Revokes an access or refresh token and its counterpart.
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-introspection.mdx b/docs/python-sdk/fastmcp-server-auth-providers-introspection.mdx
deleted file mode 100644
index 811737e34..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-introspection.mdx
+++ /dev/null
@@ -1,82 +0,0 @@
----
-title: introspection
-sidebarTitle: introspection
----
-
-# `fastmcp.server.auth.providers.introspection`
-
-
-OAuth 2.0 Token Introspection (RFC 7662) provider for FastMCP.
-
-This module provides token verification for opaque tokens using the OAuth 2.0
-Token Introspection protocol defined in RFC 7662. It allows FastMCP servers to
-validate tokens issued by authorization servers that don't use JWT format.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
-
- # Verify opaque tokens via RFC 7662 introspection
- verifier = IntrospectionTokenVerifier(
- introspection_url="https://auth.example.com/oauth/introspect",
- client_id="your-client-id",
- client_secret="your-client-secret",
- required_scopes=["read", "write"]
- )
-
- mcp = FastMCP("My Protected Server", auth=verifier)
- ```
-
-
-## Classes
-
-### `IntrospectionTokenVerifier`
-
-
-OAuth 2.0 Token Introspection verifier (RFC 7662).
-
-This verifier validates opaque tokens by calling an OAuth 2.0 token introspection
-endpoint. Unlike JWT verification which is stateless, token introspection requires
-a network call to the authorization server for each token validation.
-
-The verifier authenticates to the introspection endpoint using either:
-- HTTP Basic Auth (client_secret_basic, default): credentials in Authorization header
-- POST body authentication (client_secret_post): credentials in request body
-
-Both methods are specified in RFC 6749 (OAuth 2.0) and RFC 7662 (Token Introspection).
-
-Use this when:
-- Your authorization server issues opaque (non-JWT) tokens
-- You need to validate tokens from Auth0, Okta, Keycloak, or other OAuth servers
-- Your tokens require real-time revocation checking
-- Your authorization server supports RFC 7662 introspection
-
-Caching is disabled by default to preserve real-time revocation semantics.
-Set ``cache_ttl_seconds`` to enable caching and reduce load on the
-introspection endpoint (e.g., ``cache_ttl_seconds=300`` for 5 minutes).
-
-
-**Methods:**
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify a bearer token using OAuth 2.0 Token Introspection (RFC 7662).
-
-This method makes a POST request to the introspection endpoint with the token,
-authenticated using the configured client authentication method (client_secret_basic
-or client_secret_post).
-
-Results are cached in-memory to reduce load on the introspection endpoint.
-Cache TTL and size are configurable via constructor parameters.
-
-**Args:**
-- `token`: The opaque token string to validate
-
-**Returns:**
-- AccessToken object if valid and active, None if invalid, inactive, or expired
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx b/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx
deleted file mode 100644
index 6ba9054c2..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx
+++ /dev/null
@@ -1,146 +0,0 @@
----
-title: jwt
-sidebarTitle: jwt
----
-
-# `fastmcp.server.auth.providers.jwt`
-
-
-TokenVerifier implementations for FastMCP.
-
-## Classes
-
-### `JWKData`
-
-
-JSON Web Key data structure.
-
-
-### `JWKSData`
-
-
-JSON Web Key Set data structure.
-
-
-### `RSAKeyPair`
-
-
-RSA key pair for JWT testing.
-
-
-**Methods:**
-
-#### `generate`
-
-```python
-generate(cls) -> RSAKeyPair
-```
-
-Generate an RSA key pair for testing.
-
-**Returns:**
-- Generated key pair
-
-
-#### `create_token`
-
-```python
-create_token(self, subject: str = 'fastmcp-user', issuer: str = 'https://fastmcp.example.com', audience: str | list[str] | None = None, scopes: list[str] | None = None, expires_in_seconds: int = 3600, additional_claims: dict[str, Any] | None = None, kid: str | None = None) -> str
-```
-
-Generate a test JWT token for testing purposes.
-
-**Args:**
-- `subject`: Subject claim (usually user ID)
-- `issuer`: Issuer claim
-- `audience`: Audience claim - can be a string or list of strings (optional)
-- `scopes`: List of scopes to include
-- `expires_in_seconds`: Token expiration time in seconds
-- `additional_claims`: Any additional claims to include
-- `kid`: Key ID to include in header
-
-
-### `JWTVerifier`
-
-
-JWT token verifier supporting both asymmetric (RSA/ECDSA) and symmetric (HMAC) algorithms.
-
-This verifier validates JWT tokens using various signing algorithms:
-- **Asymmetric algorithms** (RS256/384/512, ES256/384/512, PS256/384/512):
- Uses public/private key pairs. Ideal for external clients and services where
- only the authorization server has the private key.
-- **Symmetric algorithms** (HS256/384/512): Uses a shared secret for both
- signing and verification. Perfect for internal microservices and trusted
- environments where the secret can be securely shared.
-
-Use this when:
-- You have JWT tokens issued by an external service (asymmetric)
-- You need JWKS support for automatic key rotation (asymmetric)
-- You have internal microservices sharing a secret key (symmetric)
-- Your tokens contain standard OAuth scopes and claims
-
-
-**Methods:**
-
-#### `load_access_token`
-
-```python
-load_access_token(self, token: str) -> AccessToken | None
-```
-
-Validate a JWT bearer token and return an AccessToken when the token is valid.
-
-**Args:**
-- `token`: The JWT bearer token string to validate.
-
-**Returns:**
-- AccessToken | None: An AccessToken populated from token claims if the token is valid; `None` if the token is expired, has an invalid signature or format, fails issuer/audience/scope validation, or any other validation error occurs.
-
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify a bearer token and return access info if valid.
-
-This method implements the TokenVerifier protocol by delegating
-to our existing load_access_token method.
-
-**Args:**
-- `token`: The JWT token string to validate
-
-**Returns:**
-- AccessToken object if valid, None if invalid or expired
-
-
-### `StaticTokenVerifier`
-
-
-Simple static token verifier for testing and development.
-
-This verifier validates tokens against a predefined dictionary of valid token
-strings and their associated claims. When a token string matches a key in the
-dictionary, the verifier returns the corresponding claims as if the token was
-validated by a real authorization server.
-
-Use this when:
-- You're developing or testing locally without a real OAuth server
-- You need predictable tokens for automated testing
-- You want to simulate different users/scopes without complex setup
-- You're prototyping and need simple API key-style authentication
-
-WARNING: Never use this in production - tokens are stored in plain text!
-
-
-**Methods:**
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify token against static token dictionary.
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-oci.mdx b/docs/python-sdk/fastmcp-server-auth-providers-oci.mdx
deleted file mode 100644
index b88c7c49e..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-oci.mdx
+++ /dev/null
@@ -1,97 +0,0 @@
----
-title: oci
-sidebarTitle: oci
----
-
-# `fastmcp.server.auth.providers.oci`
-
-
-OCI OIDC provider for FastMCP.
-
-The pull request for the provider is submitted to fastmcp.
-
-This module provides OIDC Implementation to integrate MCP servers with OCI.
-You only need OCI Identity Domain's discovery URL, client ID, client secret, and base URL.
-
-Post Authentication, you get OCI IAM domain access token. That is not authorized to invoke OCI control plane.
-You need to exchange the IAM domain access token for OCI UPST token to invoke OCI control plane APIs.
-The sample code below has get_oci_signer function that returns OCI TokenExchangeSigner object.
-You can use the signer object to create OCI service object.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.oci import OCIProvider
- from fastmcp.server.dependencies import get_access_token
- from fastmcp.utilities.logging import get_logger
-
- import os
-
- import oci
- from oci.auth.signers import TokenExchangeSigner
-
- logger = get_logger(__name__)
-
- # Load configuration from environment
- config_url = os.environ.get("OCI_CONFIG_URL") # OCI IAM Domain OIDC discovery URL
- client_id = os.environ.get("OCI_CLIENT_ID") # Client ID configured for the OCI IAM Domain Integrated Application
- client_secret = os.environ.get("OCI_CLIENT_SECRET") # Client secret configured for the OCI IAM Domain Integrated Application
- iam_guid = os.environ.get("OCI_IAM_GUID") # IAM GUID configured for the OCI IAM Domain
-
- # Simple OCI OIDC protection
- auth = OCIProvider(
- config_url=config_url, # config URL is the OCI IAM Domain OIDC discovery URL
- client_id=client_id, # This is same as the client ID configured for the OCI IAM Domain Integrated Application
- client_secret=client_secret, # This is same as the client secret configured for the OCI IAM Domain Integrated Application
- required_scopes=["openid", "profile", "email"],
- redirect_path="/auth/callback",
- base_url="http://localhost:8000",
- )
-
- # NOTE: For production use, replace this with a thread-safe cache implementation
- # such as threading.Lock-protected dict or a proper caching library
- _global_token_cache = {} # In memory cache for OCI session token signer
-
- def get_oci_signer() -> TokenExchangeSigner:
-
- authntoken = get_access_token()
- tokenID = authntoken.claims.get("jti")
- token = authntoken.token
-
- # Check if the signer exists for the token ID in memory cache
- cached_signer = _global_token_cache.get(tokenID)
- logger.debug(f"Global cached signer: {cached_signer}")
- if cached_signer:
- logger.debug(f"Using globally cached signer for token ID: {tokenID}")
- return cached_signer
-
- # If the signer is not yet created for the token then create new OCI signer object
- logger.debug(f"Creating new signer for token ID: {tokenID}")
- signer = TokenExchangeSigner(
- jwt_or_func=token,
- oci_domain_id=iam_guid.split(".")[0] if iam_guid else None, # This is same as IAM GUID configured for the OCI IAM Domain
- client_id=client_id, # This is same as the client ID configured for the OCI IAM Domain Integrated Application
- client_secret=client_secret, # This is same as the client secret configured for the OCI IAM Domain Integrated Application
- )
- logger.debug(f"Signer {signer} created for token ID: {tokenID}")
-
- #Cache the signer object in memory cache
- _global_token_cache[tokenID] = signer
- logger.debug(f"Signer cached for token ID: {tokenID}")
-
- return signer
-
- mcp = FastMCP("My Protected Server", auth=auth)
- ```
-
-
-## Classes
-
-### `OCIProvider`
-
-
-An OCI IAM Domain provider implementation for FastMCP.
-
-This provider is a complete OCI integration that's ready to use with
-just the configuration URL, client ID, client secret, and base URL.
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-propelauth.mdx b/docs/python-sdk/fastmcp-server-auth-providers-propelauth.mdx
deleted file mode 100644
index 3b31b00d8..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-propelauth.mdx
+++ /dev/null
@@ -1,69 +0,0 @@
----
-title: propelauth
-sidebarTitle: propelauth
----
-
-# `fastmcp.server.auth.providers.propelauth`
-
-
-PropelAuth authentication provider for FastMCP.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth.providers.propelauth import PropelAuthProvider
-
- auth = PropelAuthProvider(
- auth_url="https://auth.yourdomain.com",
- introspection_client_id="your-client-id",
- introspection_client_secret="your-client-secret",
- base_url="https://your-fastmcp-server.com",
- required_scopes=["read:user_data"],
- )
-
- mcp = FastMCP("My App", auth=auth)
- ```
-
-
-## Classes
-
-### `PropelAuthTokenIntrospectionOverrides`
-
-### `PropelAuthProvider`
-
-
-PropelAuth resource server provider using OAuth 2.1 token introspection.
-
-This provider validates access tokens via PropelAuth's introspection endpoint
-and forwards authorization server metadata for OAuth discovery.
-
-For detailed setup instructions, see:
-https://docs.propelauth.com/mcp-authentication/overview
-
-
-**Methods:**
-
-#### `get_routes`
-
-```python
-get_routes(self, mcp_path: str | None = None) -> list[Route]
-```
-
-Get routes for this provider.
-
-Includes the standard routes from the RemoteAuthProvider (protected resource metadata routes (RFC 9728)),
-and creates an authorization server metadata route that forwards to PropelAuth's route
-
-**Args:**
-- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
-This is used to advertise the resource URL in metadata.
-
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify token and check the ``aud`` claim against the configured resource.
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx b/docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx
deleted file mode 100644
index 1aa125c6c..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx
+++ /dev/null
@@ -1,61 +0,0 @@
----
-title: scalekit
-sidebarTitle: scalekit
----
-
-# `fastmcp.server.auth.providers.scalekit`
-
-
-Scalekit authentication provider for FastMCP.
-
-This module provides ScalekitProvider - a complete authentication solution that integrates
-with Scalekit's OAuth 2.1 and OpenID Connect services, supporting Resource Server
-authentication for seamless MCP client authentication.
-
-
-## Classes
-
-### `ScalekitProvider`
-
-
-Scalekit resource server provider for OAuth 2.1 authentication.
-
-This provider implements Scalekit integration using resource server pattern.
-FastMCP acts as a protected resource server that validates access tokens issued
-by Scalekit's authorization server.
-
-IMPORTANT SETUP REQUIREMENTS:
-
-1. Create an MCP Server in Scalekit Dashboard:
- - Go to your [Scalekit Dashboard](https://app.scalekit.com/)
- - Navigate to MCP Servers section
- - Register a new MCP Server with appropriate scopes
- - Ensure the Resource Identifier matches exactly what you configure as MCP URL
- - Note the Resource ID
-
-2. Environment Configuration:
- - Set SCALEKIT_ENVIRONMENT_URL (e.g., https://your-env.scalekit.com)
- - Set SCALEKIT_RESOURCE_ID from your created resource
- - Set BASE_URL to your FastMCP server's public URL
-
-For detailed setup instructions, see:
-https://docs.scalekit.com/mcp/overview/
-
-
-**Methods:**
-
-#### `get_routes`
-
-```python
-get_routes(self, mcp_path: str | None = None) -> list[Route]
-```
-
-Get OAuth routes including Scalekit authorization server metadata forwarding.
-
-This returns the standard protected resource routes plus an authorization server
-metadata endpoint that forwards Scalekit's OAuth metadata to clients.
-
-**Args:**
-- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
-This is used to advertise the resource URL in metadata.
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-supabase.mdx b/docs/python-sdk/fastmcp-server-auth-providers-supabase.mdx
deleted file mode 100644
index c44deecae..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-supabase.mdx
+++ /dev/null
@@ -1,67 +0,0 @@
----
-title: supabase
-sidebarTitle: supabase
----
-
-# `fastmcp.server.auth.providers.supabase`
-
-
-Supabase authentication provider for FastMCP.
-
-This module provides SupabaseProvider - a complete authentication solution that integrates
-with Supabase Auth's JWT verification, supporting Dynamic Client Registration (DCR)
-for seamless MCP client authentication.
-
-
-## Classes
-
-### `SupabaseProvider`
-
-
-Supabase metadata provider for DCR (Dynamic Client Registration).
-
-This provider implements Supabase Auth integration using metadata forwarding.
-This approach allows Supabase to handle the OAuth flow directly while FastMCP acts
-as a resource server, verifying JWTs issued by Supabase Auth.
-
-IMPORTANT SETUP REQUIREMENTS:
-
-1. Supabase Project Setup:
- - Create a Supabase project at https://supabase.com
- - Note your project URL (e.g., "https://abc123.supabase.co")
- - Configure your JWT algorithm in Supabase Auth settings (HS256, RS256, or ES256)
- - Asymmetric keys (RS256/ES256) are recommended for production
-
-2. JWT Verification:
- - FastMCP verifies JWTs using the JWKS endpoint at {project_url}{auth_route}/.well-known/jwks.json
- - JWTs are issued by {project_url}{auth_route}
- - Default auth_route is "/auth/v1" (can be customized for self-hosted setups)
- - Tokens are cached for up to 10 minutes by Supabase's edge servers
- - Algorithm must match your Supabase Auth configuration
-
-3. Authorization:
- - Supabase uses Row Level Security (RLS) policies for database authorization
- - OAuth-level scopes are an upcoming feature in Supabase Auth
- - Both approaches will be supported once scope handling is available
-
-For detailed setup instructions, see:
-https://supabase.com/docs/guides/auth/jwts
-
-
-**Methods:**
-
-#### `get_routes`
-
-```python
-get_routes(self, mcp_path: str | None = None) -> list[Route]
-```
-
-Get OAuth routes including Supabase authorization server metadata forwarding.
-
-This returns the standard protected resource routes plus an authorization server
-metadata endpoint that forwards Supabase's OAuth metadata to clients.
-
-**Args:**
-- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
-This is used to advertise the resource URL in metadata.
-
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
deleted file mode 100644
index cb263d9ec..000000000
--- a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
+++ /dev/null
@@ -1,102 +0,0 @@
----
-title: workos
-sidebarTitle: workos
----
-
-# `fastmcp.server.auth.providers.workos`
-
-
-WorkOS authentication providers for FastMCP.
-
-This module provides two WorkOS authentication strategies:
-
-1. WorkOSProvider - OAuth proxy for WorkOS Connect applications (non-DCR)
-2. AuthKitProvider - DCR-compliant provider for WorkOS AuthKit
-
-Choose based on your WorkOS setup and authentication requirements.
-
-
-## Classes
-
-### `WorkOSTokenVerifier`
-
-
-Token verifier for WorkOS OAuth tokens.
-
-WorkOS AuthKit tokens are opaque, so we verify them by calling
-the /oauth2/userinfo endpoint to check validity and get user info.
-
-
-**Methods:**
-
-#### `verify_token`
-
-```python
-verify_token(self, token: str) -> AccessToken | None
-```
-
-Verify WorkOS OAuth token by calling userinfo endpoint.
-
-
-### `WorkOSProvider`
-
-
-Complete WorkOS OAuth provider for FastMCP.
-
-This provider implements WorkOS AuthKit OAuth using the OAuth Proxy pattern.
-It provides OAuth2 authentication for users through WorkOS Connect applications.
-
-Features:
-- Transparent OAuth proxy to WorkOS AuthKit
-- Automatic token validation via userinfo endpoint
-- User information extraction from ID tokens
-- Support for standard OAuth scopes (openid, profile, email)
-
-Setup Requirements:
-1. Create a WorkOS Connect application in your dashboard
-2. Note your AuthKit domain (e.g., "https://your-app.authkit.app")
-3. Configure redirect URI as: http://localhost:8000/auth/callback
-4. Note your Client ID and Client Secret
-
-
-### `AuthKitProvider`
-
-
-AuthKit metadata provider for DCR (Dynamic Client Registration).
-
-This provider implements AuthKit integration using metadata forwarding
-instead of OAuth proxying. This is the recommended approach for WorkOS DCR
-as it allows WorkOS to handle the OAuth flow directly while FastMCP acts
-as a resource server.
-
-IMPORTANT SETUP REQUIREMENTS:
-
-1. Enable Dynamic Client Registration in WorkOS Dashboard:
- - Go to Applications → Configuration
- - Toggle "Dynamic Client Registration" to enabled
-
-2. Configure your FastMCP server URL as a callback:
- - Add your server URL to the Redirects tab in WorkOS dashboard
- - Example: https://your-fastmcp-server.com/oauth2/callback
-
-For detailed setup instructions, see:
-https://workos.com/docs/authkit/mcp/integrating/token-verification
-
-
-**Methods:**
-
-#### `get_routes`
-
-```python
-get_routes(self, mcp_path: str | None = None) -> list[Route]
-```
-
-Get OAuth routes including AuthKit authorization server metadata forwarding.
-
-This returns the standard protected resource routes plus an authorization server
-metadata endpoint that forwards AuthKit's OAuth metadata to clients.
-
-**Args:**
-- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
-This is used to advertise the resource URL in metadata.
-
diff --git a/docs/python-sdk/fastmcp-server-auth-redirect_validation.mdx b/docs/python-sdk/fastmcp-server-auth-redirect_validation.mdx
deleted file mode 100644
index 65155a160..000000000
--- a/docs/python-sdk/fastmcp-server-auth-redirect_validation.mdx
+++ /dev/null
@@ -1,63 +0,0 @@
----
-title: redirect_validation
-sidebarTitle: redirect_validation
----
-
-# `fastmcp.server.auth.redirect_validation`
-
-
-Utilities for validating client redirect URIs in OAuth flows.
-
-This module provides secure redirect URI validation with wildcard support,
-protecting against userinfo-based bypass attacks like http://localhost@evil.com.
-
-
-## Functions
-
-### `matches_allowed_pattern`
-
-```python
-matches_allowed_pattern(uri: str, pattern: str) -> bool
-```
-
-
-Securely check if a URI matches an allowed pattern with wildcard support.
-
-This function parses both the URI and pattern as URLs, comparing each
-component separately to prevent bypass attacks like userinfo injection.
-
-Patterns support wildcards:
-- http://localhost:* matches any localhost port
-- http://127.0.0.1:* matches any 127.0.0.1 port
-- https://*.example.com/* matches any subdomain of example.com
-- https://app.example.com/auth/* matches any path under /auth/
-
-Security: Rejects URIs with userinfo (user:pass@host) which could bypass
-naive string matching (e.g., http://localhost@evil.com).
-
-**Args:**
-- `uri`: The redirect URI to validate
-- `pattern`: The allowed pattern (may contain wildcards)
-
-**Returns:**
-- True if the URI matches the pattern
-
-
-### `validate_redirect_uri`
-
-```python
-validate_redirect_uri(redirect_uri: str | AnyUrl | None, allowed_patterns: list[str] | None) -> bool
-```
-
-
-Validate a redirect URI against allowed patterns.
-
-**Args:**
-- `redirect_uri`: The redirect URI to validate
-- `allowed_patterns`: List of allowed patterns. If None, all URIs are allowed (for DCR compatibility).
- If empty list, no URIs are allowed.
- To restrict to localhost only, explicitly pass DEFAULT_LOCALHOST_PATTERNS.
-
-**Returns:**
-- True if the redirect URI is allowed
-
diff --git a/docs/python-sdk/fastmcp-server-auth-ssrf.mdx b/docs/python-sdk/fastmcp-server-auth-ssrf.mdx
deleted file mode 100644
index c54ac5000..000000000
--- a/docs/python-sdk/fastmcp-server-auth-ssrf.mdx
+++ /dev/null
@@ -1,172 +0,0 @@
----
-title: ssrf
-sidebarTitle: ssrf
----
-
-# `fastmcp.server.auth.ssrf`
-
-
-SSRF-safe HTTP utilities for FastMCP.
-
-This module provides SSRF-protected HTTP fetching with:
-- DNS resolution and IP validation before requests
-- DNS pinning to prevent rebinding TOCTOU attacks
-- Support for both CIMD and JWKS fetches
-
-
-## Functions
-
-### `format_ip_for_url`
-
-```python
-format_ip_for_url(ip_str: str) -> str
-```
-
-
-Format IP address for use in URL (bracket IPv6 addresses).
-
-IPv6 addresses must be bracketed in URLs to distinguish the address from
-the port separator. For example: https://[2001:db8::1]:443/path
-
-**Args:**
-- `ip_str`: IP address string
-
-**Returns:**
-- IP string suitable for URL (IPv6 addresses are bracketed)
-
-
-### `is_ip_allowed`
-
-```python
-is_ip_allowed(ip_str: str) -> bool
-```
-
-
-Check if an IP address is allowed (must be globally routable unicast).
-
-Uses ip.is_global which catches:
-- Private (10.x, 172.16-31.x, 192.168.x)
-- Loopback (127.x, ::1)
-- Link-local (169.254.x, fe80::) - includes AWS metadata!
-- Reserved, unspecified
-- RFC6598 Carrier-Grade NAT (100.64.0.0/10) - can point to internal networks
-
-Additionally blocks multicast addresses (not caught by is_global).
-
-**Args:**
-- `ip_str`: IP address string to check
-
-**Returns:**
-- True if the IP is allowed (public unicast internet), False if blocked
-
-
-### `resolve_hostname`
-
-```python
-resolve_hostname(hostname: str, port: int = 443) -> list[str]
-```
-
-
-Resolve hostname to IP addresses using DNS.
-
-**Args:**
-- `hostname`: Hostname to resolve
-- `port`: Port number (used for getaddrinfo)
-
-**Returns:**
-- List of resolved IP addresses
-
-**Raises:**
-- `SSRFError`: If resolution fails
-
-
-### `validate_url`
-
-```python
-validate_url(url: str, require_path: bool = False) -> ValidatedURL
-```
-
-
-Validate URL for SSRF and resolve to IPs.
-
-**Args:**
-- `url`: URL to validate
-- `require_path`: If True, require non-root path (for CIMD)
-
-**Returns:**
-- ValidatedURL with resolved IPs
-
-**Raises:**
-- `SSRFError`: If URL is invalid or resolves to blocked IPs
-
-
-### `ssrf_safe_fetch`
-
-```python
-ssrf_safe_fetch(url: str) -> bytes
-```
-
-
-Fetch URL with comprehensive SSRF protection and DNS pinning.
-
-Security measures:
-1. HTTPS only
-2. DNS resolution with IP validation
-3. Connects to validated IP directly (DNS pinning prevents rebinding)
-4. Response size limit
-5. Redirects disabled
-6. Overall timeout
-
-**Args:**
-- `url`: URL to fetch
-- `require_path`: If True, require non-root path
-- `max_size`: Maximum response size in bytes (default 5KB)
-- `timeout`: Per-operation timeout in seconds
-- `overall_timeout`: Overall timeout for entire operation
-
-**Returns:**
-- Response body as bytes
-
-**Raises:**
-- `SSRFError`: If SSRF validation fails
-- `SSRFFetchError`: If fetch fails
-
-
-### `ssrf_safe_fetch_response`
-
-```python
-ssrf_safe_fetch_response(url: str) -> SSRFFetchResponse
-```
-
-
-Fetch URL with SSRF protection and return response metadata.
-
-This is equivalent to :func:`ssrf_safe_fetch` but returns response headers
-and status code, and supports conditional request headers.
-
-
-## Classes
-
-### `SSRFError`
-
-
-Raised when an SSRF protection check fails.
-
-
-### `SSRFFetchError`
-
-
-Raised when SSRF-safe fetch fails.
-
-
-### `ValidatedURL`
-
-
-A URL that has been validated for SSRF with resolved IPs.
-
-
-### `SSRFFetchResponse`
-
-
-Response payload from an SSRF-safe fetch.
-
diff --git a/docs/python-sdk/fastmcp-server-caching.mdx b/docs/python-sdk/fastmcp-server-caching.mdx
new file mode 100644
index 000000000..d4e76a033
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-caching.mdx
@@ -0,0 +1,48 @@
+---
+title: caching
+sidebarTitle: caching
+---
+
+# `fastmcp.server.caching`
+
+
+Server-level cache hints for FastMCP (SEP-2549).
+
+A FastMCP server opts every SDK-cacheable result it emits into client-side
+caching by setting `cache_ttl` (seconds) and, optionally, `cache_scope` on the
+`FastMCP` constructor. The hint is uniform by construction: one server-level
+value applies to `tools/list`, `prompts/list`, `resources/list`,
+`resources/templates/list`, `resources/read`, and `server/discover` alike — no
+per-component surface and no aggregation.
+
+FastMCP does not hand-set the wire fields. It passes the hint through to the SDK
+low-level `Server(cache_hints=...)`, whose runner fills `ttlMs`/`cacheScope` on
+every cacheable result via `apply_cache_hint`, leaving any field a handler set
+explicitly untouched. Honoring is modern-only and opt-in on the client: a hinted
+server is inert unless the client passes `cache=` and negotiates `2026-07-28`.
+
+
+## Functions
+
+### `build_cache_hints`
+
+```python
+build_cache_hints(cache_ttl: int | None, cache_scope: CacheScope | None) -> dict[CacheableMethod, CacheHint] | None
+```
+
+
+Build the per-method `CacheHint` map for the SDK low-level server.
+
+`cache_ttl` is in seconds and is converted to the wire's milliseconds. When
+`cache_ttl` is `None` the server emits no hint, so its wire output is
+identical to a server that never set one; a `cache_scope` given without a
+`cache_ttl` is meaningless (the client gates caching on the presence of a
+TTL) and is rejected rather than silently ignored.
+
+Returns `None` when no hint is set, or a map applying the same hint to every
+SDK-cacheable method otherwise.
+
+**Raises:**
+- `ValueError`: If `cache_ttl` is not positive, or if `cache_scope` is set
+without `cache_ttl`.
+
diff --git a/docs/python-sdk/fastmcp-server-completions.mdx b/docs/python-sdk/fastmcp-server-completions.mdx
new file mode 100644
index 000000000..dcea2c00d
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-completions.mdx
@@ -0,0 +1,41 @@
+---
+title: completions
+sidebarTitle: completions
+---
+
+# `fastmcp.server.completions`
+
+
+Server-side argument completion for FastMCP.
+
+A completion request names a reference — a specific prompt or resource
+template — and the argument being completed, plus a context of the argument
+values already supplied. The server answers with candidate string values.
+
+FastMCP surfaces this as a single server-level handler registered with
+``@mcp.completion``, mirroring the MCP SDK's own ``completion/complete`` shape
+and FastMCP's client-side ``Client.complete()``. The handler receives the
+reference, the argument, and the optional context, and returns candidates for
+whichever reference/argument pair it recognizes.
+
+
+## Functions
+
+### `normalize_completion`
+
+```python
+normalize_completion(result: CompletionValues) -> mcp_types.Completion
+```
+
+
+Coerce a handler's return value into a wire ``Completion``.
+
+A returned ``str`` is rejected: it is almost always a mistake (the value
+would iterate into one-character candidates), so it raises rather than
+silently producing surprising output.
+
+The MCP contract caps a completion at 100 values, so a longer result is
+truncated to the first 100 with ``has_more`` set — a handler that returns
+thousands of matches emits a conforming response rather than an oversized
+one that strict clients reject.
+
diff --git a/docs/python-sdk/fastmcp-server-context.mdx b/docs/python-sdk/fastmcp-server-context.mdx
index 58a916faf..a9d766b6b 100644
--- a/docs/python-sdk/fastmcp-server-context.mdx
+++ b/docs/python-sdk/fastmcp-server-context.mdx
@@ -7,7 +7,7 @@ sidebarTitle: context
## Functions
-### `set_transport`
+### `set_transport`
```python
set_transport(transport: TransportType) -> Token[TransportType | None]
@@ -17,7 +17,7 @@ set_transport(transport: TransportType) -> Token[TransportType | None]
Set the current transport type. Returns token for reset.
-### `reset_transport`
+### `reset_transport`
```python
reset_transport(token: Token[TransportType | None]) -> None
@@ -27,7 +27,7 @@ reset_transport(token: Token[TransportType | None]) -> None
Reset transport to previous value.
-### `set_context`
+### `set_context`
```python
set_context(context: Context) -> Generator[Context, None, None]
@@ -35,7 +35,7 @@ set_context(context: Context) -> Generator[Context, None, None]
## Classes
-### `LogData`
+### `LogData`
Data object for passing log arguments to client-side handlers.
@@ -44,7 +44,7 @@ This provides an interface to match the Python standard library logging,
for compatibility with structured logging.
-### `Context`
+### `Context`
Context object providing access to MCP capabilities.
@@ -99,7 +99,7 @@ The context is optional - tools that don't need it can omit the parameter.
**Methods:**
-#### `is_background_task`
+#### `is_background_task`
```python
is_background_task(self) -> bool
@@ -107,12 +107,11 @@ is_background_task(self) -> bool
True when this context is running in a background task (Docket worker).
-When True, certain operations like elicit() and sample() will use
-task-aware implementations that can pause the task and wait for
-client input.
+When True, certain operations like elicit() will use task-aware
+implementations that can pause the task and wait for client input.
-#### `task_id`
+#### `task_id`
```python
task_id(self) -> str | None
@@ -123,7 +122,7 @@ Get the background task ID if running in a background task.
Returns None if not running in a background task context.
-#### `origin_request_id`
+#### `origin_request_id`
```python
origin_request_id(self) -> str | None
@@ -136,7 +135,7 @@ In background task mode, this is the request_id captured when the task
was submitted, if one was available.
-#### `fastmcp`
+#### `fastmcp`
```python
fastmcp(self) -> FastMCP
@@ -145,16 +144,16 @@ fastmcp(self) -> FastMCP
Get the FastMCP instance.
-#### `request_context`
+#### `request_context`
```python
-request_context(self) -> RequestContext[ServerSession, Any, Request] | None
+request_context(self) -> FastMCPRequestContext | None
```
Access to the underlying request context.
Returns None when the MCP session has not been established yet.
-Returns the full RequestContext once the MCP session is available.
+Returns the FastMCPRequestContext wrapper once the MCP session is available.
For HTTP request access in middleware, use `get_http_request()` from fastmcp.server.dependencies,
which works whether or not the MCP session is available.
@@ -174,7 +173,65 @@ async def on_request(self, context, call_next):
```
-#### `lifespan_context`
+#### `client_extension_settings`
+
+```python
+client_extension_settings(self, identifier: str) -> dict[str, Any] | None
+```
+
+This request's per-request opt-in settings for an MCP extension.
+
+SEP-2133 extensions negotiate per request: the client repeats its
+extension capabilities in each request's ``_meta`` under
+``io.modelcontextprotocol/clientCapabilities`` → ``extensions`` →
+``identifier``. Returns the declared settings dict (possibly empty) when
+the extension was opted in for this request, or ``None`` when it was
+not (or there is no active request). This bridges an extension's
+``tools/call`` interceptor — which receives a FastMCP ``Context`` — to
+the request's declared client capabilities.
+
+
+#### `input_responses`
+
+```python
+input_responses(self) -> mcp_types.InputResponses | None
+```
+
+Client responses to a prior `InputRequiredResult.input_requests`.
+
+The multi-round-trip guard channel (SEP-2322). A guard tool inspects
+this to decide what to do on each round: `None` on the initial round
+(nothing has been asked yet, or the client retried without responses),
+so the tool returns an `InputRequiredResult` to ask; present on a later
+round, so the tool reads the answers and proceeds. It is a mapping whose
+keys match the `input_requests` map the tool minted; each value is the
+client's result for that request (an `ElicitResult`, `CreateMessageResult`,
+or `ListRootsResult`).
+
+In a background task there is no wire request, so this falls back to the
+responses the in-task guard loop delivered (see the tasks extension).
+
+
+#### `request_state`
+
+```python
+request_state(self) -> str | None
+```
+
+Opaque state echoed from a prior `InputRequiredResult.request_state`.
+
+The multi-round-trip guard channel (SEP-2322): whatever a tool put in
+`InputRequiredResult.request_state` on an earlier round is handed back
+here (as plaintext — the framework seals it on the wire and unseals it
+before the tool runs, so tampering is rejected before this is read).
+`None` on the initial round. Use it to carry a small amount of computed
+state across rounds without re-deriving it.
+
+In a background task there is no wire request, so this falls back to the
+state the in-task guard loop re-injected (see the tasks extension).
+
+
+#### `lifespan_context`
```python
lifespan_context(self) -> dict[str, Any]
@@ -182,13 +239,14 @@ lifespan_context(self) -> dict[str, Any]
Access the server's lifespan context.
-Returns the context dict yielded by the server's lifespan function.
-Returns an empty dict if no lifespan was configured or if the MCP
-session is not yet established.
+Returns the context dict yielded by *this* server's lifespan function.
+For a mounted child this is the child's own lifespan, not the parent's
+— the MCP session always belongs to the parent, so reading from the
+request context would return the parent's. We read directly from the
+server's cached lifespan result instead, which is set by the
+per-server ``_lifespan_manager`` regardless of mount position.
-In background tasks (Docket workers), where request_context is not
-available, falls back to reading from the FastMCP server's lifespan
-result directly.
+Returns an empty dict if no lifespan was configured.
Example:
```python
@@ -201,7 +259,7 @@ def my_tool(ctx: Context) -> str:
```
-#### `report_progress`
+#### `report_progress`
```python
report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None
@@ -218,7 +276,7 @@ Works in both foreground (MCP progress notifications) and background
- `message`: Optional status message describing current progress
-#### `list_resources`
+#### `list_resources`
```python
list_resources(self) -> list[SDKResource]
@@ -230,7 +288,7 @@ List all available resources from the server.
- List of Resource objects available on the server
-#### `list_prompts`
+#### `list_prompts`
```python
list_prompts(self) -> list[SDKPrompt]
@@ -242,7 +300,7 @@ List all available prompts from the server.
- List of Prompt objects available on the server
-#### `get_prompt`
+#### `get_prompt`
```python
get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult
@@ -258,7 +316,7 @@ Get a prompt by name with optional arguments.
- The prompt result
-#### `read_resource`
+#### `read_resource`
```python
read_resource(self, uri: str | AnyUrl) -> ResourceResult
@@ -273,7 +331,7 @@ Read a resource by URI.
- ResourceResult with contents
-#### `log`
+#### `log`
```python
log(self, message: str, level: LoggingLevel | None = None, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@@ -291,7 +349,7 @@ Messages sent to Clients are also logged to the `fastmcp.server.context.to_clien
- `extra`: Optional mapping for additional arguments
-#### `transport`
+#### `transport`
```python
transport(self) -> TransportType | None
@@ -303,7 +361,7 @@ Returns the transport type used to run this server: "stdio", "sse",
or "streamable-http". Returns None if called outside of a server context.
-#### `client_supports_extension`
+#### `client_supports_extension`
```python
client_supports_extension(self, extension_id: str) -> bool
@@ -314,12 +372,16 @@ Check whether the connected client supports a given MCP extension.
Inspects the ``extensions`` extra field on ``ClientCapabilities``
sent by the client during initialization.
-Returns ``False`` when no session is available (e.g., outside a
-request context) or when the client did not advertise the extension.
+Reads the client's advertised capabilities from the session, which is
+available in request mode and in background-task mode (where the
+snapshot session preserves the client's initialize params). Returns
+``False`` when no session is available (e.g., a distributed worker with
+no live session, or outside any context) or when the client did not
+advertise the extension.
Example::
- from fastmcp.server.apps import UI_EXTENSION_ID
+ from fastmcp.apps.config import UI_EXTENSION_ID
@mcp.tool
async def my_tool(ctx: Context) -> str:
@@ -328,7 +390,7 @@ Example::
return "text-only client"
-#### `client_id`
+#### `client_id`
```python
client_id(self) -> str | None
@@ -337,7 +399,7 @@ client_id(self) -> str | None
Get the client ID if available.
-#### `request_id`
+#### `request_id`
```python
request_id(self) -> str
@@ -348,7 +410,7 @@ Get the unique ID for this request.
Raises RuntimeError if MCP request context is not available.
-#### `session_id`
+#### `session_id`
```python
session_id(self) -> str
@@ -365,7 +427,7 @@ the same client session.
- for other transports.
-#### `session`
+#### `session`
```python
session(self) -> ServerSession
@@ -379,7 +441,7 @@ In background task mode: Returns the session stored at Context creation.
Raises RuntimeError if no session is available.
-#### `debug`
+#### `debug`
```python
debug(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@@ -390,7 +452,7 @@ Send a `DEBUG`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
-#### `info`
+#### `info`
```python
info(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@@ -401,7 +463,7 @@ Send a `INFO`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
-#### `warning`
+#### `warning`
```python
warning(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@@ -412,7 +474,7 @@ Send a `WARNING`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
-#### `error`
+#### `error`
```python
error(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@@ -423,19 +485,10 @@ Send a `ERROR`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
-#### `list_roots`
+#### `send_notification`
```python
-list_roots(self) -> list[Root]
-```
-
-List the roots available to the server, as indicated by the client.
-
-
-#### `send_notification`
-
-```python
-send_notification(self, notification: mcp.types.ServerNotificationType) -> None
+send_notification(self, notification: mcp_types.ServerNotification) -> None
```
Send a notification to the client immediately.
@@ -444,7 +497,7 @@ Send a notification to the client immediately.
- `notification`: An MCP notification instance (e.g., ToolListChangedNotification())
-#### `close_sse_stream`
+#### `close_sse_stream`
```python
close_sse_stream(self) -> None
@@ -462,155 +515,60 @@ Instead of holding a connection open for minutes, you can periodically close
and let the client reconnect.
-#### `sample_step`
-
-```python
-sample_step(self, messages: str | Sequence[str | SamplingMessage]) -> SampleStep
-```
-
-Make a single LLM sampling call.
-
-This is a stateless function that makes exactly one LLM call and optionally
-executes any requested tools. Use this for fine-grained control over the
-sampling loop.
-
-**Args:**
-- `messages`: The message(s) to send. Can be a string, list of strings,
-or list of SamplingMessage objects.
-- `system_prompt`: Optional system prompt for the LLM.
-- `temperature`: Optional sampling temperature.
-- `max_tokens`: Maximum tokens to generate. Defaults to 512.
-- `model_preferences`: Optional model preferences.
-- `tools`: Optional list of tools the LLM can use.
-- `tool_choice`: Tool choice mode ("auto", "required", or "none").
-- `execute_tools`: If True (default), execute tool calls and append results
-to history. If False, return immediately with tool_calls available
-in the step for manual execution.
-- `mask_error_details`: If True, mask detailed error messages from tool
-execution. When None (default), uses the global settings value.
-Tools can raise ToolError to bypass masking.
-- `tool_concurrency`: Controls parallel execution of tools\:
-- None (default)\: Sequential execution (one at a time)
-- 0\: Unlimited parallel execution
-- N > 0\: Execute at most N tools concurrently
-If any tool has sequential=True, all tools execute sequentially
-regardless of this setting.
-
-**Returns:**
-- SampleStep containing:
-- - .response: The raw LLM response
-- - .history: Messages including input, assistant response, and tool results
-- - .is_tool_use: True if the LLM requested tool execution
-- - .tool_calls: List of tool calls (if any)
-- - .text: The text content (if any)
-
-
-#### `sample`
-
-```python
-sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT]
-```
-
-Overload: With result_type, returns SamplingResult[ResultT].
-
-
-#### `sample`
-
-```python
-sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[str]
-```
-
-Overload: Without result_type, returns SamplingResult[str].
-
-
-#### `sample`
-
-```python
-sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT] | SamplingResult[str]
-```
-
-Send a sampling request to the client and await the response.
-
-This method runs to completion automatically. When tools are provided,
-it executes a tool loop: if the LLM returns a tool use request, the tools
-are executed and the results are sent back to the LLM. This continues
-until the LLM provides a final text response.
-
-When result_type is specified, a synthetic `final_response` tool is
-created. The LLM calls this tool to provide the structured response,
-which is validated against the result_type and returned as `.result`.
-
-For fine-grained control over the sampling loop, use sample_step() instead.
-
-**Args:**
-- `messages`: The message(s) to send. Can be a string, list of strings,
-or list of SamplingMessage objects.
-- `system_prompt`: Optional system prompt for the LLM.
-- `temperature`: Optional sampling temperature.
-- `max_tokens`: Maximum tokens to generate. Defaults to 512.
-- `model_preferences`: Optional model preferences.
-- `tools`: Optional list of tools the LLM can use. Accepts plain
-functions or SamplingTools.
-- `result_type`: Optional type for structured output. When specified,
-a synthetic `final_response` tool is created and the LLM's
-response is validated against this type.
-- `mask_error_details`: If True, mask detailed error messages from tool
-execution. When None (default), uses the global settings value.
-Tools can raise ToolError to bypass masking.
-- `tool_concurrency`: Controls parallel execution of tools\:
-- None (default)\: Sequential execution (one at a time)
-- 0\: Unlimited parallel execution
-- N > 0\: Execute at most N tools concurrently
-If any tool has sequential=True, all tools execute sequentially
-regardless of this setting.
-
-**Returns:**
-- SamplingResult[T] containing:
-- - .text: The text representation (raw text or JSON for structured)
-- - .result: The typed result (str for text, parsed object for structured)
-- - .history: All messages exchanged during sampling
-
-
-#### `elicit`
-
-```python
-elicit(self, message: str, response_type: None) -> AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation
-```
-
-#### `elicit`
+#### `elicit`
```python
elicit(self, message: str, response_type: type[T]) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+The accepted elicitation will contain the response data
+
+
+#### `elicit`
```python
elicit(self, message: str, response_type: list[str]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+When response_type is a list of strings, the accepted elicitation will
+contain the selected string response
+
+
+#### `elicit`
```python
elicit(self, message: str, response_type: dict[str, dict[str, str]]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+When response_type is a dict mapping keys to title dicts, the accepted
+elicitation will contain the selected key
+
+
+#### `elicit`
```python
elicit(self, message: str, response_type: list[list[str]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+When response_type is a list containing a list of strings (multi-select),
+the accepted elicitation will contain a list of selected strings
+
+
+#### `elicit`
```python
elicit(self, message: str, response_type: list[dict[str, dict[str, str]]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+When response_type is a list containing a dict mapping keys to title dicts
+(multi-select with titles), the accepted elicitation will contain a list of
+selected keys
+
+
+#### `elicit`
```python
-elicit(self, message: str, response_type: type[T] | list[str] | dict[str, dict[str, str]] | list[list[str]] | list[dict[str, dict[str, str]]] | None = None) -> AcceptedElicitation[T] | AcceptedElicitation[dict[str, Any]] | AcceptedElicitation[str] | AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
+elicit(self, message: str, response_type: type[T] | list[str] | dict[str, dict[str, str]] | list[list[str]] | list[dict[str, dict[str, str]]]) -> AcceptedElicitation[T] | AcceptedElicitation[dict[str, Any]] | AcceptedElicitation[str] | AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
```
Send an elicitation request to the client and await the response.
@@ -625,18 +583,26 @@ comply. If you provide a primitive type, an object schema with a single
"value" field will be generated for the MCP interaction and
automatically deconstructed into the primitive type upon response.
-If the response_type is None, the generated schema will be that of an
-empty object in order to comply with the MCP protocol requirements.
-Clients must send an empty object ("{}")in response.
+``response_type`` is required. Pass ``bool`` when all you need is a
+confirmation; an empty schema leaves some clients rendering an empty,
+non-functional form.
**Args:**
- `message`: A human-readable message explaining what information is needed
- `response_type`: The type of the response, which should be a primitive
type or dataclass or BaseModel. If it is a primitive type, an
object schema with a single "value" field will be generated.
+- `response_title`: Optional label to display for the wrapped ``value``
+field when ``response_type`` is a scalar, Literal, Enum, or one
+of the dict/list shorthand forms. Overrides the auto-generated
+"Value" label. Raises ``TypeError`` if passed with a BaseModel,
+dataclass, or ``None`` response type (use ``Field(title=...)``
+on the model instead).
+- `response_description`: Optional description to attach to the wrapped
+``value`` field. Same scope rules as ``response_title``.
-#### `set_state`
+#### `set_state`
```python
set_state(self, key: str, value: Any) -> None
@@ -657,7 +623,7 @@ requests.
The key is automatically prefixed with the session identifier.
-#### `get_state`
+#### `get_state`
```python
get_state(self, key: str) -> Any
@@ -671,7 +637,7 @@ then falls back to the session-scoped state store.
Returns None if the key is not found.
-#### `delete_state`
+#### `delete_state`
```python
delete_state(self, key: str) -> None
@@ -682,7 +648,7 @@ Delete a value from the state store.
Removes from both request-scoped and session-scoped stores.
-#### `enable_components`
+#### `enable_components`
```python
enable_components(self) -> None
@@ -706,7 +672,7 @@ ResourceListChangedNotification, and PromptListChangedNotification.
- `match_all`: If True, matches all components regardless of other criteria.
-#### `disable_components`
+#### `disable_components`
```python
disable_components(self) -> None
@@ -730,7 +696,7 @@ ResourceListChangedNotification, and PromptListChangedNotification.
- `match_all`: If True, matches all components regardless of other criteria.
-#### `reset_visibility`
+#### `reset_visibility`
```python
reset_visibility(self) -> None
diff --git a/docs/python-sdk/fastmcp-server-dependencies.mdx b/docs/python-sdk/fastmcp-server-dependencies.mdx
index 60439e182..1ab291fb1 100644
--- a/docs/python-sdk/fastmcp-server-dependencies.mdx
+++ b/docs/python-sdk/fastmcp-server-dependencies.mdx
@@ -9,97 +9,96 @@ sidebarTitle: dependencies
Dependency injection for FastMCP.
DI features (Depends, CurrentContext, CurrentFastMCP) work without pydocket
-using the uncalled-for DI engine. Only task-related dependencies (CurrentDocket,
-CurrentWorker) and background task execution require fastmcp[tasks].
+using the uncalled-for DI engine. The docket-specific dependencies
+(``CurrentDocket``, ``CurrentWorker``) and background task execution live in the
+``fastmcp-tasks`` package.
## Functions
-### `get_task_context`
+### `bind_request_context`
```python
-get_task_context() -> TaskContextInfo | None
+bind_request_context(ctx: ServerRequestContext) -> Generator[FastMCPRequestContext, None, None]
```
-Get the current task context if running inside a background task worker.
+Bind a ``FastMCPRequestContext`` for the duration of a handler.
-This function extracts task information from the Docket execution context.
-Returns None if not running in a task context (e.g., foreground execution).
-
-**Returns:**
-- TaskContextInfo with task_id and session_id, or None if not in a task.
+Constructs the wrapper from the SDK's per-request context and sets/resets
+the ``fastmcp_request_ctx`` ContextVar. Every request adapter and the
+initialize middleware enters this so ``Context`` and dependency helpers can
+read the active request from the ContextVar.
-### `register_task_session`
+### `extract_version_spec`
```python
-register_task_session(session_id: str, session: ServerSession) -> None
+extract_version_spec(meta: dict[str, Any] | None) -> str | None
```
-Register a session for Context access in background tasks.
-
-Called automatically when a task is submitted to Docket. The session is
-stored as a weakref so it doesn't prevent garbage collection when the
-client disconnects.
-
-**Args:**
-- `session_id`: The session identifier
-- `session`: The ServerSession instance
+Extract the FastMCP component version from a lifted ``_meta`` block.
-### `get_task_session`
+### `set_background_context_factory`
```python
-get_task_session(session_id: str) -> ServerSession | None
+set_background_context_factory(factory: Callable[[], Awaitable[Context | None]] | None) -> None
```
-Get a registered session by ID if still alive.
+Install (or clear) the background-task ``Context`` factory.
-**Args:**
-- `session_id`: The session identifier
-
-**Returns:**
-- The ServerSession if found and alive, None otherwise
+The factory returns an already-entered ``Context`` (so ``_current_context``
+is set for cleanup) when called inside a worker, or ``None`` when there is
+no task context. Passing ``None`` restores core's no-worker-fallback
+behavior.
-### `is_docket_available`
+### `set_worker_server_resolver`
+
+```python
+set_worker_server_resolver(resolver: Callable[[], FastMCP | None] | None) -> None
+```
+
+
+Install (or clear) the worker-server resolver used by ``get_server()``.
+
+
+### `is_docket_available`
```python
is_docket_available() -> bool
```
-Check if pydocket is installed.
+Check if a compatible pydocket (>= 0.19.0) is installed and importable.
+
+Three things have to be true for fastmcp's task features to work:
+ 1. pydocket distribution metadata is discoverable
+ 2. its version is at least ``_MIN_DOCKET_VERSION`` (older versions are
+ missing symbols like ``docket.dependencies.current_execution``,
+ which fastmcp imports on the request hot path)
+ 3. the package actually imports — guards against broken/partial
+ installs where metadata exists but ``import docket`` blows up
+
+Any of those failing means we treat docket as unavailable and fall back
+to the no-tasks code paths instead of crashing deep inside a request.
-### `require_docket`
-
-```python
-require_docket(feature: str) -> None
-```
-
-
-Raise ImportError with install instructions if docket not available.
-
-**Args:**
-- `feature`: Description of what requires docket (e.g., "`task=True`",
- "CurrentDocket()"). Will be included in the error message.
-
-
-### `transform_context_annotations`
+### `transform_context_annotations`
```python
transform_context_annotations(fn: Callable[..., Any]) -> Callable[..., Any]
```
-Transform ctx: Context into ctx: Context = CurrentContext().
+Transform injected-by-type params into Dependency-defaulted params.
-Transforms ALL params typed as Context to use Docket's DI system,
-unless they already have a Dependency-based default (like CurrentContext()).
+Transforms ALL params typed as Context (into ``= CurrentContext()``) and as
+UserSession (into ``= CurrentSession()``) to use Docket's DI system, unless
+they already have a Dependency-based default.
This unifies the legacy type annotation DI with Docket's Depends() system,
allowing both patterns to work through a single resolution path.
@@ -115,7 +114,7 @@ allows them to have defaults in any order.
- Function with modified signature (same function object, updated __signature__)
-### `get_context`
+### `get_context`
```python
get_context() -> Context
@@ -125,7 +124,7 @@ get_context() -> Context
Get the current FastMCP Context instance directly.
-### `get_server`
+### `get_server`
```python
get_server() -> FastMCP
@@ -134,6 +133,10 @@ get_server() -> FastMCP
Get the current FastMCP server instance directly.
+In a background-task worker the tasks extension's resolver is consulted
+first, so a mounted-child task resolves to the child server rather than the
+root that started the worker (#3571).
+
**Returns:**
- The active FastMCP server
@@ -141,7 +144,32 @@ Get the current FastMCP server instance directly.
- `RuntimeError`: If no server in context
-### `get_http_request`
+### `get_session`
+
+```python
+get_session(session_id: str) -> Session
+```
+
+
+Resolve and validate a `Session` for an explicit `session_id`.
+
+Pair with a `session_id: SessionId` tool argument (the agent obtains an id
+from `create_session` and passes it back). For a single per-user bucket with
+nothing for the agent to pass, inject `session: UserSession` instead.
+
+State is keyed by `(principal, session_id)`: the authenticated principal is
+the isolation wall and `session_id` organizes sessions within it. The id must
+have been minted by `create_session` under the current principal; an id that
+was never created, or created under a different principal, raises
+`InvalidSession` rather than resolving to a fresh empty bucket (the specific
+reason is logged at debug level, never returned to the caller).
+
+Like `get_server()`, this resolves through the task-aware server, so it needs
+no foreground context — it works from a `task=True` tool's Docket worker as
+well as a normal request.
+
+
+### `get_http_request`
```python
get_http_request() -> Request
@@ -153,7 +181,7 @@ Get the current HTTP request.
Tries MCP SDK's request_ctx first, then falls back to FastMCP's HTTP context.
-### `get_http_headers`
+### `get_http_headers`
```python
get_http_headers(include_all: bool = False, include: set[str] | None = None) -> dict[str, str]
@@ -174,7 +202,7 @@ normally be excluded. This is useful for proxy transports that need to forward
authorization headers to upstream MCP servers.
-### `get_access_token`
+### `get_access_token`
```python
get_access_token() -> AccessToken | None
@@ -186,14 +214,13 @@ Get the FastMCP access token from the current context.
This function first tries to get the token from the current HTTP request's scope,
which is more reliable for long-lived connections where the SDK's auth_context_var
may become stale after token refresh. Falls back to the SDK's context var if no
-request is available. In background tasks (Docket workers), falls back to the
-token snapshot stored in Redis at task submission time.
+request is available.
**Returns:**
- The access token if an authenticated user is available, None otherwise.
-### `without_injected_parameters`
+### `without_injected_parameters`
```python
without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any]
@@ -213,12 +240,16 @@ Handles:
**Args:**
- `fn`: Original function with Context and/or dependencies
+- `run_in_thread`: For sync ``fn``, whether to dispatch the call to a worker
+thread after resolving dependencies. Defaults to True. Set to False
+to call ``fn`` inline on the event loop thread — required for
+thread-affinity libraries (e.g. Windows COM). Ignored for async fns.
**Returns:**
- Async wrapper function without injected parameters
-### `resolve_dependencies`
+### `resolve_dependencies`
```python
resolve_dependencies(fn: Callable[..., Any], arguments: dict[str, Any]) -> AsyncGenerator[dict[str, Any], None]
@@ -244,7 +275,7 @@ time, so all injection goes through the unified DI system.
which will be filtered out)
-### `CurrentContext`
+### `CurrentContext`
```python
CurrentContext() -> Context
@@ -263,7 +294,7 @@ current MCP operation (tool/resource/prompt call).
- `RuntimeError`: If no active context found (during resolution)
-### `OptionalCurrentContext`
+### `OptionalCurrentContext`
```python
OptionalCurrentContext() -> Context | None
@@ -273,47 +304,7 @@ OptionalCurrentContext() -> Context | None
Get the current FastMCP Context, or None when no context is active.
-### `CurrentDocket`
-
-```python
-CurrentDocket() -> Docket
-```
-
-
-Get the current Docket instance managed by FastMCP.
-
-This dependency provides access to the Docket instance that FastMCP
-automatically creates for background task scheduling.
-
-**Returns:**
-- A dependency that resolves to the active Docket instance
-
-**Raises:**
-- `RuntimeError`: If not within a FastMCP server context
-- `ImportError`: If fastmcp[tasks] not installed
-
-
-### `CurrentWorker`
-
-```python
-CurrentWorker() -> Worker
-```
-
-
-Get the current Docket Worker instance managed by FastMCP.
-
-This dependency provides access to the Worker instance that FastMCP
-automatically creates for background task processing.
-
-**Returns:**
-- A dependency that resolves to the active Worker instance
-
-**Raises:**
-- `RuntimeError`: If not within a FastMCP server context
-- `ImportError`: If fastmcp[tasks] not installed
-
-
-### `CurrentFastMCP`
+### `CurrentFastMCP`
```python
CurrentFastMCP() -> FastMCP
@@ -331,7 +322,7 @@ This dependency provides access to the active FastMCP server.
- `RuntimeError`: If no server in context (during resolution)
-### `CurrentRequest`
+### `CurrentRequest`
```python
CurrentRequest() -> Request
@@ -351,7 +342,7 @@ current HTTP request. Only available when running over HTTP transports
- `RuntimeError`: If no HTTP request in context (e.g., STDIO transport)
-### `CurrentHeaders`
+### `CurrentHeaders`
```python
CurrentHeaders() -> dict[str, str]
@@ -369,7 +360,7 @@ transport.
- A dependency that resolves to a dictionary of header name -> value
-### `CurrentAccessToken`
+### `CurrentAccessToken`
```python
CurrentAccessToken() -> AccessToken
@@ -388,7 +379,7 @@ authenticated request. Raises an error if no authentication is present.
- `RuntimeError`: If no authenticated user (use get_access_token() for optional)
-### `TokenClaim`
+### `TokenClaim`
```python
TokenClaim(name: str) -> str
@@ -413,16 +404,25 @@ without needing the full token object.
## Classes
-### `TaskContextInfo`
+### `FastMCPRequestContext`
-Information about the current background task context.
+FastMCP-owned wrapper around the SDK's per-request context.
-Returned by ``get_task_context()`` when running inside a Docket worker.
-Contains identifiers needed to communicate with the MCP session.
+The SDK v2 runner hands each handler a fresh ``ServerRequestContext`` as an
+argument rather than exposing it through a ContextVar. FastMCP owns this
+ContextVar (``fastmcp_request_ctx``) and each request adapter binds a
+``FastMCPRequestContext`` at the top of the handler (and the initialize
+middleware binds it too).
+
+A wrapper rather than the raw context because the SDK's
+``ServerRequestContext.meta`` is a bare ``RequestParamsMeta`` TypedDict that
+only carries ``progress_token`` — it does not carry ``_meta.fastmcp`` or the
+distributed-trace parent. Those live in the raw params dict under ``_meta``,
+which this wrapper lifts once so downstream consumers have a stable surface.
-### `ProgressLike`
+### `ProgressLike`
Protocol for progress tracking interface.
@@ -433,7 +433,7 @@ and Docket's Progress (worker context).
**Methods:**
-#### `current`
+#### `current`
```python
current(self) -> int | None
@@ -442,7 +442,7 @@ current(self) -> int | None
Current progress value.
-#### `total`
+#### `total`
```python
total(self) -> int
@@ -451,7 +451,7 @@ total(self) -> int
Total/target progress value.
-#### `message`
+#### `message`
```python
message(self) -> str | None
@@ -460,7 +460,7 @@ message(self) -> str | None
Current progress message.
-#### `set_total`
+#### `set_total`
```python
set_total(self, total: int) -> None
@@ -469,7 +469,7 @@ set_total(self, total: int) -> None
Set the total/target value for progress tracking.
-#### `increment`
+#### `increment`
```python
increment(self, amount: int = 1) -> None
@@ -478,7 +478,7 @@ increment(self, amount: int = 1) -> None
Atomically increment the current progress value.
-#### `set_message`
+#### `set_message`
```python
set_message(self, message: str | None) -> None
@@ -487,7 +487,7 @@ set_message(self, message: str | None) -> None
Update the progress status message.
-### `InMemoryProgress`
+### `InMemoryProgress`
In-memory progress tracker for immediate tool execution.
@@ -499,25 +499,25 @@ progress doesn't need to be observable across processes.
**Methods:**
-#### `current`
+#### `current`
```python
current(self) -> int | None
```
-#### `total`
+#### `total`
```python
total(self) -> int
```
-#### `message`
+#### `message`
```python
message(self) -> str | None
```
-#### `set_total`
+#### `set_total`
```python
set_total(self, total: int) -> None
@@ -526,7 +526,7 @@ set_total(self, total: int) -> None
Set the total/target value for progress tracking.
-#### `increment`
+#### `increment`
```python
increment(self, amount: int = 1) -> None
@@ -535,7 +535,7 @@ increment(self, amount: int = 1) -> None
Atomically increment the current progress value.
-#### `set_message`
+#### `set_message`
```python
set_message(self, message: str | None) -> None
@@ -544,24 +544,22 @@ set_message(self, message: str | None) -> None
Update the progress status message.
-### `Progress`
+### `Progress`
-FastMCP Progress dependency that works in both server and worker contexts.
+Progress dependency that works in both server and worker contexts.
-Handles three execution modes:
-- In Docket worker: Uses the execution's progress (observable via Redis)
-- In FastMCP server with Docket: Falls back to in-memory progress
-- In FastMCP server without Docket: Uses in-memory progress
+In a Docket worker, delegates to the execution's Redis-backed progress
+(observable across processes). Otherwise, uses in-memory tracking.
-This allows tools to use Progress() regardless of whether they're called
-immediately or as background tasks, and regardless of whether pydocket
-is installed.
+The shared default instance acts as a stateless factory — ``__aenter__``
+creates a fresh ``Progress`` per invocation so concurrent tasks never
+share mutable state.
**Methods:**
-#### `current`
+#### `current`
```python
current(self) -> int | None
@@ -570,7 +568,7 @@ current(self) -> int | None
Current progress value.
-#### `total`
+#### `total`
```python
total(self) -> int
@@ -579,7 +577,7 @@ total(self) -> int
Total/target progress value.
-#### `message`
+#### `message`
```python
message(self) -> str | None
@@ -588,7 +586,7 @@ message(self) -> str | None
Current progress message.
-#### `set_total`
+#### `set_total`
```python
set_total(self, total: int) -> None
@@ -597,7 +595,7 @@ set_total(self, total: int) -> None
Set the total/target value for progress tracking.
-#### `increment`
+#### `increment`
```python
increment(self, amount: int = 1) -> None
@@ -606,7 +604,7 @@ increment(self, amount: int = 1) -> None
Atomically increment the current progress value.
-#### `set_message`
+#### `set_message`
```python
set_message(self, message: str | None) -> None
diff --git a/docs/python-sdk/fastmcp-server-elicitation.mdx b/docs/python-sdk/fastmcp-server-elicitation.mdx
index 85f140c6e..824ab59e9 100644
--- a/docs/python-sdk/fastmcp-server-elicitation.mdx
+++ b/docs/python-sdk/fastmcp-server-elicitation.mdx
@@ -7,28 +7,35 @@ sidebarTitle: elicitation
## Functions
-### `parse_elicit_response_type`
+### `parse_elicit_response_type`
```python
-parse_elicit_response_type(response_type: Any) -> ElicitConfig
+parse_elicit_response_type(response_type: Any, response_title: str | None = None, response_description: str | None = None) -> ElicitConfig
```
Parse response_type into schema and handling configuration.
-Supports multiple syntaxes:
-- None: Empty object schema, expect empty response
+A response type is required; ``None`` raises ``TypeError``. Supports
+multiple syntaxes:
- dict: `{"low": {"title": "..."}}` -> single-select titled enum
- list patterns:
- `[["a", "b"]]` -> multi-select untitled
- `[{"low": {...}}]` -> multi-select titled
- `["a", "b"]` -> single-select untitled
-- `list[X]` type annotation: multi-select with type
+- `list\[X]` type annotation: multi-select with type
- Scalar types (bool, int, float, str, Literal, Enum): single value
- Other types (dataclass, BaseModel): use directly
+The ``response_title`` and ``response_description`` arguments customize the
+label and description of the wrapped ``value`` property for the scalar/dict/list
+shorthand forms. They are only valid when FastMCP is wrapping the response
+type; passing them with a full BaseModel/dataclass raises ``TypeError``,
+because in those cases the user already controls field metadata via
+``Field(title=..., description=...)``.
-### `handle_elicit_accept`
+
+### `handle_elicit_accept`
```python
handle_elicit_accept(config: ElicitConfig, content: Any) -> AcceptedElicitation[Any]
@@ -45,7 +52,7 @@ Handle an accepted elicitation response.
- AcceptedElicitation with the extracted/validated data
-### `get_elicitation_schema`
+### `get_elicitation_schema`
```python
get_elicitation_schema(response_type: type[T]) -> dict[str, Any]
@@ -58,7 +65,7 @@ Get the schema for an elicitation response.
- `response_type`: The type of the response
-### `validate_elicitation_json_schema`
+### `validate_elicitation_json_schema`
```python
validate_elicitation_json_schema(schema: dict[str, Any]) -> None
@@ -83,7 +90,7 @@ This ensures the schema is compatible with MCP elicitation requirements:
## Classes
-### `ElicitationJsonSchema`
+### `ElicitationJsonSchema`
Custom JSON schema generator for MCP elicitation that always inlines enums.
@@ -95,7 +102,7 @@ Optionally adds enumNames for better UI display when available.
**Methods:**
-#### `generate_inner`
+#### `generate_inner`
```python
generate_inner(self, schema: core_schema.CoreSchema) -> JsonSchemaValue
@@ -104,7 +111,7 @@ generate_inner(self, schema: core_schema.CoreSchema) -> JsonSchemaValue
Override to prevent ref generation for enums and handle list schemas.
-#### `list_schema`
+#### `list_schema`
```python
list_schema(self, schema: core_schema.ListSchema) -> JsonSchemaValue
@@ -113,7 +120,7 @@ list_schema(self, schema: core_schema.ListSchema) -> JsonSchemaValue
Generate schema for list types, detecting enum items for multi-select.
-#### `enum_schema`
+#### `enum_schema`
```python
enum_schema(self, schema: core_schema.EnumSchema) -> JsonSchemaValue
@@ -125,15 +132,15 @@ Always generates enum pattern: `{"enum": [value, ...]}`
Titled enums are handled separately via dict-based syntax in ctx.elicit().
-### `AcceptedElicitation`
+### `AcceptedElicitation`
Result when user accepts the elicitation.
-### `ScalarElicitationType`
+### `ScalarElicitationType`
-### `ElicitConfig`
+### `ElicitConfig`
Configuration for an elicitation request.
diff --git a/docs/python-sdk/fastmcp-server-event_store.mdx b/docs/python-sdk/fastmcp-server-event_store.mdx
index 004e94405..39a2ba77b 100644
--- a/docs/python-sdk/fastmcp-server-event_store.mdx
+++ b/docs/python-sdk/fastmcp-server-event_store.mdx
@@ -16,19 +16,19 @@ AsyncKeyValue protocol, allowing users to configure any compatible backend
## Classes
-### `EventEntry`
+### `EventEntry`
Stored event entry.
-### `StreamEventList`
+### `StreamEventList`
List of event IDs for a stream.
-### `EventStore`
+### `EventStore`
EventStore implementation backed by AsyncKeyValue.
@@ -45,7 +45,7 @@ following the same pattern as ResponseCachingMiddleware and OAuthProxy.
**Methods:**
-#### `store_event`
+#### `store_event`
```python
store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId
@@ -61,7 +61,7 @@ Store an event and return its ID.
- The generated event ID for the stored event
-#### `replay_events_after`
+#### `replay_events_after`
```python
replay_events_after(self, last_event_id: EventId, send_callback: EventCallback) -> StreamId | None
diff --git a/docs/python-sdk/fastmcp-server-extensions.mdx b/docs/python-sdk/fastmcp-server-extensions.mdx
new file mode 100644
index 000000000..3c8c2f62f
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-extensions.mdx
@@ -0,0 +1,194 @@
+---
+title: extensions
+sidebarTitle: extensions
+---
+
+# `fastmcp.server.extensions`
+
+
+FastMCP-native server extension API (SEP-2133).
+
+An MCP extension is an opt-in, capability-negotiated bundle of protocol
+behaviour identified by a reverse-DNS string (e.g. `io.modelcontextprotocol/tasks`).
+Unlike the SDK's `mcp.server.extension.Extension`, a FastMCP `ServerExtension`
+is bound to its `FastMCP` instance at registration, so its request handlers and
+its `tools/call` interceptor can reach the component registry, `Context`, and
+auth scope that the SDK's model withholds.
+
+An extension contributes any subset of four things:
+
+- **A negotiated capability.** `settings()` is spliced into
+ `ServerCapabilities.extensions[identifier]` (see `LowLevelServer.get_capabilities`).
+- **New request methods.** `methods()` returns `MethodBinding`s, each wired onto
+ the low-level server via `add_request_handler` when the extension is registered.
+- **A `tools/call` interceptor.** `intercept_tool_call()` is the last gate before
+ a tool body runs — it composes *after* the FastMCP middleware chain and *before*
+ component execution, so it can observe, short-circuit, or pass a call through.
+- **A lifespan.** `lifespan()` is entered with the server's lifespan and exited on
+ shutdown — the hook the SDK's `Extension` lacks, needed to start backends/workers.
+
+The base class follows the SDK's httpx-style shape: every contribution method has
+a default, so a subclass overrides only what it needs.
+
+
+## Functions
+
+### `read_client_extension_settings`
+
+```python
+read_client_extension_settings(ctx: ServerRequestContext[Any, Any], identifier: str) -> dict[str, Any] | None
+```
+
+
+Read a client's per-request extension opt-in from the request `_meta`.
+
+SEP-2133 extensions negotiate per request: the client repeats its extension
+capabilities in each request's `_meta` under
+`io.modelcontextprotocol/clientCapabilities` → `extensions` → `identifier`.
+Returns the declared settings dict (possibly empty) when the extension was
+opted in for this request, or `None` when it was not.
+
+
+### `build_method_handler`
+
+```python
+build_method_handler(binding: MethodBinding) -> ExtensionRequestHandler
+```
+
+
+Wrap a `MethodBinding` into a low-level request handler.
+
+The adapter enforces `protocol_versions` gating (rejecting other versions as
+`METHOD_NOT_FOUND`, since `add_request_handler` registers unconditionally)
+and binds the FastMCP request context so the handler can use `get_context()`,
+auth, and other request-scoped dependencies.
+
+
+### `wrap_tool_call_interceptor`
+
+```python
+wrap_tool_call_interceptor(extension: ServerExtension, call_next: Callable[[Any], Awaitable[Any]]) -> Callable[[Any], Awaitable[Any]]
+```
+
+
+Fold one extension's `intercept_tool_call` around a middleware `call_next`.
+
+The returned wrapper is a FastMCP `CallNext`: it hands the extension the
+validated `tools/call` params, the FastMCP `Context`, and a zero-arg
+continuation that runs the rest of the chain and, finally, the tool body.
+
+
+## Classes
+
+### `MethodBinding`
+
+
+A new request method an extension serves, e.g. `tasks/get`.
+
+`params_type` validates incoming params before `handler` runs; it should
+subclass `RequestParams` so `_meta` parses uniformly. `protocol_versions`,
+when set, restricts the method to those wire versions — a request at any
+other version is rejected as `METHOD_NOT_FOUND`, mirroring the spec's
+`(method, version)` boundary. `None` (the default) admits every version.
+
+Extension methods are additive: `method` must not name a spec-defined
+request method (`tools/call`, `completion/complete`, ...). Binding one would
+silently shadow the server's own handler. Both constraints are enforced at
+construction.
+
+
+### `ServerExtension`
+
+
+Base class for an opt-in FastMCP server extension (SEP-2133).
+
+Subclass, set `identifier`, and override the contribution methods that
+apply. Every method has a default, so a minimal extension overrides only
+`identifier` and one contribution. `identifier` is validated at
+subclass-definition time when set as a class attribute, and again at
+registration (which covers per-instance identifiers assigned in `__init__`).
+
+Register an instance with `FastMCP.add_extension(...)`, which binds the
+extension to the server so `self.server`, `intercept_tool_call`, and method
+handlers can reach FastMCP-level constructs.
+
+
+**Methods:**
+
+#### `server`
+
+```python
+server(self) -> FastMCP
+```
+
+The FastMCP server this extension is registered on.
+
+Handlers, interceptors, and lifespan code reach the component registry,
+`Context`, and auth scope through here. Raises if the extension has not
+been registered with `FastMCP.add_extension()`.
+
+
+#### `settings`
+
+```python
+settings(self) -> dict[str, Any]
+```
+
+Per-extension settings advertised at `capabilities.extensions[identifier]`.
+
+An empty dict (the default) advertises the extension with no settings.
+
+
+#### `methods`
+
+```python
+methods(self) -> Sequence[MethodBinding]
+```
+
+New request methods this extension serves (additive).
+
+
+#### `lifespan`
+
+```python
+lifespan(self) -> AbstractAsyncContextManager[None]
+```
+
+A context manager entered with the server's lifespan, exited on shutdown.
+
+Default: a no-op. Override to start and stop resources an extension owns
+(a task-queue backend and worker, say). Entered once per runtime tree, at
+the root — a mounted child defers to the root, as the shared Docket does.
+
+
+#### `intercept_tool_call`
+
+```python
+intercept_tool_call(self, params: CallToolRequestParams, context: Context, call_next: ToolCallContinuation) -> ToolCallOutcome
+```
+
+Wrap `tools/call`. Default: pass through unchanged.
+
+Runs after the FastMCP middleware chain and before the tool body, so it
+is the last gate before execution. Override to observe the call, to
+short-circuit (return a result without awaiting `call_next`), or to pass
+it through (`return await call_next()`). `params` is the validated
+`tools/call` params; `context` is the FastMCP `Context`, from which the
+tool being called (`context.fastmcp.get_tool(params.name)`), auth scope,
+and the server are reachable. Multiple extensions nest with the
+first-registered outermost.
+
+
+#### `client_settings`
+
+```python
+client_settings(self, ctx: ServerRequestContext[Any, Any]) -> dict[str, Any] | None
+```
+
+This extension's per-request opt-in settings declared by the client.
+
+Reads the request's `_meta` client-capabilities block. Returns the
+declared settings dict (possibly empty) when the client opted this
+extension in for the request, or `None` when it did not. Convenience for
+`read_client_extension_settings(ctx, self.identifier)`.
+
diff --git a/docs/python-sdk/fastmcp-server-http.mdx b/docs/python-sdk/fastmcp-server-http.mdx
index 9b2fe758d..46db6c15a 100644
--- a/docs/python-sdk/fastmcp-server-http.mdx
+++ b/docs/python-sdk/fastmcp-server-http.mdx
@@ -7,13 +7,13 @@ sidebarTitle: http
## Functions
-### `set_http_request`
+### `set_http_request`
```python
set_http_request(request: Request) -> Generator[Request, None, None]
```
-### `create_base_app`
+### `create_base_app`
```python
create_base_app(routes: list[BaseRoute], middleware: list[Middleware], debug: bool = False, lifespan: Callable | None = None) -> StarletteWithLifespan
@@ -32,7 +32,7 @@ Create a base Starlette app with common middleware and routes.
- A Starlette application
-### `create_sse_app`
+### `create_sse_app`
```python
create_sse_app(server: FastMCP[LifespanResultT], message_path: str, sse_path: str, auth: AuthProvider | None = None, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None) -> StarletteWithLifespan
@@ -54,10 +54,10 @@ Returns:
A Starlette application with RequestContextMiddleware
-### `create_streamable_http_app`
+### `create_streamable_http_app`
```python
-create_streamable_http_app(server: FastMCP[LifespanResultT], streamable_http_path: str, event_store: EventStore | None = None, retry_interval: int | None = None, auth: AuthProvider | None = None, json_response: bool = False, stateless_http: bool = False, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None) -> StarletteWithLifespan
+create_streamable_http_app(server: FastMCP[LifespanResultT], streamable_http_path: str, event_store: EventStore | None = None, retry_interval: int | None = None, auth: AuthProvider | None = None, json_response: bool = False, stateless_http: bool = False, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None, host_origin_protection: HostOriginProtection = False, allowed_hosts: Sequence[str] | None = None, allowed_origins: Sequence[str] | None = None, session_idle_timeout: float | None = None) -> StarletteWithLifespan
```
@@ -76,6 +76,18 @@ disconnections. Requires event_store to be set. Defaults to SDK default.
- `debug`: Whether to enable debug mode
- `routes`: Optional list of custom routes
- `middleware`: Optional list of middleware
+- `host_origin_protection`: Whether to validate Host and Origin headers
+before requests reach the MCP endpoint. Defaults to False for
+compatibility. "auto" protects localhost-bound servers and explicit
+host/origin allowlists.
+- `allowed_hosts`: Additional hostnames that may appear in the Host header.
+- `allowed_origins`: Additional browser origins trusted by the request guard.
+Configure CORS separately when browser JavaScript must read
+cross-origin responses.
+- `session_idle_timeout`: Maximum time in seconds a session may remain idle
+before it is terminated. The deadline is pushed forward on every
+request. When None, sessions never expire from inactivity. Not
+supported in stateless mode.
**Returns:**
- A Starlette application with StreamableHTTP support
@@ -83,23 +95,49 @@ disconnections. Requires event_store to be set. Defaults to SDK default.
## Classes
-### `StreamableHTTPASGIApp`
+### `FastMCPStreamableHTTPSessionManager`
+
+
+Session manager that scopes resumability storage per transport session.
+
+
+**Methods:**
+
+#### `event_store`
+
+```python
+event_store(self) -> EventStore | None
+```
+
+#### `event_store`
+
+```python
+event_store(self, event_store: EventStore | None) -> None
+```
+
+### `StreamableHTTPASGIApp`
ASGI application wrapper for Streamable HTTP server transport.
-### `StarletteWithLifespan`
+### `HostOriginGuardMiddleware`
+
+
+Validate Host and Origin headers before requests reach MCP sessions.
+
+
+### `StarletteWithLifespan`
**Methods:**
-#### `lifespan`
+#### `lifespan`
```python
lifespan(self) -> Lifespan[Starlette]
```
-### `RequestContextMiddleware`
+### `RequestContextMiddleware`
Middleware that stores each request in a ContextVar and sets transport type.
diff --git a/docs/python-sdk/fastmcp-server-lifespan.mdx b/docs/python-sdk/fastmcp-server-lifespan.mdx
index 08a67ddd9..091836304 100644
--- a/docs/python-sdk/fastmcp-server-lifespan.mdx
+++ b/docs/python-sdk/fastmcp-server-lifespan.mdx
@@ -52,7 +52,7 @@ To compose with existing `@asynccontextmanager` lifespans, wrap them explicitly:
## Functions
-### `lifespan`
+### `lifespan`
```python
lifespan(fn: LifespanFn) -> Lifespan
@@ -74,7 +74,7 @@ a dict for the lifespan context.
## Classes
-### `Lifespan`
+### `Lifespan`
Composable lifespan wrapper.
@@ -83,7 +83,7 @@ Wraps an async generator function and enables composition via the `|` operator.
The wrapped function should yield a dict that becomes part of the lifespan context.
-### `ContextManagerLifespan`
+### `ContextManagerLifespan`
Lifespan wrapper for already-wrapped context manager functions.
@@ -91,7 +91,7 @@ Lifespan wrapper for already-wrapped context manager functions.
Use this for functions already decorated with @asynccontextmanager.
-### `ComposedLifespan`
+### `ComposedLifespan`
Two lifespans composed together.
diff --git a/docs/python-sdk/fastmcp-server-low_level.mdx b/docs/python-sdk/fastmcp-server-low_level.mdx
index 78acc7225..f515849e4 100644
--- a/docs/python-sdk/fastmcp-server-low_level.mdx
+++ b/docs/python-sdk/fastmcp-server-low_level.mdx
@@ -5,17 +5,74 @@ sidebarTitle: low_level
# `fastmcp.server.low_level`
+## Functions
+
+### `client_supports_extension`
+
+```python
+client_supports_extension(session: ServerSession, extension_id: str) -> bool
+```
+
+
+Check whether the connected client supports a given MCP extension.
+
+Inspects the ``extensions`` capability on ``ClientCapabilities`` sent by the
+client during initialization. In v2 the client's initialize params are
+reachable via ``session.client_params``.
+
+SDK v2 declares ``extensions`` as a real field on ``ClientCapabilities``, so
+a client sending ``ClientCapabilities(extensions={...})`` populates the field
+directly. We read that field first and fall back to ``model_extra`` only for
+legacy-serialized clients that carried ``extensions`` as an extra key.
+
+
## Classes
-### `MiddlewareServerSession`
+### `FastMCPServerMiddleware`
-ServerSession that routes initialization requests through FastMCP middleware.
+Root dispatch for the FastMCP middleware chain, in the SDK's middleware layer.
+v2 no longer lets FastMCP subclass ``ServerSession`` (the runner constructs
+it per request), so the old ``MiddlewareServerSession._received_request``
+override is replaced by a ``ServerMiddleware`` — an ordinary entry in the
+SDK's own middleware list. Sitting at the root of dispatch, this
+is the single entry point through which *every* inbound message flows —
+requests, notifications, cancellations, ``initialize``, and even malformed or
+unroutable messages the SDK can still hand us. It binds the FastMCP
+request-context ContextVar and re-applies the app-scoped ``SharedContext`` for
+the whole chain, then runs the FastMCP ``Middleware`` chain so
+``on_message`` / ``on_request`` / ``on_notification`` observe the message.
+
+Dispatch shapes:
+
+- Negotiation runs the *whole* FastMCP chain here: ``initialize`` dispatches
+ through ``on_initialize`` and ``server/discover`` through ``on_discover``.
+ Neither has an interior FastMCP handler adapter, and the SDK serializes both
+ results before returning through its middleware seam, so this root adapter
+ restores core results to typed models before FastMCP middleware observes them.
+- The component methods (``tools/call``, ``tools/list``, ``resources/read``,
+ ...) still run their FastMCP chain *interior*, in the handler adapter, where
+ ``on_call_tool`` receives the typed component result and a tool exception
+ propagates through ``on_message``/``on_request`` exactly where the built-in
+ error/logging/timing middleware expect it. The root dispatch does not re-run the
+ chain for these — it only steps in when such a request fails *before* the
+ interior runs (malformed params, routing), so ``on_message`` still observes
+ the failure.
+- Every other message — all notifications (including ``notifications/cancelled``
+ and ``notifications/initialized``), ``ping``, ``logging/setLevel``, and any
+ unroutable/non-component request — has no interior FastMCP dispatch, so the
+ root dispatch runs the ``"outer"`` pass (``on_message`` plus
+ ``on_request``/``on_notification``) here, wrapping the real SDK dispatch.
+ This closes the long-standing gap where these messages were invisible to
+ FastMCP middleware.
+
+
+### `LowLevelServer`
**Methods:**
-#### `fastmcp`
+#### `fastmcp`
```python
fastmcp(self) -> FastMCP
@@ -24,85 +81,26 @@ fastmcp(self) -> FastMCP
Get the FastMCP instance.
-#### `client_supports_extension`
+#### `create_initialization_options`
```python
-client_supports_extension(self, extension_id: str) -> bool
+create_initialization_options(self, notification_options: NotificationOptions | None = None, experimental_capabilities: dict[str, dict[str, Any]] | None = None, extensions: dict[str, dict[str, Any]] | None = None) -> InitializationOptions
```
-Check if the connected client supports a given MCP extension.
-
-Inspects the ``extensions`` extra field on ``ClientCapabilities``
-sent by the client during initialization.
-
-
-### `LowLevelServer`
-
-**Methods:**
-
-#### `fastmcp`
+#### `get_capabilities`
```python
-fastmcp(self) -> FastMCP
+get_capabilities(self, notification_options: NotificationOptions | None = None, experimental_capabilities: dict[str, dict[str, Any]] | None = None, extensions: dict[str, dict[str, Any]] | None = None) -> mcp_types.ServerCapabilities
```
-Get the FastMCP instance.
+Override to advertise registered extensions and the MCP Apps UI extension.
-
-#### `create_initialization_options`
-
-```python
-create_initialization_options(self, notification_options: NotificationOptions | None = None, experimental_capabilities: dict[str, dict[str, Any]] | None = None, **kwargs: Any) -> InitializationOptions
-```
-
-#### `get_capabilities`
-
-```python
-get_capabilities(self, notification_options: NotificationOptions, experimental_capabilities: dict[str, dict[str, Any]]) -> mcp.types.ServerCapabilities
-```
-
-Override to set capabilities.tasks as a first-class field per SEP-1686.
-
-This ensures task capabilities appear in capabilities.tasks instead of
-capabilities.experimental.tasks, which is required by the MCP spec and
-enables proper task detection by clients like VS Code Copilot 1.107+.
-
-
-#### `run`
-
-```python
-run(self, read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], write_stream: MemoryObjectSendStream[SessionMessage], initialization_options: InitializationOptions, raise_exceptions: bool = False, stateless: bool = False)
-```
-
-Overrides the run method to use the MiddlewareServerSession.
-
-
-#### `read_resource`
-
-```python
-read_resource(self) -> Callable[[Callable[[AnyUrl], Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult]]], Callable[[AnyUrl], Awaitable[mcp.types.ReadResourceResult | mcp.types.CreateTaskResult]]]
-```
-
-Decorator for registering a read_resource handler with CreateTaskResult support.
-
-The MCP SDK's read_resource decorator does not support returning CreateTaskResult
-for background task execution. This decorator wraps the result in ServerResult.
-
-This decorator can be removed once the MCP SDK adds native CreateTaskResult support
-for resources.
-
-
-#### `get_prompt`
-
-```python
-get_prompt(self) -> Callable[[Callable[[str, dict[str, Any] | None], Awaitable[mcp.types.GetPromptResult | mcp.types.CreateTaskResult]]], Callable[[str, dict[str, Any] | None], Awaitable[mcp.types.GetPromptResult | mcp.types.CreateTaskResult]]]
-```
-
-Decorator for registering a get_prompt handler with CreateTaskResult support.
-
-The MCP SDK's get_prompt decorator does not support returning CreateTaskResult
-for background task execution. This decorator wraps the result in ServerResult.
-
-This decorator can be removed once the MCP SDK adds native CreateTaskResult support
-for prompts.
+``ServerCapabilities.extensions`` is a real declared field in v2, so we
+update it directly. The
+`FastMCP(experimental_capabilities=...)` merge also lives here rather
+than in `create_initialization_options`: the modern `server/discover`
+handler calls this directly, without going through
+`create_initialization_options` at all, so merging there only reached
+the handshake-era `initialize` response and silently dropped
+constructor-configured experimental capabilities from `discover`.
diff --git a/docs/python-sdk/fastmcp-server-middleware-__init__.mdx b/docs/python-sdk/fastmcp-server-middleware-__init__.mdx
deleted file mode 100644
index 8583b1df9..000000000
--- a/docs/python-sdk/fastmcp-server-middleware-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.server.middleware`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-server-middleware-authorization.mdx b/docs/python-sdk/fastmcp-server-middleware-authorization.mdx
deleted file mode 100644
index 5bf859a09..000000000
--- a/docs/python-sdk/fastmcp-server-middleware-authorization.mdx
+++ /dev/null
@@ -1,116 +0,0 @@
----
-title: authorization
-sidebarTitle: authorization
----
-
-# `fastmcp.server.middleware.authorization`
-
-
-Authorization middleware for FastMCP.
-
-This module provides middleware-based authorization using callable auth checks.
-AuthMiddleware applies auth checks globally to all components on the server.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.auth import require_scopes, restrict_tag
- from fastmcp.server.middleware import AuthMiddleware
-
- # Require specific scope for all components
- mcp = FastMCP(middleware=[
- AuthMiddleware(auth=require_scopes("api"))
- ])
-
- # Tag-based: components tagged "admin" require "admin" scope
- mcp = FastMCP(middleware=[
- AuthMiddleware(auth=restrict_tag("admin", scopes=["admin"]))
- ])
- ```
-
-
-## Classes
-
-### `AuthMiddleware`
-
-
-Global authorization middleware using callable checks.
-
-This middleware applies auth checks to all components (tools, resources,
-prompts) on the server. It uses the same callable API as component-level
-auth checks.
-
-The middleware:
-- Filters tools/resources/prompts from list responses based on auth checks
-- Checks auth before tool execution, resource read, and prompt render
-- Skips all auth checks for STDIO transport (no OAuth concept)
-
-**Args:**
-- `auth`: A single auth check function or list of check functions.
-All checks must pass for authorization to succeed (AND logic).
-
-
-**Methods:**
-
-#### `on_list_tools`
-
-```python
-on_list_tools(self, context: MiddlewareContext[mt.ListToolsRequest], call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]
-```
-
-Filter tools/list response based on auth checks.
-
-
-#### `on_call_tool`
-
-```python
-on_call_tool(self, context: MiddlewareContext[mt.CallToolRequestParams], call_next: CallNext[mt.CallToolRequestParams, ToolResult]) -> ToolResult
-```
-
-Check auth before tool execution.
-
-
-#### `on_list_resources`
-
-```python
-on_list_resources(self, context: MiddlewareContext[mt.ListResourcesRequest], call_next: CallNext[mt.ListResourcesRequest, Sequence[Resource]]) -> Sequence[Resource]
-```
-
-Filter resources/list response based on auth checks.
-
-
-#### `on_read_resource`
-
-```python
-on_read_resource(self, context: MiddlewareContext[mt.ReadResourceRequestParams], call_next: CallNext[mt.ReadResourceRequestParams, ResourceResult]) -> ResourceResult
-```
-
-Check auth before resource read.
-
-
-#### `on_list_resource_templates`
-
-```python
-on_list_resource_templates(self, context: MiddlewareContext[mt.ListResourceTemplatesRequest], call_next: CallNext[mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]]) -> Sequence[ResourceTemplate]
-```
-
-Filter resource templates/list response based on auth checks.
-
-
-#### `on_list_prompts`
-
-```python
-on_list_prompts(self, context: MiddlewareContext[mt.ListPromptsRequest], call_next: CallNext[mt.ListPromptsRequest, Sequence[Prompt]]) -> Sequence[Prompt]
-```
-
-Filter prompts/list response based on auth checks.
-
-
-#### `on_get_prompt`
-
-```python
-on_get_prompt(self, context: MiddlewareContext[mt.GetPromptRequestParams], call_next: CallNext[mt.GetPromptRequestParams, PromptResult]) -> PromptResult
-```
-
-Check auth before prompt render.
-
diff --git a/docs/python-sdk/fastmcp-server-middleware-caching.mdx b/docs/python-sdk/fastmcp-server-middleware-caching.mdx
deleted file mode 100644
index 66b86999d..000000000
--- a/docs/python-sdk/fastmcp-server-middleware-caching.mdx
+++ /dev/null
@@ -1,221 +0,0 @@
----
-title: caching
-sidebarTitle: caching
----
-
-# `fastmcp.server.middleware.caching`
-
-
-A middleware for response caching.
-
-## Classes
-
-### `CachableResourceContent`
-
-
-A wrapper for ResourceContent that can be cached.
-
-
-### `CachableResourceResult`
-
-
-A wrapper for ResourceResult that can be cached.
-
-
-**Methods:**
-
-#### `get_size`
-
-```python
-get_size(self) -> int
-```
-
-#### `wrap`
-
-```python
-wrap(cls, value: ResourceResult) -> Self
-```
-
-#### `unwrap`
-
-```python
-unwrap(self) -> ResourceResult
-```
-
-### `CachableToolResult`
-
-**Methods:**
-
-#### `wrap`
-
-```python
-wrap(cls, value: ToolResult) -> Self
-```
-
-#### `unwrap`
-
-```python
-unwrap(self) -> ToolResult
-```
-
-### `CachableMessage`
-
-
-A wrapper for Message that can be cached.
-
-
-### `CachablePromptResult`
-
-
-A wrapper for PromptResult that can be cached.
-
-
-**Methods:**
-
-#### `get_size`
-
-```python
-get_size(self) -> int
-```
-
-#### `wrap`
-
-```python
-wrap(cls, value: PromptResult) -> Self
-```
-
-#### `unwrap`
-
-```python
-unwrap(self) -> PromptResult
-```
-
-### `SharedMethodSettings`
-
-
-Shared config for a cache method.
-
-
-### `ListToolsSettings`
-
-
-Configuration options for Tool-related caching.
-
-
-### `ListResourcesSettings`
-
-
-Configuration options for Resource-related caching.
-
-
-### `ListPromptsSettings`
-
-
-Configuration options for Prompt-related caching.
-
-
-### `CallToolSettings`
-
-
-Configuration options for Tool-related caching.
-
-
-### `ReadResourceSettings`
-
-
-Configuration options for Resource-related caching.
-
-
-### `GetPromptSettings`
-
-
-Configuration options for Prompt-related caching.
-
-
-### `ResponseCachingStatistics`
-
-### `ResponseCachingMiddleware`
-
-
-The response caching middleware offers a simple way to cache responses to mcp methods. The Middleware
-supports cache invalidation via notifications from the server. The Middleware implements TTL-based caching
-but cache implementations may offer additional features like LRU eviction, size limits, and more.
-
-When items are retrieved from the cache they will no longer be the original objects, but rather no-op objects
-this means that response caching may not be compatible with other middleware that expects original subclasses.
-
-Notes:
-- Caches `tools/call`, `resources/read`, `prompts/get`, `tools/list`, `resources/list`, and `prompts/list` requests.
-- Cache keys are derived from method name and arguments.
-
-
-**Methods:**
-
-#### `on_list_tools`
-
-```python
-on_list_tools(self, context: MiddlewareContext[mcp.types.ListToolsRequest], call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]
-```
-
-List tools from the cache, if caching is enabled, and the result is in the cache. Otherwise,
-otherwise call the next middleware and store the result in the cache if caching is enabled.
-
-
-#### `on_list_resources`
-
-```python
-on_list_resources(self, context: MiddlewareContext[mcp.types.ListResourcesRequest], call_next: CallNext[mcp.types.ListResourcesRequest, Sequence[Resource]]) -> Sequence[Resource]
-```
-
-List resources from the cache, if caching is enabled, and the result is in the cache. Otherwise,
-otherwise call the next middleware and store the result in the cache if caching is enabled.
-
-
-#### `on_list_prompts`
-
-```python
-on_list_prompts(self, context: MiddlewareContext[mcp.types.ListPromptsRequest], call_next: CallNext[mcp.types.ListPromptsRequest, Sequence[Prompt]]) -> Sequence[Prompt]
-```
-
-List prompts from the cache, if caching is enabled, and the result is in the cache. Otherwise,
-otherwise call the next middleware and store the result in the cache if caching is enabled.
-
-
-#### `on_call_tool`
-
-```python
-on_call_tool(self, context: MiddlewareContext[mcp.types.CallToolRequestParams], call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult]) -> ToolResult
-```
-
-Call a tool from the cache, if caching is enabled, and the result is in the cache. Otherwise,
-otherwise call the next middleware and store the result in the cache if caching is enabled.
-
-
-#### `on_read_resource`
-
-```python
-on_read_resource(self, context: MiddlewareContext[mcp.types.ReadResourceRequestParams], call_next: CallNext[mcp.types.ReadResourceRequestParams, ResourceResult]) -> ResourceResult
-```
-
-Read a resource from the cache, if caching is enabled, and the result is in the cache. Otherwise,
-otherwise call the next middleware and store the result in the cache if caching is enabled.
-
-
-#### `on_get_prompt`
-
-```python
-on_get_prompt(self, context: MiddlewareContext[mcp.types.GetPromptRequestParams], call_next: CallNext[mcp.types.GetPromptRequestParams, PromptResult]) -> PromptResult
-```
-
-Get a prompt from the cache, if caching is enabled, and the result is in the cache. Otherwise,
-otherwise call the next middleware and store the result in the cache if caching is enabled.
-
-
-#### `statistics`
-
-```python
-statistics(self) -> ResponseCachingStatistics
-```
-
-Get the statistics for the cache.
-
diff --git a/docs/python-sdk/fastmcp-server-middleware-dereference.mdx b/docs/python-sdk/fastmcp-server-middleware-dereference.mdx
deleted file mode 100644
index 57c5edf88..000000000
--- a/docs/python-sdk/fastmcp-server-middleware-dereference.mdx
+++ /dev/null
@@ -1,35 +0,0 @@
----
-title: dereference
-sidebarTitle: dereference
----
-
-# `fastmcp.server.middleware.dereference`
-
-
-Middleware that dereferences $ref in JSON schemas before sending to clients.
-
-## Classes
-
-### `DereferenceRefsMiddleware`
-
-
-Dereferences $ref in component schemas before sending to clients.
-
-Some MCP clients (e.g., VS Code Copilot) don't handle JSON Schema $ref
-properly. This middleware inlines all $ref definitions so schemas are
-self-contained. Enabled by default via ``FastMCP(dereference_schemas=True)``.
-
-
-**Methods:**
-
-#### `on_list_tools`
-
-```python
-on_list_tools(self, context: MiddlewareContext[mt.ListToolsRequest], call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]
-```
-
-#### `on_list_resource_templates`
-
-```python
-on_list_resource_templates(self, context: MiddlewareContext[mt.ListResourceTemplatesRequest], call_next: CallNext[mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]]) -> Sequence[ResourceTemplate]
-```
diff --git a/docs/python-sdk/fastmcp-server-middleware-error_handling.mdx b/docs/python-sdk/fastmcp-server-middleware-error_handling.mdx
deleted file mode 100644
index 3089f4d2c..000000000
--- a/docs/python-sdk/fastmcp-server-middleware-error_handling.mdx
+++ /dev/null
@@ -1,60 +0,0 @@
----
-title: error_handling
-sidebarTitle: error_handling
----
-
-# `fastmcp.server.middleware.error_handling`
-
-
-Error handling middleware for consistent error responses and tracking.
-
-## Classes
-
-### `ErrorHandlingMiddleware`
-
-
-Middleware that provides consistent error handling and logging.
-
-Catches exceptions, logs them appropriately, and converts them to
-proper MCP error responses. Also tracks error patterns for monitoring.
-
-
-**Methods:**
-
-#### `on_message`
-
-```python
-on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any
-```
-
-Handle errors for all messages.
-
-
-#### `get_error_stats`
-
-```python
-get_error_stats(self) -> dict[str, int]
-```
-
-Get error statistics for monitoring.
-
-
-### `RetryMiddleware`
-
-
-Middleware that implements automatic retry logic for failed requests.
-
-Retries requests that fail with transient errors, using exponential
-backoff to avoid overwhelming the server or external dependencies.
-
-
-**Methods:**
-
-#### `on_request`
-
-```python
-on_request(self, context: MiddlewareContext, call_next: CallNext) -> Any
-```
-
-Implement retry logic for requests.
-
diff --git a/docs/python-sdk/fastmcp-server-middleware-logging.mdx b/docs/python-sdk/fastmcp-server-middleware-logging.mdx
deleted file mode 100644
index 10f6933a1..000000000
--- a/docs/python-sdk/fastmcp-server-middleware-logging.mdx
+++ /dev/null
@@ -1,58 +0,0 @@
----
-title: logging
-sidebarTitle: logging
----
-
-# `fastmcp.server.middleware.logging`
-
-
-Comprehensive logging middleware for FastMCP servers.
-
-## Functions
-
-### `default_serializer`
-
-```python
-default_serializer(data: Any) -> str
-```
-
-
-The default serializer for Payloads in the logging middleware.
-
-
-## Classes
-
-### `BaseLoggingMiddleware`
-
-
-Base class for logging middleware.
-
-
-**Methods:**
-
-#### `on_message`
-
-```python
-on_message(self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]) -> Any
-```
-
-Log messages for configured methods.
-
-
-### `LoggingMiddleware`
-
-
-Middleware that provides comprehensive request and response logging.
-
-Logs all MCP messages with configurable detail levels. Useful for debugging,
-monitoring, and understanding server usage patterns.
-
-
-### `StructuredLoggingMiddleware`
-
-
-Middleware that provides structured JSON logging for better log analysis.
-
-Outputs structured logs that are easier to parse and analyze with log
-aggregation tools like ELK stack, Splunk, or cloud logging services.
-
diff --git a/docs/python-sdk/fastmcp-server-middleware-middleware.mdx b/docs/python-sdk/fastmcp-server-middleware-middleware.mdx
deleted file mode 100644
index 04a87f554..000000000
--- a/docs/python-sdk/fastmcp-server-middleware-middleware.mdx
+++ /dev/null
@@ -1,112 +0,0 @@
----
-title: middleware
-sidebarTitle: middleware
----
-
-# `fastmcp.server.middleware.middleware`
-
-## Functions
-
-### `make_middleware_wrapper`
-
-```python
-make_middleware_wrapper(middleware: Middleware, call_next: CallNext[T, R]) -> CallNext[T, R]
-```
-
-
-Create a wrapper that applies a single middleware to a context. The
-closure bakes in the middleware and call_next function, so it can be
-passed to other functions that expect a call_next function.
-
-
-## Classes
-
-### `CallNext`
-
-### `MiddlewareContext`
-
-
-Unified context for all middleware operations.
-
-
-**Methods:**
-
-#### `copy`
-
-```python
-copy(self, **kwargs: Any) -> MiddlewareContext[T]
-```
-
-### `Middleware`
-
-
-Base class for FastMCP middleware with dispatching hooks.
-
-
-**Methods:**
-
-#### `on_message`
-
-```python
-on_message(self, context: MiddlewareContext[Any], call_next: CallNext[Any, Any]) -> Any
-```
-
-#### `on_request`
-
-```python
-on_request(self, context: MiddlewareContext[mt.Request[Any, Any]], call_next: CallNext[mt.Request[Any, Any], Any]) -> Any
-```
-
-#### `on_notification`
-
-```python
-on_notification(self, context: MiddlewareContext[mt.Notification[Any, Any]], call_next: CallNext[mt.Notification[Any, Any], Any]) -> Any
-```
-
-#### `on_initialize`
-
-```python
-on_initialize(self, context: MiddlewareContext[mt.InitializeRequest], call_next: CallNext[mt.InitializeRequest, mt.InitializeResult | None]) -> mt.InitializeResult | None
-```
-
-#### `on_call_tool`
-
-```python
-on_call_tool(self, context: MiddlewareContext[mt.CallToolRequestParams], call_next: CallNext[mt.CallToolRequestParams, ToolResult]) -> ToolResult
-```
-
-#### `on_read_resource`
-
-```python
-on_read_resource(self, context: MiddlewareContext[mt.ReadResourceRequestParams], call_next: CallNext[mt.ReadResourceRequestParams, ResourceResult]) -> ResourceResult
-```
-
-#### `on_get_prompt`
-
-```python
-on_get_prompt(self, context: MiddlewareContext[mt.GetPromptRequestParams], call_next: CallNext[mt.GetPromptRequestParams, PromptResult]) -> PromptResult
-```
-
-#### `on_list_tools`
-
-```python
-on_list_tools(self, context: MiddlewareContext[mt.ListToolsRequest], call_next: CallNext[mt.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]
-```
-
-#### `on_list_resources`
-
-```python
-on_list_resources(self, context: MiddlewareContext[mt.ListResourcesRequest], call_next: CallNext[mt.ListResourcesRequest, Sequence[Resource]]) -> Sequence[Resource]
-```
-
-#### `on_list_resource_templates`
-
-```python
-on_list_resource_templates(self, context: MiddlewareContext[mt.ListResourceTemplatesRequest], call_next: CallNext[mt.ListResourceTemplatesRequest, Sequence[ResourceTemplate]]) -> Sequence[ResourceTemplate]
-```
-
-#### `on_list_prompts`
-
-```python
-on_list_prompts(self, context: MiddlewareContext[mt.ListPromptsRequest], call_next: CallNext[mt.ListPromptsRequest, Sequence[Prompt]]) -> Sequence[Prompt]
-```
diff --git a/docs/python-sdk/fastmcp-server-middleware-ping.mdx b/docs/python-sdk/fastmcp-server-middleware-ping.mdx
deleted file mode 100644
index 7ff39d339..000000000
--- a/docs/python-sdk/fastmcp-server-middleware-ping.mdx
+++ /dev/null
@@ -1,32 +0,0 @@
----
-title: ping
-sidebarTitle: ping
----
-
-# `fastmcp.server.middleware.ping`
-
-
-Ping middleware for keeping client connections alive.
-
-## Classes
-
-### `PingMiddleware`
-
-
-Middleware that sends periodic pings to keep client connections alive.
-
-Starts a background ping task on first message from each session. The task
-sends server-to-client pings at the configured interval until the session
-ends.
-
-
-**Methods:**
-
-#### `on_message`
-
-```python
-on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any
-```
-
-Start ping task on first message from a session.
-
diff --git a/docs/python-sdk/fastmcp-server-middleware-rate_limiting.mdx b/docs/python-sdk/fastmcp-server-middleware-rate_limiting.mdx
deleted file mode 100644
index 0ac2c263f..000000000
--- a/docs/python-sdk/fastmcp-server-middleware-rate_limiting.mdx
+++ /dev/null
@@ -1,97 +0,0 @@
----
-title: rate_limiting
-sidebarTitle: rate_limiting
----
-
-# `fastmcp.server.middleware.rate_limiting`
-
-
-Rate limiting middleware for protecting FastMCP servers from abuse.
-
-## Classes
-
-### `RateLimitError`
-
-
-Error raised when rate limit is exceeded.
-
-
-### `TokenBucketRateLimiter`
-
-
-Token bucket implementation for rate limiting.
-
-
-**Methods:**
-
-#### `consume`
-
-```python
-consume(self, tokens: int = 1) -> bool
-```
-
-Try to consume tokens from the bucket.
-
-**Args:**
-- `tokens`: Number of tokens to consume
-
-**Returns:**
-- True if tokens were available and consumed, False otherwise
-
-
-### `SlidingWindowRateLimiter`
-
-
-Sliding window rate limiter implementation.
-
-
-**Methods:**
-
-#### `is_allowed`
-
-```python
-is_allowed(self) -> bool
-```
-
-Check if a request is allowed.
-
-
-### `RateLimitingMiddleware`
-
-
-Middleware that implements rate limiting to prevent server abuse.
-
-Uses a token bucket algorithm by default, allowing for burst traffic
-while maintaining a sustainable long-term rate.
-
-
-**Methods:**
-
-#### `on_request`
-
-```python
-on_request(self, context: MiddlewareContext, call_next: CallNext) -> Any
-```
-
-Apply rate limiting to requests.
-
-
-### `SlidingWindowRateLimitingMiddleware`
-
-
-Middleware that implements sliding window rate limiting.
-
-Uses a sliding window approach which provides more precise rate limiting
-but uses more memory to track individual request timestamps.
-
-
-**Methods:**
-
-#### `on_request`
-
-```python
-on_request(self, context: MiddlewareContext, call_next: CallNext) -> Any
-```
-
-Apply sliding window rate limiting to requests.
-
diff --git a/docs/python-sdk/fastmcp-server-middleware-response_limiting.mdx b/docs/python-sdk/fastmcp-server-middleware-response_limiting.mdx
deleted file mode 100644
index 0f344100b..000000000
--- a/docs/python-sdk/fastmcp-server-middleware-response_limiting.mdx
+++ /dev/null
@@ -1,32 +0,0 @@
----
-title: response_limiting
-sidebarTitle: response_limiting
----
-
-# `fastmcp.server.middleware.response_limiting`
-
-
-Response limiting middleware for controlling tool response sizes.
-
-## Classes
-
-### `ResponseLimitingMiddleware`
-
-
-Middleware that limits the response size of tool calls.
-
-Intercepts tool call responses and enforces size limits. If a response
-exceeds the limit, it extracts text content, truncates it, and returns
-a single TextContent block.
-
-
-**Methods:**
-
-#### `on_call_tool`
-
-```python
-on_call_tool(self, context: MiddlewareContext[mt.CallToolRequestParams], call_next: CallNext[mt.CallToolRequestParams, ToolResult]) -> ToolResult
-```
-
-Intercept tool calls and limit response size.
-
diff --git a/docs/python-sdk/fastmcp-server-middleware-timing.mdx b/docs/python-sdk/fastmcp-server-middleware-timing.mdx
deleted file mode 100644
index 533ff5de8..000000000
--- a/docs/python-sdk/fastmcp-server-middleware-timing.mdx
+++ /dev/null
@@ -1,105 +0,0 @@
----
-title: timing
-sidebarTitle: timing
----
-
-# `fastmcp.server.middleware.timing`
-
-
-Timing middleware for measuring and logging request performance.
-
-## Classes
-
-### `TimingMiddleware`
-
-
-Middleware that logs the execution time of requests.
-
-Only measures and logs timing for request messages (not notifications).
-Provides insights into performance characteristics of your MCP server.
-
-
-**Methods:**
-
-#### `on_request`
-
-```python
-on_request(self, context: MiddlewareContext, call_next: CallNext) -> Any
-```
-
-Time request execution and log the results.
-
-
-### `DetailedTimingMiddleware`
-
-
-Enhanced timing middleware with per-operation breakdowns.
-
-Provides detailed timing information for different types of MCP operations,
-allowing you to identify performance bottlenecks in specific operations.
-
-
-**Methods:**
-
-#### `on_call_tool`
-
-```python
-on_call_tool(self, context: MiddlewareContext, call_next: CallNext) -> Any
-```
-
-Time tool execution.
-
-
-#### `on_read_resource`
-
-```python
-on_read_resource(self, context: MiddlewareContext, call_next: CallNext) -> Any
-```
-
-Time resource reading.
-
-
-#### `on_get_prompt`
-
-```python
-on_get_prompt(self, context: MiddlewareContext, call_next: CallNext) -> Any
-```
-
-Time prompt retrieval.
-
-
-#### `on_list_tools`
-
-```python
-on_list_tools(self, context: MiddlewareContext, call_next: CallNext) -> Any
-```
-
-Time tool listing.
-
-
-#### `on_list_resources`
-
-```python
-on_list_resources(self, context: MiddlewareContext, call_next: CallNext) -> Any
-```
-
-Time resource listing.
-
-
-#### `on_list_resource_templates`
-
-```python
-on_list_resource_templates(self, context: MiddlewareContext, call_next: CallNext) -> Any
-```
-
-Time resource template listing.
-
-
-#### `on_list_prompts`
-
-```python
-on_list_prompts(self, context: MiddlewareContext, call_next: CallNext) -> Any
-```
-
-Time prompt listing.
-
diff --git a/docs/python-sdk/fastmcp-server-middleware-tool_injection.mdx b/docs/python-sdk/fastmcp-server-middleware-tool_injection.mdx
deleted file mode 100644
index 6c9c01346..000000000
--- a/docs/python-sdk/fastmcp-server-middleware-tool_injection.mdx
+++ /dev/null
@@ -1,91 +0,0 @@
----
-title: tool_injection
-sidebarTitle: tool_injection
----
-
-# `fastmcp.server.middleware.tool_injection`
-
-
-A middleware for injecting tools into the MCP server context.
-
-## Functions
-
-### `list_prompts`
-
-```python
-list_prompts(context: Context) -> list[Prompt]
-```
-
-
-List prompts available on the server.
-
-
-### `get_prompt`
-
-```python
-get_prompt(context: Context, name: Annotated[str, 'The name of the prompt to render.'], arguments: Annotated[dict[str, Any] | None, 'The arguments to pass to the prompt.'] = None) -> mcp.types.GetPromptResult
-```
-
-
-Render a prompt available on the server.
-
-
-### `list_resources`
-
-```python
-list_resources(context: Context) -> list[mcp.types.Resource]
-```
-
-
-List resources available on the server.
-
-
-### `read_resource`
-
-```python
-read_resource(context: Context, uri: Annotated[AnyUrl | str, 'The URI of the resource to read.']) -> ResourceResult
-```
-
-
-Read a resource available on the server.
-
-
-## Classes
-
-### `ToolInjectionMiddleware`
-
-
-A middleware for injecting tools into the context.
-
-
-**Methods:**
-
-#### `on_list_tools`
-
-```python
-on_list_tools(self, context: MiddlewareContext[mcp.types.ListToolsRequest], call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]
-```
-
-Inject tools into the response.
-
-
-#### `on_call_tool`
-
-```python
-on_call_tool(self, context: MiddlewareContext[mcp.types.CallToolRequestParams], call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult]) -> ToolResult
-```
-
-Intercept tool calls to injected tools.
-
-
-### `PromptToolMiddleware`
-
-
-A middleware for injecting prompts as tools into the context.
-
-
-### `ResourceToolMiddleware`
-
-
-A middleware for injecting resources as tools into the context.
-
diff --git a/docs/python-sdk/fastmcp-server-mixins-lifespan.mdx b/docs/python-sdk/fastmcp-server-mixins-lifespan.mdx
deleted file mode 100644
index 9d0eec22e..000000000
--- a/docs/python-sdk/fastmcp-server-mixins-lifespan.mdx
+++ /dev/null
@@ -1,30 +0,0 @@
----
-title: lifespan
-sidebarTitle: lifespan
----
-
-# `fastmcp.server.mixins.lifespan`
-
-
-Lifespan and Docket task infrastructure for FastMCP Server.
-
-## Classes
-
-### `LifespanMixin`
-
-
-Mixin providing lifespan and Docket task infrastructure for FastMCP.
-
-
-**Methods:**
-
-#### `docket`
-
-```python
-docket(self: FastMCP) -> Docket | None
-```
-
-Get the Docket instance if Docket support is enabled.
-
-Returns None if Docket is not enabled or server hasn't been started yet.
-
diff --git a/docs/python-sdk/fastmcp-server-mixins-mcp_operations.mdx b/docs/python-sdk/fastmcp-server-mixins-mcp_operations.mdx
deleted file mode 100644
index 0b6bef529..000000000
--- a/docs/python-sdk/fastmcp-server-mixins-mcp_operations.mdx
+++ /dev/null
@@ -1,23 +0,0 @@
----
-title: mcp_operations
-sidebarTitle: mcp_operations
----
-
-# `fastmcp.server.mixins.mcp_operations`
-
-
-MCP protocol handler setup and wire-format handlers for FastMCP Server.
-
-## Classes
-
-### `MCPOperationsMixin`
-
-
-Mixin providing MCP protocol handler setup and wire-format handlers.
-
-Note: Methods registered with SDK decorators (e.g., _list_tools_mcp, _call_tool_mcp)
-cannot use `self: FastMCP` type hints because the SDK's `get_type_hints()` fails
-to resolve FastMCP at runtime (it's only available under TYPE_CHECKING). When
-type hints fail to resolve, the SDK falls back to calling handlers with no arguments.
-These methods use untyped `self` to avoid this issue.
-
diff --git a/docs/python-sdk/fastmcp-server-mixins-transport.mdx b/docs/python-sdk/fastmcp-server-mixins-transport.mdx
deleted file mode 100644
index c5cc4e2fd..000000000
--- a/docs/python-sdk/fastmcp-server-mixins-transport.mdx
+++ /dev/null
@@ -1,131 +0,0 @@
----
-title: transport
-sidebarTitle: transport
----
-
-# `fastmcp.server.mixins.transport`
-
-
-Transport-related methods for FastMCP Server.
-
-## Classes
-
-### `TransportMixin`
-
-
-Mixin providing transport-related methods for FastMCP.
-
-Includes HTTP/stdio/SSE transport handling and custom HTTP routes.
-
-
-**Methods:**
-
-#### `run_async`
-
-```python
-run_async(self: FastMCP, transport: Transport | None = None, show_banner: bool | None = None, **transport_kwargs: Any) -> None
-```
-
-Run the FastMCP server asynchronously.
-
-**Args:**
-- `transport`: Transport protocol to use ("stdio", "http", "sse", or "streamable-http")
-- `show_banner`: Whether to display the server banner. If None, uses the
-FASTMCP_SHOW_SERVER_BANNER setting (default\: True).
-
-
-#### `run`
-
-```python
-run(self: FastMCP, transport: Transport | None = None, show_banner: bool | None = None, **transport_kwargs: Any) -> None
-```
-
-Run the FastMCP server. Note this is a synchronous function.
-
-**Args:**
-- `transport`: Transport protocol to use ("http", "stdio", "sse", or "streamable-http")
-- `show_banner`: Whether to display the server banner. If None, uses the
-FASTMCP_SHOW_SERVER_BANNER setting (default\: True).
-
-
-#### `custom_route`
-
-```python
-custom_route(self: FastMCP, path: str, methods: list[str], name: str | None = None, include_in_schema: bool = True) -> Callable[[Callable[[Request], Awaitable[Response]]], Callable[[Request], Awaitable[Response]]]
-```
-
-Decorator to register a custom HTTP route on the FastMCP server.
-
-Allows adding arbitrary HTTP endpoints outside the standard MCP protocol,
-which can be useful for OAuth callbacks, health checks, or admin APIs.
-The handler function must be an async function that accepts a Starlette
-Request and returns a Response.
-
-**Args:**
-- `path`: URL path for the route (e.g., "/auth/callback")
-- `methods`: List of HTTP methods to support (e.g., ["GET", "POST"])
-- `name`: Optional name for the route (to reference this route with
-Starlette's reverse URL lookup feature)
-- `include_in_schema`: Whether to include in OpenAPI schema, defaults to True
-
-
-#### `run_stdio_async`
-
-```python
-run_stdio_async(self: FastMCP, show_banner: bool = True, log_level: str | None = None, stateless: bool = False) -> None
-```
-
-Run the server using stdio transport.
-
-**Args:**
-- `show_banner`: Whether to display the server banner
-- `log_level`: Log level for the server
-- `stateless`: Whether to run in stateless mode (no session initialization)
-
-
-#### `run_http_async`
-
-```python
-run_http_async(self: FastMCP, show_banner: bool = True, transport: Literal['http', 'streamable-http', 'sse'] = 'http', host: str | None = None, port: int | None = None, log_level: str | None = None, path: str | None = None, uvicorn_config: dict[str, Any] | None = None, middleware: list[ASGIMiddleware] | None = None, json_response: bool | None = None, stateless_http: bool | None = None, stateless: bool | None = None) -> None
-```
-
-Run the server using HTTP transport.
-
-**Args:**
-- `transport`: Transport protocol to use - "http" (default), "streamable-http", or "sse"
-- `host`: Host address to bind to (defaults to settings.host)
-- `port`: Port to bind to (defaults to settings.port)
-- `log_level`: Log level for the server (defaults to settings.log_level)
-- `path`: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
-- `uvicorn_config`: Additional configuration for the Uvicorn server
-- `middleware`: A list of middleware to apply to the app
-- `json_response`: Whether to use JSON response format (defaults to settings.json_response)
-- `stateless_http`: Whether to use stateless HTTP (defaults to settings.stateless_http)
-- `stateless`: Alias for stateless_http for CLI consistency
-
-
-#### `http_app`
-
-```python
-http_app(self: FastMCP, path: str | None = None, middleware: list[ASGIMiddleware] | None = None, json_response: bool | None = None, stateless_http: bool | None = None, transport: Literal['http', 'streamable-http', 'sse'] = 'http', event_store: EventStore | None = None, retry_interval: int | None = None) -> StarletteWithLifespan
-```
-
-Create a Starlette app using the specified HTTP transport.
-
-**Args:**
-- `path`: The path for the HTTP endpoint
-- `middleware`: A list of middleware to apply to the app
-- `json_response`: Whether to use JSON response format
-- `stateless_http`: Whether to use stateless mode (new transport per request)
-- `transport`: Transport protocol to use - "http", "streamable-http", or "sse"
-- `event_store`: Optional event store for SSE polling/resumability. When set,
-enables clients to reconnect and resume receiving events after
-server-initiated disconnections. Only used with streamable-http transport.
-- `retry_interval`: Optional retry interval in milliseconds for SSE polling.
-Controls how quickly clients should reconnect after server-initiated
-disconnections. Requires event_store to be set. Only used with
-streamable-http transport.
-
-**Returns:**
-- A Starlette application configured with the specified transport
-
diff --git a/docs/python-sdk/fastmcp-server-mixins-__init__.mdx b/docs/python-sdk/fastmcp-server-mixins.mdx
similarity index 62%
rename from docs/python-sdk/fastmcp-server-mixins-__init__.mdx
rename to docs/python-sdk/fastmcp-server-mixins.mdx
index d35f9fc06..9734da93c 100644
--- a/docs/python-sdk/fastmcp-server-mixins-__init__.mdx
+++ b/docs/python-sdk/fastmcp-server-mixins.mdx
@@ -1,6 +1,6 @@
---
-title: __init__
-sidebarTitle: __init__
+title: mixins
+sidebarTitle: mixins
---
# `fastmcp.server.mixins`
diff --git a/docs/python-sdk/fastmcp-server-openapi-__init__.mdx b/docs/python-sdk/fastmcp-server-openapi-__init__.mdx
deleted file mode 100644
index af9cd5517..000000000
--- a/docs/python-sdk/fastmcp-server-openapi-__init__.mdx
+++ /dev/null
@@ -1,27 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.server.openapi`
-
-
-OpenAPI server implementation for FastMCP.
-
-.. deprecated::
- This module is deprecated. Import from fastmcp.server.providers.openapi instead.
-
-The recommended approach is to use OpenAPIProvider with FastMCP:
-
- from fastmcp import FastMCP
- from fastmcp.server.providers.openapi import OpenAPIProvider
- import httpx
-
- client = httpx.AsyncClient(base_url="https://api.example.com")
- provider = OpenAPIProvider(openapi_spec=spec, client=client)
-
- mcp = FastMCP("My API Server")
- mcp.add_provider(provider)
-
-FastMCPOpenAPI is still available but deprecated.
-
diff --git a/docs/python-sdk/fastmcp-server-openapi-components.mdx b/docs/python-sdk/fastmcp-server-openapi-components.mdx
deleted file mode 100644
index 320cc7092..000000000
--- a/docs/python-sdk/fastmcp-server-openapi-components.mdx
+++ /dev/null
@@ -1,12 +0,0 @@
----
-title: components
-sidebarTitle: components
----
-
-# `fastmcp.server.openapi.components`
-
-
-OpenAPI component implementations - backwards compatibility stub.
-
-This module is deprecated. Import from fastmcp.server.providers.openapi instead.
-
diff --git a/docs/python-sdk/fastmcp-server-openapi-routing.mdx b/docs/python-sdk/fastmcp-server-openapi-routing.mdx
deleted file mode 100644
index 650a4a497..000000000
--- a/docs/python-sdk/fastmcp-server-openapi-routing.mdx
+++ /dev/null
@@ -1,13 +0,0 @@
----
-title: routing
-sidebarTitle: routing
----
-
-# `fastmcp.server.openapi.routing`
-
-
-Route mapping logic for OpenAPI operations.
-
-.. deprecated::
- This module is deprecated. Import from fastmcp.server.providers.openapi instead.
-
diff --git a/docs/python-sdk/fastmcp-server-openapi-server.mdx b/docs/python-sdk/fastmcp-server-openapi-server.mdx
deleted file mode 100644
index 4f751e090..000000000
--- a/docs/python-sdk/fastmcp-server-openapi-server.mdx
+++ /dev/null
@@ -1,43 +0,0 @@
----
-title: server
-sidebarTitle: server
----
-
-# `fastmcp.server.openapi.server`
-
-
-FastMCPOpenAPI - backwards compatibility wrapper.
-
-This class is deprecated. Use FastMCP with OpenAPIProvider instead:
-
- from fastmcp import FastMCP
- from fastmcp.server.providers.openapi import OpenAPIProvider
- import httpx
-
- client = httpx.AsyncClient(base_url="https://api.example.com")
- provider = OpenAPIProvider(openapi_spec=spec, client=client)
- mcp = FastMCP("My API Server", providers=[provider])
-
-
-## Classes
-
-### `FastMCPOpenAPI`
-
-
-FastMCP server implementation that creates components from an OpenAPI schema.
-
-.. deprecated::
- Use FastMCP with OpenAPIProvider instead. This class will be
- removed in a future version.
-
-Example (deprecated):
- ```python
- from fastmcp.server.openapi import FastMCPOpenAPI
- import httpx
-
- server = FastMCPOpenAPI(
- openapi_spec=spec,
- client=httpx.AsyncClient(),
- )
- ```
-
diff --git a/docs/python-sdk/fastmcp-server-providers-aggregate.mdx b/docs/python-sdk/fastmcp-server-providers-aggregate.mdx
deleted file mode 100644
index e0a8103da..000000000
--- a/docs/python-sdk/fastmcp-server-providers-aggregate.mdx
+++ /dev/null
@@ -1,83 +0,0 @@
----
-title: aggregate
-sidebarTitle: aggregate
----
-
-# `fastmcp.server.providers.aggregate`
-
-
-AggregateProvider for combining multiple providers into one.
-
-This module provides `AggregateProvider`, a utility class that presents
-multiple providers as a single unified provider. Useful when you want to
-combine custom providers without creating a full FastMCP server.
-
-Example:
- ```python
- from fastmcp.server.providers import AggregateProvider
-
- # Combine multiple providers into one
- combined = AggregateProvider()
- combined.add_provider(provider1)
- combined.add_provider(provider2, namespace="api") # Tools become "api_foo"
-
- # Use like any other provider
- tools = await combined.list_tools()
- ```
-
-
-## Classes
-
-### `AggregateProvider`
-
-
-Utility provider that combines multiple providers into one.
-
-Components are aggregated from all providers. For get_* operations,
-providers are queried in parallel and the highest version is returned.
-
-When adding providers with a namespace, wrap_transform() is used to apply
-the Namespace transform. This means namespace transformation is handled
-by the wrapped provider, not by AggregateProvider.
-
-Errors from individual providers are logged and skipped (graceful degradation).
-
-
-**Methods:**
-
-#### `add_provider`
-
-```python
-add_provider(self, provider: Provider) -> None
-```
-
-Add a provider with optional namespace.
-
-If the provider is a FastMCP server, it's automatically wrapped in
-FastMCPProvider to ensure middleware is invoked correctly.
-
-**Args:**
-- `provider`: The provider to add.
-- `namespace`: Optional namespace prefix. When set\:
-- Tools become "namespace_toolname"
-- Resources become "protocol\://namespace/path"
-- Prompts become "namespace_promptname"
-
-
-#### `get_tasks`
-
-```python
-get_tasks(self) -> Sequence[FastMCPComponent]
-```
-
-Get all task-eligible components from all providers.
-
-
-#### `lifespan`
-
-```python
-lifespan(self) -> AsyncIterator[None]
-```
-
-Combine lifespans of all providers.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-base.mdx b/docs/python-sdk/fastmcp-server-providers-base.mdx
deleted file mode 100644
index 09d02595b..000000000
--- a/docs/python-sdk/fastmcp-server-providers-base.mdx
+++ /dev/null
@@ -1,306 +0,0 @@
----
-title: base
-sidebarTitle: base
----
-
-# `fastmcp.server.providers.base`
-
-
-Base Provider class for dynamic MCP components.
-
-This module provides the `Provider` abstraction for providing tools,
-resources, and prompts dynamically at runtime.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.providers import Provider
- from fastmcp.tools import Tool
-
- class DatabaseProvider(Provider):
- def __init__(self, db_url: str):
- super().__init__()
- self.db = Database(db_url)
-
- async def _list_tools(self) -> list[Tool]:
- rows = await self.db.fetch("SELECT * FROM tools")
- return [self._make_tool(row) for row in rows]
-
- async def _get_tool(self, name: str) -> Tool | None:
- row = await self.db.fetchone("SELECT * FROM tools WHERE name = ?", name)
- return self._make_tool(row) if row else None
-
- mcp = FastMCP("Server", providers=[DatabaseProvider(db_url)])
- ```
-
-
-## Classes
-
-### `Provider`
-
-
-Base class for dynamic component providers.
-
-Subclass and override whichever methods you need. Default implementations
-return empty lists / None, so you only need to implement what your provider
-supports.
-
-
-**Methods:**
-
-#### `transforms`
-
-```python
-transforms(self) -> list[Transform]
-```
-
-All transforms applied to components from this provider.
-
-
-#### `add_transform`
-
-```python
-add_transform(self, transform: Transform) -> None
-```
-
-Add a transform to this provider.
-
-Transforms modify components (tools, resources, prompts) as they flow
-through the provider. They're applied in order - first added is innermost.
-
-**Args:**
-- `transform`: The transform to add.
-
-
-#### `wrap_transform`
-
-```python
-wrap_transform(self, transform: Transform) -> Provider
-```
-
-Return a new provider with this transform applied (immutable).
-
-Unlike add_transform() which mutates this provider, wrap_transform()
-returns a new provider that wraps this one. The original provider
-is unchanged.
-
-This is useful when you want to apply transforms without side effects,
-such as adding the same provider to multiple aggregators with different
-namespaces.
-
-**Args:**
-- `transform`: The transform to apply.
-
-**Returns:**
-- A new provider that wraps this one with the transform applied.
-
-
-#### `list_tools`
-
-```python
-list_tools(self) -> Sequence[Tool]
-```
-
-List tools with all transforms applied.
-
-Applies transforms sequentially: base → transforms (in order).
-Each transform receives the result from the previous transform.
-Components may be marked as disabled but are NOT filtered here -
-filtering happens at the server level to allow session transforms to override.
-
-**Returns:**
-- Transformed sequence of tools (including disabled ones).
-
-
-#### `get_tool`
-
-```python
-get_tool(self, name: str, version: VersionSpec | None = None) -> Tool | None
-```
-
-Get tool by transformed name with all transforms applied.
-
-Note: This method does NOT filter disabled components. The Server
-(FastMCP) performs enabled filtering after all transforms complete,
-allowing session-level transforms to override provider-level disables.
-
-**Args:**
-- `name`: The transformed tool name to look up.
-- `version`: Optional version filter. If None, returns highest version.
-
-**Returns:**
-- The tool if found (may be marked disabled), None if not found.
-
-
-#### `list_resources`
-
-```python
-list_resources(self) -> Sequence[Resource]
-```
-
-List resources with all transforms applied.
-
-Components may be marked as disabled but are NOT filtered here.
-
-
-#### `get_resource`
-
-```python
-get_resource(self, uri: str, version: VersionSpec | None = None) -> Resource | None
-```
-
-Get resource by transformed URI with all transforms applied.
-
-Note: This method does NOT filter disabled components. The Server
-(FastMCP) performs enabled filtering after all transforms complete.
-
-**Args:**
-- `uri`: The transformed resource URI to look up.
-- `version`: Optional version filter. If None, returns highest version.
-
-**Returns:**
-- The resource if found (may be marked disabled), None if not found.
-
-
-#### `list_resource_templates`
-
-```python
-list_resource_templates(self) -> Sequence[ResourceTemplate]
-```
-
-List resource templates with all transforms applied.
-
-Components may be marked as disabled but are NOT filtered here.
-
-
-#### `get_resource_template`
-
-```python
-get_resource_template(self, uri: str, version: VersionSpec | None = None) -> ResourceTemplate | None
-```
-
-Get resource template by transformed URI with all transforms applied.
-
-Note: This method does NOT filter disabled components. The Server
-(FastMCP) performs enabled filtering after all transforms complete.
-
-**Args:**
-- `uri`: The transformed template URI to look up.
-- `version`: Optional version filter. If None, returns highest version.
-
-**Returns:**
-- The template if found (may be marked disabled), None if not found.
-
-
-#### `list_prompts`
-
-```python
-list_prompts(self) -> Sequence[Prompt]
-```
-
-List prompts with all transforms applied.
-
-Components may be marked as disabled but are NOT filtered here.
-
-
-#### `get_prompt`
-
-```python
-get_prompt(self, name: str, version: VersionSpec | None = None) -> Prompt | None
-```
-
-Get prompt by transformed name with all transforms applied.
-
-Note: This method does NOT filter disabled components. The Server
-(FastMCP) performs enabled filtering after all transforms complete.
-
-**Args:**
-- `name`: The transformed prompt name to look up.
-- `version`: Optional version filter. If None, returns highest version.
-
-**Returns:**
-- The prompt if found (may be marked disabled), None if not found.
-
-
-#### `get_tasks`
-
-```python
-get_tasks(self) -> Sequence[FastMCPComponent]
-```
-
-Return components that should be registered as background tasks.
-
-Override to customize which components are task-eligible.
-Default calls list_* methods, applies provider transforms, and filters
-for components with task_config.mode != 'forbidden'.
-
-Used by the server during startup to register functions with Docket.
-
-
-#### `lifespan`
-
-```python
-lifespan(self) -> AsyncIterator[None]
-```
-
-User-overridable lifespan for custom setup and teardown.
-
-Override this method to perform provider-specific initialization
-like opening database connections, setting up external resources,
-or other state management needed for the provider's lifetime.
-
-The lifespan scope matches the server's lifespan - code before yield
-runs at startup, code after yield runs at shutdown.
-
-
-#### `enable`
-
-```python
-enable(self) -> Self
-```
-
-Enable components matching all specified criteria.
-
-Adds a visibility transform that marks matching components as enabled.
-Later transforms override earlier ones, so enable after disable makes
-the component enabled.
-
-With only=True, switches to allowlist mode - first disables everything,
-then enables matching components.
-
-**Args:**
-- `names`: Component names or URIs to enable.
-- `keys`: Component keys to enable (e.g., {"tool\:my_tool@v1"}).
-- `version`: Component version spec to enable (e.g., VersionSpec(eq="v1") or
-VersionSpec(gte="v2")). Unversioned components will not match.
-- `tags`: Enable components with these tags.
-- `components`: Component types to include (e.g., {"tool", "prompt"}).
-- `only`: If True, ONLY enable matching components (allowlist mode).
-
-**Returns:**
-- Self for method chaining.
-
-
-#### `disable`
-
-```python
-disable(self) -> Self
-```
-
-Disable components matching all specified criteria.
-
-Adds a visibility transform that marks matching components as disabled.
-Components can be re-enabled by calling enable() with matching criteria
-(the later transform wins).
-
-**Args:**
-- `names`: Component names or URIs to disable.
-- `keys`: Component keys to disable (e.g., {"tool\:my_tool@v1"}).
-- `version`: Component version spec to disable (e.g., VersionSpec(eq="v1") or
-VersionSpec(gte="v2")). Unversioned components will not match.
-- `tags`: Disable components with these tags.
-- `components`: Component types to include (e.g., {"tool", "prompt"}).
-
-**Returns:**
-- Self for method chaining.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-fastmcp_provider.mdx b/docs/python-sdk/fastmcp-server-providers-fastmcp_provider.mdx
deleted file mode 100644
index 84d60da3d..000000000
--- a/docs/python-sdk/fastmcp-server-providers-fastmcp_provider.mdx
+++ /dev/null
@@ -1,238 +0,0 @@
----
-title: fastmcp_provider
-sidebarTitle: fastmcp_provider
----
-
-# `fastmcp.server.providers.fastmcp_provider`
-
-
-FastMCPProvider for wrapping FastMCP servers as providers.
-
-This module provides the `FastMCPProvider` class that wraps a FastMCP server
-and exposes its components through the Provider interface.
-
-It also provides FastMCPProvider* component classes that delegate execution to
-the wrapped server's middleware, ensuring middleware runs when components are
-executed.
-
-
-## Classes
-
-### `FastMCPProviderTool`
-
-
-Tool that delegates execution to a wrapped server's middleware.
-
-When `run()` is called, this tool invokes the wrapped server's
-`_call_tool_middleware()` method, ensuring the server's middleware
-chain is executed.
-
-
-**Methods:**
-
-#### `wrap`
-
-```python
-wrap(cls, server: Any, tool: Tool) -> FastMCPProviderTool
-```
-
-Wrap a Tool to delegate execution to the server's middleware.
-
-
-#### `run`
-
-```python
-run(self, arguments: dict[str, Any]) -> ToolResult
-```
-
-Delegate to child server's call_tool() without task_meta.
-
-This is called when the tool is used within a TransformedTool
-forwarding function or other contexts where task_meta is not available.
-
-
-#### `get_span_attributes`
-
-```python
-get_span_attributes(self) -> dict[str, Any]
-```
-
-### `FastMCPProviderResource`
-
-
-Resource that delegates reading to a wrapped server's read_resource().
-
-When `read()` is called, this resource invokes the wrapped server's
-`read_resource()` method, ensuring the server's middleware chain is executed.
-
-
-**Methods:**
-
-#### `wrap`
-
-```python
-wrap(cls, server: Any, resource: Resource) -> FastMCPProviderResource
-```
-
-Wrap a Resource to delegate reading to the server's middleware.
-
-
-#### `get_span_attributes`
-
-```python
-get_span_attributes(self) -> dict[str, Any]
-```
-
-### `FastMCPProviderPrompt`
-
-
-Prompt that delegates rendering to a wrapped server's render_prompt().
-
-When `render()` is called, this prompt invokes the wrapped server's
-`render_prompt()` method, ensuring the server's middleware chain is executed.
-
-
-**Methods:**
-
-#### `wrap`
-
-```python
-wrap(cls, server: Any, prompt: Prompt) -> FastMCPProviderPrompt
-```
-
-Wrap a Prompt to delegate rendering to the server's middleware.
-
-
-#### `render`
-
-```python
-render(self, arguments: dict[str, Any] | None = None) -> PromptResult
-```
-
-Delegate to child server's render_prompt() without task_meta.
-
-This is called when the prompt is used within a transformed context
-or other contexts where task_meta is not available.
-
-
-#### `get_span_attributes`
-
-```python
-get_span_attributes(self) -> dict[str, Any]
-```
-
-### `FastMCPProviderResourceTemplate`
-
-
-Resource template that creates FastMCPProviderResources.
-
-When `create_resource()` is called, this template creates a
-FastMCPProviderResource that will invoke the wrapped server's middleware
-when read.
-
-
-**Methods:**
-
-#### `wrap`
-
-```python
-wrap(cls, server: Any, template: ResourceTemplate) -> FastMCPProviderResourceTemplate
-```
-
-Wrap a ResourceTemplate to create FastMCPProviderResources.
-
-
-#### `create_resource`
-
-```python
-create_resource(self, uri: str, params: dict[str, Any]) -> Resource
-```
-
-Create a FastMCPProviderResource for the given URI.
-
-The `uri` is the external/transformed URI (e.g., with namespace prefix).
-We use `_original_uri_template` with `params` to construct the internal
-URI that the nested server understands.
-
-
-#### `read`
-
-```python
-read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult
-```
-
-Read the resource content for background task execution.
-
-Reads the resource via the wrapped server and returns the ResourceResult.
-This method is called by Docket during background task execution.
-
-
-#### `register_with_docket`
-
-```python
-register_with_docket(self, docket: Docket) -> None
-```
-
-No-op: the child's actual template is registered via get_tasks().
-
-
-#### `add_to_docket`
-
-```python
-add_to_docket(self, docket: Docket, params: dict[str, Any], **kwargs: Any) -> Execution
-```
-
-Schedule this template for background execution via docket.
-
-The child's FunctionResourceTemplate.fn is registered (via get_tasks),
-and it expects splatted **kwargs, so we splat params here.
-
-
-#### `get_span_attributes`
-
-```python
-get_span_attributes(self) -> dict[str, Any]
-```
-
-### `FastMCPProvider`
-
-
-Provider that wraps a FastMCP server.
-
-This provider enables mounting one FastMCP server onto another, exposing
-the mounted server's tools, resources, and prompts through the parent
-server.
-
-Components returned by this provider are wrapped in FastMCPProvider*
-classes that delegate execution to the wrapped server's middleware chain.
-This ensures middleware runs when components are executed.
-
-
-**Methods:**
-
-#### `get_tasks`
-
-```python
-get_tasks(self) -> Sequence[FastMCPComponent]
-```
-
-Return task-eligible components from the mounted server.
-
-Returns the child's ACTUAL components (not wrapped) so their actual
-functions get registered with Docket. Gets components with child
-server's transforms applied, then applies this provider's transforms
-for correct registration keys.
-
-
-#### `lifespan`
-
-```python
-lifespan(self) -> AsyncIterator[None]
-```
-
-Start the mounted server's user lifespan.
-
-This starts only the wrapped server's user-defined lifespan, NOT its
-full _lifespan_manager() (which includes Docket). The parent server's
-Docket handles all background tasks.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-filesystem.mdx b/docs/python-sdk/fastmcp-server-providers-filesystem.mdx
deleted file mode 100644
index d5b589e1e..000000000
--- a/docs/python-sdk/fastmcp-server-providers-filesystem.mdx
+++ /dev/null
@@ -1,54 +0,0 @@
----
-title: filesystem
-sidebarTitle: filesystem
----
-
-# `fastmcp.server.providers.filesystem`
-
-
-FileSystemProvider for filesystem-based component discovery.
-
-FileSystemProvider scans a directory for Python files, imports them, and
-registers any Tool, Resource, ResourceTemplate, or Prompt objects found.
-
-Components are created using the standalone decorators from fastmcp.tools,
-fastmcp.resources, and fastmcp.prompts:
-
-Example:
- ```python
- # In mcp/tools.py
- from fastmcp.tools import tool
-
- @tool
- def greet(name: str) -> str:
- return f"Hello, {name}!"
-
- # In main.py
- from pathlib import Path
-
- from fastmcp import FastMCP
- from fastmcp.server.providers import FileSystemProvider
-
- mcp = FastMCP("MyServer", providers=[FileSystemProvider(Path(__file__).parent / "mcp")])
- ```
-
-
-## Classes
-
-### `FileSystemProvider`
-
-
-Provider that discovers components from the filesystem.
-
-Scans a directory for Python files and registers any Tool, Resource,
-ResourceTemplate, or Prompt objects found. Components are created using
-the standalone decorators:
-- @tool from fastmcp.tools
-- @resource from fastmcp.resources
-- @prompt from fastmcp.prompts
-
-**Args:**
-- `root`: Root directory to scan. Defaults to current directory.
-- `reload`: If True, re-scan files on every request (dev mode).
-Defaults to False (scan once at init, cache results).
-
diff --git a/docs/python-sdk/fastmcp-server-providers-filesystem_discovery.mdx b/docs/python-sdk/fastmcp-server-providers-filesystem_discovery.mdx
deleted file mode 100644
index 060398177..000000000
--- a/docs/python-sdk/fastmcp-server-providers-filesystem_discovery.mdx
+++ /dev/null
@@ -1,104 +0,0 @@
----
-title: filesystem_discovery
-sidebarTitle: filesystem_discovery
----
-
-# `fastmcp.server.providers.filesystem_discovery`
-
-
-File discovery and module import utilities for filesystem-based routing.
-
-This module provides functions to:
-1. Discover Python files in a directory tree
-2. Import modules (as packages if __init__.py exists, else directly)
-3. Extract decorated components (Tool, Resource, Prompt objects) from imported modules
-
-
-## Functions
-
-### `discover_files`
-
-```python
-discover_files(root: Path) -> list[Path]
-```
-
-
-Recursively discover all Python files under a directory.
-
-Excludes __init__.py files (they're for package structure, not components).
-
-**Args:**
-- `root`: Root directory to scan.
-
-**Returns:**
-- List of .py file paths, sorted for deterministic order.
-
-
-### `import_module_from_file`
-
-```python
-import_module_from_file(file_path: Path) -> ModuleType
-```
-
-
-Import a Python file as a module.
-
-If the file is part of a package (directory has __init__.py), imports
-it as a proper package member (relative imports work). Otherwise,
-imports directly using spec_from_file_location.
-
-**Args:**
-- `file_path`: Path to the Python file.
-
-**Returns:**
-- The imported module.
-
-**Raises:**
-- `ImportError`: If the module cannot be imported.
-
-
-### `extract_components`
-
-```python
-extract_components(module: ModuleType) -> list[FastMCPComponent]
-```
-
-
-Extract all MCP components from a module.
-
-Scans all module attributes for instances of Tool, Resource,
-ResourceTemplate, or Prompt objects created by standalone decorators,
-or functions decorated with @tool/@resource/@prompt that have __fastmcp__ metadata.
-
-**Args:**
-- `module`: The imported module to scan.
-
-**Returns:**
-- List of component objects (Tool, Resource, ResourceTemplate, Prompt).
-
-
-### `discover_and_import`
-
-```python
-discover_and_import(root: Path) -> DiscoveryResult
-```
-
-
-Discover files, import modules, and extract components.
-
-This is the main entry point for filesystem-based discovery.
-
-**Args:**
-- `root`: Root directory to scan.
-
-**Returns:**
-- DiscoveryResult with components and any failed files.
-
-
-## Classes
-
-### `DiscoveryResult`
-
-
-Result of filesystem discovery.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-local_provider-__init__.mdx b/docs/python-sdk/fastmcp-server-providers-local_provider-__init__.mdx
deleted file mode 100644
index 5fe082a63..000000000
--- a/docs/python-sdk/fastmcp-server-providers-local_provider-__init__.mdx
+++ /dev/null
@@ -1,13 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.server.providers.local_provider`
-
-
-LocalProvider for locally-defined MCP components.
-
-This module provides the `LocalProvider` class that manages tools, resources,
-templates, and prompts registered via decorators or direct methods.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-__init__.mdx b/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-__init__.mdx
deleted file mode 100644
index d6009a6de..000000000
--- a/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-__init__.mdx
+++ /dev/null
@@ -1,13 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.server.providers.local_provider.decorators`
-
-
-Decorator mixins for LocalProvider.
-
-This module provides mixin classes that add decorator functionality
-to LocalProvider for tools, resources, templates, and prompts.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-prompts.mdx b/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-prompts.mdx
deleted file mode 100644
index e7d7d68f9..000000000
--- a/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-prompts.mdx
+++ /dev/null
@@ -1,81 +0,0 @@
----
-title: prompts
-sidebarTitle: prompts
----
-
-# `fastmcp.server.providers.local_provider.decorators.prompts`
-
-
-Prompt decorator mixin for LocalProvider.
-
-This module provides the PromptDecoratorMixin class that adds prompt
-registration functionality to LocalProvider.
-
-
-## Classes
-
-### `PromptDecoratorMixin`
-
-
-Mixin class providing prompt decorator functionality for LocalProvider.
-
-This mixin contains all methods related to:
-- Prompt registration via add_prompt()
-- Prompt decorator (@provider.prompt)
-
-
-**Methods:**
-
-#### `add_prompt`
-
-```python
-add_prompt(self: LocalProvider, prompt: Prompt | Callable[..., Any]) -> Prompt
-```
-
-Add a prompt to this provider's storage.
-
-Accepts either a Prompt object or a decorated function with __fastmcp__ metadata.
-
-
-#### `prompt`
-
-```python
-prompt(self: LocalProvider, name_or_fn: F) -> F
-```
-
-#### `prompt`
-
-```python
-prompt(self: LocalProvider, name_or_fn: str | None = None) -> Callable[[F], F]
-```
-
-#### `prompt`
-
-```python
-prompt(self: LocalProvider, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt | partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt]
-```
-
-Decorator to register a prompt.
-
-This decorator supports multiple calling patterns:
-- @provider.prompt (without parentheses)
-- @provider.prompt() (with empty parentheses)
-- @provider.prompt("custom_name") (with name as first argument)
-- @provider.prompt(name="custom_name") (with name as keyword argument)
-- provider.prompt(function, name="custom_name") (direct function call)
-
-**Args:**
-- `name_or_fn`: Either a function (when used as @prompt), a string name, or None
-- `name`: Optional name for the prompt (keyword-only, alternative to name_or_fn)
-- `title`: Optional title for the prompt
-- `description`: Optional description of what the prompt does
-- `icons`: Optional icons for the prompt
-- `tags`: Optional set of tags for categorizing the prompt
-- `enabled`: Whether the prompt is enabled (default True). If False, adds to blocklist.
-- `meta`: Optional meta information about the prompt
-- `task`: Optional task configuration for background execution
-- `auth`: Optional authorization checks for the prompt
-
-**Returns:**
-- The registered FunctionPrompt or a decorator function.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-resources.mdx b/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-resources.mdx
deleted file mode 100644
index 70c67d91c..000000000
--- a/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-resources.mdx
+++ /dev/null
@@ -1,77 +0,0 @@
----
-title: resources
-sidebarTitle: resources
----
-
-# `fastmcp.server.providers.local_provider.decorators.resources`
-
-
-Resource decorator mixin for LocalProvider.
-
-This module provides the ResourceDecoratorMixin class that adds resource
-and template registration functionality to LocalProvider.
-
-
-## Classes
-
-### `ResourceDecoratorMixin`
-
-
-Mixin class providing resource decorator functionality for LocalProvider.
-
-This mixin contains all methods related to:
-- Resource registration via add_resource()
-- Resource template registration via add_template()
-- Resource decorator (@provider.resource)
-
-
-**Methods:**
-
-#### `add_resource`
-
-```python
-add_resource(self: LocalProvider, resource: Resource | ResourceTemplate | Callable[..., Any]) -> Resource | ResourceTemplate
-```
-
-Add a resource to this provider's storage.
-
-Accepts either a Resource/ResourceTemplate object or a decorated function with __fastmcp__ metadata.
-
-
-#### `add_template`
-
-```python
-add_template(self: LocalProvider, template: ResourceTemplate) -> ResourceTemplate
-```
-
-Add a resource template to this provider's storage.
-
-
-#### `resource`
-
-```python
-resource(self: LocalProvider, uri: str) -> Callable[[F], F]
-```
-
-Decorator to register a function as a resource.
-
-If the URI contains parameters (e.g. "resource://{param}") or the function
-has parameters, it will be registered as a template resource.
-
-**Args:**
-- `uri`: URI for the resource (e.g. "resource\://my-resource" or "resource\://{param}")
-- `name`: Optional name for the resource
-- `title`: Optional title for the resource
-- `description`: Optional description of the resource
-- `icons`: Optional icons for the resource
-- `mime_type`: Optional MIME type for the resource
-- `tags`: Optional set of tags for categorizing the resource
-- `enabled`: Whether the resource is enabled (default True). If False, adds to blocklist.
-- `annotations`: Optional annotations about the resource's behavior
-- `meta`: Optional meta information about the resource
-- `task`: Optional task configuration for background execution
-- `auth`: Optional authorization checks for the resource
-
-**Returns:**
-- A decorator function.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-tools.mdx b/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-tools.mdx
deleted file mode 100644
index efeae3661..000000000
--- a/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-tools.mdx
+++ /dev/null
@@ -1,84 +0,0 @@
----
-title: tools
-sidebarTitle: tools
----
-
-# `fastmcp.server.providers.local_provider.decorators.tools`
-
-
-Tool decorator mixin for LocalProvider.
-
-This module provides the ToolDecoratorMixin class that adds tool
-registration functionality to LocalProvider.
-
-
-## Classes
-
-### `ToolDecoratorMixin`
-
-
-Mixin class providing tool decorator functionality for LocalProvider.
-
-This mixin contains all methods related to:
-- Tool registration via add_tool()
-- Tool decorator (@provider.tool)
-
-
-**Methods:**
-
-#### `add_tool`
-
-```python
-add_tool(self: LocalProvider, tool: Tool | Callable[..., Any]) -> Tool
-```
-
-Add a tool to this provider's storage.
-
-Accepts either a Tool object or a decorated function with __fastmcp__ metadata.
-
-
-#### `tool`
-
-```python
-tool(self: LocalProvider, name_or_fn: F) -> F
-```
-
-#### `tool`
-
-```python
-tool(self: LocalProvider, name_or_fn: str | None = None) -> Callable[[F], F]
-```
-
-#### `tool`
-
-```python
-tool(self: LocalProvider, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]
-```
-
-Decorator to register a tool.
-
-This decorator supports multiple calling patterns:
-- @provider.tool (without parentheses)
-- @provider.tool() (with empty parentheses)
-- @provider.tool("custom_name") (with name as first argument)
-- @provider.tool(name="custom_name") (with name as keyword argument)
-- provider.tool(function, name="custom_name") (direct function call)
-
-**Args:**
-- `name_or_fn`: Either a function (when used as @tool), a string name, or None
-- `name`: Optional name for the tool (keyword-only, alternative to name_or_fn)
-- `title`: Optional title for the tool
-- `description`: Optional description of what the tool does
-- `icons`: Optional icons for the tool
-- `tags`: Optional set of tags for categorizing the tool
-- `output_schema`: Optional JSON schema for the tool's output
-- `annotations`: Optional annotations about the tool's behavior
-- `exclude_args`: Optional list of argument names to exclude from the tool schema
-- `meta`: Optional meta information about the tool
-- `enabled`: Whether the tool is enabled (default True). If False, adds to blocklist.
-- `task`: Optional task configuration for background execution
-- `serializer`: Deprecated. Return ToolResult from your tools for full control over serialization.
-
-**Returns:**
-- The registered FunctionTool or a decorator function.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-local_provider-local_provider.mdx b/docs/python-sdk/fastmcp-server-providers-local_provider-local_provider.mdx
deleted file mode 100644
index f3bc03205..000000000
--- a/docs/python-sdk/fastmcp-server-providers-local_provider-local_provider.mdx
+++ /dev/null
@@ -1,125 +0,0 @@
----
-title: local_provider
-sidebarTitle: local_provider
----
-
-# `fastmcp.server.providers.local_provider.local_provider`
-
-
-LocalProvider for locally-defined MCP components.
-
-This module provides the `LocalProvider` class that manages tools, resources,
-templates, and prompts registered via decorators or direct methods.
-
-LocalProvider can be used standalone and attached to multiple servers:
-
-```python
-from fastmcp.server.providers import LocalProvider
-
-# Create a reusable provider with tools
-provider = LocalProvider()
-
-@provider.tool
-def greet(name: str) -> str:
- return f"Hello, {name}!"
-
-# Attach to any server
-from fastmcp import FastMCP
-server1 = FastMCP("Server1", providers=[provider])
-server2 = FastMCP("Server2", providers=[provider])
-```
-
-
-## Classes
-
-### `LocalProvider`
-
-
-Provider for locally-defined components.
-
-Supports decorator-based registration (`@provider.tool`, `@provider.resource`,
-`@provider.prompt`) and direct object registration methods.
-
-When used standalone, LocalProvider uses default settings. When attached
-to a FastMCP server via the server's decorators, server-level settings
-like `_tool_serializer` and `_support_tasks_by_default` are injected.
-
-
-**Methods:**
-
-#### `remove_tool`
-
-```python
-remove_tool(self, name: str, version: str | None = None) -> None
-```
-
-Remove tool(s) from this provider's storage.
-
-**Args:**
-- `name`: The tool name.
-- `version`: If None, removes ALL versions. If specified, removes only that version.
-
-**Raises:**
-- `KeyError`: If no matching tool is found.
-
-
-#### `remove_resource`
-
-```python
-remove_resource(self, uri: str, version: str | None = None) -> None
-```
-
-Remove resource(s) from this provider's storage.
-
-**Args:**
-- `uri`: The resource URI.
-- `version`: If None, removes ALL versions. If specified, removes only that version.
-
-**Raises:**
-- `KeyError`: If no matching resource is found.
-
-
-#### `remove_template`
-
-```python
-remove_template(self, uri_template: str, version: str | None = None) -> None
-```
-
-Remove resource template(s) from this provider's storage.
-
-**Args:**
-- `uri_template`: The template URI pattern.
-- `version`: If None, removes ALL versions. If specified, removes only that version.
-
-**Raises:**
-- `KeyError`: If no matching template is found.
-
-
-#### `remove_prompt`
-
-```python
-remove_prompt(self, name: str, version: str | None = None) -> None
-```
-
-Remove prompt(s) from this provider's storage.
-
-**Args:**
-- `name`: The prompt name.
-- `version`: If None, removes ALL versions. If specified, removes only that version.
-
-**Raises:**
-- `KeyError`: If no matching prompt is found.
-
-
-#### `get_tasks`
-
-```python
-get_tasks(self) -> Sequence[FastMCPComponent]
-```
-
-Return components eligible for background task execution.
-
-Returns components that have task_config.mode != 'forbidden'.
-This includes both FunctionTool/Resource/Prompt instances created via
-decorators and custom Tool/Resource/Prompt subclasses.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-openapi-__init__.mdx b/docs/python-sdk/fastmcp-server-providers-openapi-__init__.mdx
deleted file mode 100644
index bd79a038b..000000000
--- a/docs/python-sdk/fastmcp-server-providers-openapi-__init__.mdx
+++ /dev/null
@@ -1,23 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.server.providers.openapi`
-
-
-OpenAPI provider for FastMCP.
-
-This module provides OpenAPI integration for FastMCP through the Provider pattern.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.providers.openapi import OpenAPIProvider
- import httpx
-
- client = httpx.AsyncClient(base_url="https://api.example.com")
- provider = OpenAPIProvider(openapi_spec=spec, client=client)
- mcp = FastMCP("API Server", providers=[provider])
- ```
-
diff --git a/docs/python-sdk/fastmcp-server-providers-openapi-components.mdx b/docs/python-sdk/fastmcp-server-providers-openapi-components.mdx
deleted file mode 100644
index 94f0b7b6a..000000000
--- a/docs/python-sdk/fastmcp-server-providers-openapi-components.mdx
+++ /dev/null
@@ -1,62 +0,0 @@
----
-title: components
-sidebarTitle: components
----
-
-# `fastmcp.server.providers.openapi.components`
-
-
-OpenAPI component classes: Tool, Resource, and ResourceTemplate.
-
-## Classes
-
-### `OpenAPITool`
-
-
-Tool implementation for OpenAPI endpoints.
-
-
-**Methods:**
-
-#### `run`
-
-```python
-run(self, arguments: dict[str, Any]) -> ToolResult
-```
-
-Execute the HTTP request using RequestDirector.
-
-
-### `OpenAPIResource`
-
-
-Resource implementation for OpenAPI endpoints.
-
-
-**Methods:**
-
-#### `read`
-
-```python
-read(self) -> ResourceResult
-```
-
-Fetch the resource data by making an HTTP request.
-
-
-### `OpenAPIResourceTemplate`
-
-
-Resource template implementation for OpenAPI endpoints.
-
-
-**Methods:**
-
-#### `create_resource`
-
-```python
-create_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> Resource
-```
-
-Create a resource with the given parameters.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx b/docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx
deleted file mode 100644
index 5d74c19ae..000000000
--- a/docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx
+++ /dev/null
@@ -1,40 +0,0 @@
----
-title: provider
-sidebarTitle: provider
----
-
-# `fastmcp.server.providers.openapi.provider`
-
-
-OpenAPIProvider for creating MCP components from OpenAPI specifications.
-
-## Classes
-
-### `OpenAPIProvider`
-
-
-Provider that creates MCP components from an OpenAPI specification.
-
-Components are created eagerly during initialization by parsing the OpenAPI
-spec. Each component makes HTTP calls to the described API endpoints.
-
-
-**Methods:**
-
-#### `lifespan`
-
-```python
-lifespan(self) -> AsyncIterator[None]
-```
-
-Manage the lifecycle of the auto-created httpx client.
-
-
-#### `get_tasks`
-
-```python
-get_tasks(self) -> Sequence[FastMCPComponent]
-```
-
-Return empty list - OpenAPI components don't support tasks.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-openapi-routing.mdx b/docs/python-sdk/fastmcp-server-providers-openapi-routing.mdx
deleted file mode 100644
index c35ca5886..000000000
--- a/docs/python-sdk/fastmcp-server-providers-openapi-routing.mdx
+++ /dev/null
@@ -1,23 +0,0 @@
----
-title: routing
-sidebarTitle: routing
----
-
-# `fastmcp.server.providers.openapi.routing`
-
-
-Route mapping logic for OpenAPI operations.
-
-## Classes
-
-### `MCPType`
-
-
-Type of FastMCP component to create from a route.
-
-
-### `RouteMap`
-
-
-Mapping configuration for HTTP routes to FastMCP component types.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-proxy.mdx b/docs/python-sdk/fastmcp-server-providers-proxy.mdx
deleted file mode 100644
index 4c64d8566..000000000
--- a/docs/python-sdk/fastmcp-server-providers-proxy.mdx
+++ /dev/null
@@ -1,317 +0,0 @@
----
-title: proxy
-sidebarTitle: proxy
----
-
-# `fastmcp.server.providers.proxy`
-
-
-ProxyProvider for proxying to remote MCP servers.
-
-This module provides the `ProxyProvider` class that proxies components from
-a remote MCP server via a client factory. It also provides proxy component
-classes that forward execution to remote servers.
-
-
-## Functions
-
-### `default_proxy_roots_handler`
-
-```python
-default_proxy_roots_handler(context: RequestContext[ClientSession, LifespanContextT]) -> RootsList
-```
-
-
-Forward list roots request from remote server to proxy's connected clients.
-
-
-### `default_proxy_sampling_handler`
-
-```python
-default_proxy_sampling_handler(messages: list[mcp.types.SamplingMessage], params: mcp.types.CreateMessageRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> mcp.types.CreateMessageResult
-```
-
-
-Forward sampling request from remote server to proxy's connected clients.
-
-
-### `default_proxy_elicitation_handler`
-
-```python
-default_proxy_elicitation_handler(message: str, response_type: type, params: mcp.types.ElicitRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> ElicitResult
-```
-
-
-Forward elicitation request from remote server to proxy's connected clients.
-
-
-### `default_proxy_log_handler`
-
-```python
-default_proxy_log_handler(message: LogMessage) -> None
-```
-
-
-Forward log notification from remote server to proxy's connected clients.
-
-
-### `default_proxy_progress_handler`
-
-```python
-default_proxy_progress_handler(progress: float, total: float | None, message: str | None) -> None
-```
-
-
-Forward progress notification from remote server to proxy's connected clients.
-
-
-## Classes
-
-### `ProxyTool`
-
-
-A Tool that represents and executes a tool on a remote server.
-
-
-**Methods:**
-
-#### `model_copy`
-
-```python
-model_copy(self, **kwargs: Any) -> ProxyTool
-```
-
-Override to preserve _backend_name when name changes.
-
-
-#### `from_mcp_tool`
-
-```python
-from_mcp_tool(cls, client_factory: ClientFactoryT, mcp_tool: mcp.types.Tool) -> ProxyTool
-```
-
-Factory method to create a ProxyTool from a raw MCP tool schema.
-
-
-#### `run`
-
-```python
-run(self, arguments: dict[str, Any], context: Context | None = None) -> ToolResult
-```
-
-Executes the tool by making a call through the client.
-
-
-#### `get_span_attributes`
-
-```python
-get_span_attributes(self) -> dict[str, Any]
-```
-
-### `ProxyResource`
-
-
-A Resource that represents and reads a resource from a remote server.
-
-
-**Methods:**
-
-#### `model_copy`
-
-```python
-model_copy(self, **kwargs: Any) -> ProxyResource
-```
-
-Override to preserve _backend_uri when uri changes.
-
-
-#### `from_mcp_resource`
-
-```python
-from_mcp_resource(cls, client_factory: ClientFactoryT, mcp_resource: mcp.types.Resource) -> ProxyResource
-```
-
-Factory method to create a ProxyResource from a raw MCP resource schema.
-
-
-#### `read`
-
-```python
-read(self) -> ResourceResult
-```
-
-Read the resource content from the remote server.
-
-
-#### `get_span_attributes`
-
-```python
-get_span_attributes(self) -> dict[str, Any]
-```
-
-### `ProxyTemplate`
-
-
-A ResourceTemplate that represents and creates resources from a remote server template.
-
-
-**Methods:**
-
-#### `model_copy`
-
-```python
-model_copy(self, **kwargs: Any) -> ProxyTemplate
-```
-
-Override to preserve _backend_uri_template when uri_template changes.
-
-
-#### `from_mcp_template`
-
-```python
-from_mcp_template(cls, client_factory: ClientFactoryT, mcp_template: mcp.types.ResourceTemplate) -> ProxyTemplate
-```
-
-Factory method to create a ProxyTemplate from a raw MCP template schema.
-
-
-#### `create_resource`
-
-```python
-create_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> ProxyResource
-```
-
-Create a resource from the template by calling the remote server.
-
-
-#### `get_span_attributes`
-
-```python
-get_span_attributes(self) -> dict[str, Any]
-```
-
-### `ProxyPrompt`
-
-
-A Prompt that represents and renders a prompt from a remote server.
-
-
-**Methods:**
-
-#### `model_copy`
-
-```python
-model_copy(self, **kwargs: Any) -> ProxyPrompt
-```
-
-Override to preserve _backend_name when name changes.
-
-
-#### `from_mcp_prompt`
-
-```python
-from_mcp_prompt(cls, client_factory: ClientFactoryT, mcp_prompt: mcp.types.Prompt) -> ProxyPrompt
-```
-
-Factory method to create a ProxyPrompt from a raw MCP prompt schema.
-
-
-#### `render`
-
-```python
-render(self, arguments: dict[str, Any]) -> PromptResult
-```
-
-Render the prompt by making a call through the client.
-
-
-#### `get_span_attributes`
-
-```python
-get_span_attributes(self) -> dict[str, Any]
-```
-
-### `ProxyProvider`
-
-
-Provider that proxies to a remote MCP server via a client factory.
-
-This provider fetches components from a remote server and returns Proxy*
-component instances that forward execution to the remote server.
-
-All components returned by this provider have task_config.mode="forbidden"
-because tasks cannot be executed through a proxy.
-
-
-**Methods:**
-
-#### `get_tasks`
-
-```python
-get_tasks(self) -> Sequence[FastMCPComponent]
-```
-
-Return empty list since proxy components don't support tasks.
-
-Override the base implementation to avoid calling list_tools() during
-server lifespan initialization, which would open the client before any
-context is set. All Proxy* components have task_config.mode="forbidden".
-
-
-### `FastMCPProxy`
-
-
-A FastMCP server that acts as a proxy to a remote MCP-compliant server.
-
-This is a convenience wrapper that creates a FastMCP server with a
-ProxyProvider. For more control, use FastMCP with add_provider(ProxyProvider(...)).
-
-
-### `ProxyClient`
-
-
-A proxy client that forwards advanced interactions between a remote MCP server and the proxy's connected clients.
-
-Supports forwarding roots, sampling, elicitation, logging, and progress.
-
-
-### `StatefulProxyClient`
-
-
-A proxy client that provides a stateful client factory for the proxy server.
-
-The stateful proxy client bound its copy to the server session.
-And it will be disconnected when the session is exited.
-
-This is useful to proxy a stateful mcp server such as the Playwright MCP server.
-Note that it is essential to ensure that the proxy server itself is also stateful.
-
-Because session reuse means the receive-loop task inherits a stale
-``request_ctx`` ContextVar snapshot, the default proxy handlers are
-replaced with versions that restore the ContextVar before forwarding.
-``ProxyTool.run`` stashes the current ``RequestContext`` in
-``_proxy_rc_ref`` before each backend call, and the handlers consult
-it to detect (and correct) staleness.
-
-
-**Methods:**
-
-#### `clear`
-
-```python
-clear(self)
-```
-
-Clear all cached clients and force disconnect them.
-
-
-#### `new_stateful`
-
-```python
-new_stateful(self) -> Client[ClientTransportT]
-```
-
-Create a new stateful proxy client instance with the same configuration.
-
-Use this method as the client factory for stateful proxy server.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-skills-__init__.mdx b/docs/python-sdk/fastmcp-server-providers-skills-__init__.mdx
deleted file mode 100644
index c3f296111..000000000
--- a/docs/python-sdk/fastmcp-server-providers-skills-__init__.mdx
+++ /dev/null
@@ -1,32 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.server.providers.skills`
-
-
-Skills providers for exposing agent skills as MCP resources.
-
-This module provides a two-layer architecture for skill discovery:
-
-- **SkillProvider**: Handles a single skill folder, exposing its files as resources.
-- **SkillsDirectoryProvider**: Scans a directory, creates a SkillProvider per folder.
-- **Vendor providers**: Platform-specific providers for Claude, Cursor, VS Code, Codex,
- Gemini, Goose, Copilot, and OpenCode.
-
-Example:
- ```python
- from pathlib import Path
- from fastmcp import FastMCP
- from fastmcp.server.providers.skills import ClaudeSkillsProvider, SkillProvider
-
- mcp = FastMCP("Skills Server")
-
- # Load a single skill
- mcp.add_provider(SkillProvider(Path.home() / ".claude/skills/pdf-processing"))
-
- # Or load all skills in a directory
- mcp.add_provider(ClaudeSkillsProvider()) # Uses ~/.claude/skills/
- ```
-
diff --git a/docs/python-sdk/fastmcp-server-providers-skills-claude_provider.mdx b/docs/python-sdk/fastmcp-server-providers-skills-claude_provider.mdx
deleted file mode 100644
index 30ed2c7c7..000000000
--- a/docs/python-sdk/fastmcp-server-providers-skills-claude_provider.mdx
+++ /dev/null
@@ -1,25 +0,0 @@
----
-title: claude_provider
-sidebarTitle: claude_provider
----
-
-# `fastmcp.server.providers.skills.claude_provider`
-
-
-Claude-specific skills provider for Claude Code skills.
-
-## Classes
-
-### `ClaudeSkillsProvider`
-
-
-Provider for Claude Code skills from ~/.claude/skills/.
-
-A convenience subclass that sets the default root to Claude's skills location.
-
-**Args:**
-- `reload`: If True, re-scan on every request. Defaults to False.
-- `supporting_files`: How supporting files are exposed\:
-- "template"\: Accessed via ResourceTemplate, hidden from list_resources().
-- "resources"\: Each file exposed as individual Resource in list_resources().
-
diff --git a/docs/python-sdk/fastmcp-server-providers-skills-directory_provider.mdx b/docs/python-sdk/fastmcp-server-providers-skills-directory_provider.mdx
deleted file mode 100644
index bf68a00e3..000000000
--- a/docs/python-sdk/fastmcp-server-providers-skills-directory_provider.mdx
+++ /dev/null
@@ -1,31 +0,0 @@
----
-title: directory_provider
-sidebarTitle: directory_provider
----
-
-# `fastmcp.server.providers.skills.directory_provider`
-
-
-Directory scanning provider for discovering multiple skills.
-
-## Classes
-
-### `SkillsDirectoryProvider`
-
-
-Provider that scans directories and creates a SkillProvider per skill folder.
-
-This extends AggregateProvider to combine multiple SkillProviders into one.
-Each subdirectory containing a main file (default: SKILL.md) becomes a skill.
-Can scan multiple root directories - if a skill name appears in multiple roots,
-the first one found wins.
-
-**Args:**
-- `roots`: Root directory(ies) containing skill folders. Can be a single path
-or a sequence of paths.
-- `reload`: If True, re-discover skills on each request. Defaults to False.
-- `main_file_name`: Name of the main skill file. Defaults to "SKILL.md".
-- `supporting_files`: How supporting files are exposed in child SkillProviders\:
-- "template"\: Accessed via ResourceTemplate, hidden from list_resources().
-- "resources"\: Each file exposed as individual Resource in list_resources().
-
diff --git a/docs/python-sdk/fastmcp-server-providers-skills-skill_provider.mdx b/docs/python-sdk/fastmcp-server-providers-skills-skill_provider.mdx
deleted file mode 100644
index c1ab56d9d..000000000
--- a/docs/python-sdk/fastmcp-server-providers-skills-skill_provider.mdx
+++ /dev/null
@@ -1,121 +0,0 @@
----
-title: skill_provider
-sidebarTitle: skill_provider
----
-
-# `fastmcp.server.providers.skills.skill_provider`
-
-
-Basic skill provider for handling a single skill folder.
-
-## Classes
-
-### `SkillResource`
-
-
-A resource representing a skill's main file or manifest.
-
-
-**Methods:**
-
-#### `get_meta`
-
-```python
-get_meta(self) -> dict[str, Any]
-```
-
-#### `read`
-
-```python
-read(self) -> str | bytes | ResourceResult
-```
-
-Read the resource content.
-
-
-### `SkillFileTemplate`
-
-
-A template for accessing files within a skill.
-
-
-**Methods:**
-
-#### `read`
-
-```python
-read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult
-```
-
-Read a file from the skill directory.
-
-
-#### `create_resource`
-
-```python
-create_resource(self, uri: str, params: dict[str, Any]) -> Resource
-```
-
-Create a resource for the given URI and parameters.
-
-Note: This is not typically used since _read() handles file reading directly.
-Provided for compatibility with the ResourceTemplate interface.
-
-
-### `SkillFileResource`
-
-
-A resource representing a specific file within a skill.
-
-
-**Methods:**
-
-#### `get_meta`
-
-```python
-get_meta(self) -> dict[str, Any]
-```
-
-#### `read`
-
-```python
-read(self) -> str | bytes | ResourceResult
-```
-
-Read the file content.
-
-
-### `SkillProvider`
-
-
-Provider that exposes a single skill folder as MCP resources.
-
-Each skill folder must contain a main file (default: SKILL.md) and may
-contain additional supporting files.
-
-Exposes:
-- A Resource for the main file (skill://{name}/SKILL.md)
-- A Resource for the synthetic manifest (skill://{name}/_manifest)
-- Supporting files via ResourceTemplate or Resources (configurable)
-
-**Args:**
-- `skill_path`: Path to the skill directory.
-- `main_file_name`: Name of the main skill file. Defaults to "SKILL.md".
-- `supporting_files`: How supporting files (everything except main file and
-manifest) are exposed to clients\:
-- "template"\: Accessed via ResourceTemplate, hidden from list_resources().
- Clients discover files by reading the manifest first.
-- "resources"\: Each file exposed as individual Resource in list_resources().
- Full enumeration upfront.
-
-
-**Methods:**
-
-#### `skill_info`
-
-```python
-skill_info(self) -> SkillInfo
-```
-
-Get the loaded skill info.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-skills-vendor_providers.mdx b/docs/python-sdk/fastmcp-server-providers-skills-vendor_providers.mdx
deleted file mode 100644
index a5a724953..000000000
--- a/docs/python-sdk/fastmcp-server-providers-skills-vendor_providers.mdx
+++ /dev/null
@@ -1,56 +0,0 @@
----
-title: vendor_providers
-sidebarTitle: vendor_providers
----
-
-# `fastmcp.server.providers.skills.vendor_providers`
-
-
-Vendor-specific skills providers for various AI coding platforms.
-
-## Classes
-
-### `CursorSkillsProvider`
-
-
-Cursor skills from ~/.cursor/skills/.
-
-
-### `VSCodeSkillsProvider`
-
-
-VS Code skills from ~/.copilot/skills/.
-
-
-### `CodexSkillsProvider`
-
-
-Codex skills from /etc/codex/skills/ and ~/.codex/skills/.
-
-Scans both system-level and user-level directories. System skills take
-precedence if duplicates exist.
-
-
-### `GeminiSkillsProvider`
-
-
-Gemini skills from ~/.gemini/skills/.
-
-
-### `GooseSkillsProvider`
-
-
-Goose skills from ~/.config/agents/skills/.
-
-
-### `CopilotSkillsProvider`
-
-
-GitHub Copilot skills from ~/.copilot/skills/.
-
-
-### `OpenCodeSkillsProvider`
-
-
-OpenCode skills from ~/.config/opencode/skills/.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-wrapped_provider.mdx b/docs/python-sdk/fastmcp-server-providers-wrapped_provider.mdx
deleted file mode 100644
index 22fc90b40..000000000
--- a/docs/python-sdk/fastmcp-server-providers-wrapped_provider.mdx
+++ /dev/null
@@ -1,13 +0,0 @@
----
-title: wrapped_provider
-sidebarTitle: wrapped_provider
----
-
-# `fastmcp.server.providers.wrapped_provider`
-
-
-WrappedProvider for immutable transform composition.
-
-This module provides `_WrappedProvider`, an internal class that wraps a provider
-with an additional transform. Created by `Provider.wrap_transform()`.
-
diff --git a/docs/python-sdk/fastmcp-server-providers-__init__.mdx b/docs/python-sdk/fastmcp-server-providers.mdx
similarity index 95%
rename from docs/python-sdk/fastmcp-server-providers-__init__.mdx
rename to docs/python-sdk/fastmcp-server-providers.mdx
index b7addb653..c227ee1a0 100644
--- a/docs/python-sdk/fastmcp-server-providers-__init__.mdx
+++ b/docs/python-sdk/fastmcp-server-providers.mdx
@@ -1,6 +1,6 @@
---
-title: __init__
-sidebarTitle: __init__
+title: providers
+sidebarTitle: providers
---
# `fastmcp.server.providers`
diff --git a/docs/python-sdk/fastmcp-server-proxy.mdx b/docs/python-sdk/fastmcp-server-proxy.mdx
deleted file mode 100644
index a9200635c..000000000
--- a/docs/python-sdk/fastmcp-server-proxy.mdx
+++ /dev/null
@@ -1,14 +0,0 @@
----
-title: proxy
-sidebarTitle: proxy
----
-
-# `fastmcp.server.proxy`
-
-
-Backwards compatibility - import from fastmcp.server.providers.proxy instead.
-
-This module re-exports all proxy-related classes from their new location
-at fastmcp.server.providers.proxy. Direct imports from this module are
-deprecated and will be removed in a future version.
-
diff --git a/docs/python-sdk/fastmcp-server-sampling-__init__.mdx b/docs/python-sdk/fastmcp-server-sampling-__init__.mdx
deleted file mode 100644
index 0b0533971..000000000
--- a/docs/python-sdk/fastmcp-server-sampling-__init__.mdx
+++ /dev/null
@@ -1,9 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.server.sampling`
-
-
-Sampling module for FastMCP servers.
diff --git a/docs/python-sdk/fastmcp-server-sampling-run.mdx b/docs/python-sdk/fastmcp-server-sampling-run.mdx
deleted file mode 100644
index c09ad42d5..000000000
--- a/docs/python-sdk/fastmcp-server-sampling-run.mdx
+++ /dev/null
@@ -1,203 +0,0 @@
----
-title: run
-sidebarTitle: run
----
-
-# `fastmcp.server.sampling.run`
-
-
-Sampling types and helper functions for FastMCP servers.
-
-## Functions
-
-### `determine_handler_mode`
-
-```python
-determine_handler_mode(context: Context, needs_tools: bool) -> bool
-```
-
-
-Determine whether to use fallback handler or client for sampling.
-
-**Args:**
-- `context`: The MCP context.
-- `needs_tools`: Whether the sampling request requires tool support.
-
-**Returns:**
-- True if fallback handler should be used, False to use client.
-
-**Raises:**
-- `ValueError`: If client lacks required capability and no fallback configured.
-
-
-### `call_sampling_handler`
-
-```python
-call_sampling_handler(context: Context, messages: list[SamplingMessage]) -> CreateMessageResult | CreateMessageResultWithTools
-```
-
-
-Make LLM call using the fallback handler.
-
-Note: This function expects the caller (sample_step) to have validated that
-sampling_handler is set via determine_handler_mode(). The checks below are
-safeguards against internal misuse.
-
-
-### `execute_tools`
-
-```python
-execute_tools(tool_calls: list[ToolUseContent], tool_map: dict[str, SamplingTool], mask_error_details: bool = False, tool_concurrency: int | None = None) -> list[ToolResultContent]
-```
-
-
-Execute tool calls and return results.
-
-**Args:**
-- `tool_calls`: List of tool use requests from the LLM.
-- `tool_map`: Mapping from tool name to SamplingTool.
-- `mask_error_details`: If True, mask detailed error messages from tool execution.
-When masked, only generic error messages are returned to the LLM.
-Tools can explicitly raise ToolError to bypass masking when they want
-to provide specific error messages to the LLM.
-- `tool_concurrency`: Controls parallel execution of tools\:
-- None (default)\: Sequential execution (one at a time)
-- 0\: Unlimited parallel execution
-- N > 0\: Execute at most N tools concurrently
-If any tool has sequential=True, all tools execute sequentially
-regardless of this setting.
-
-**Returns:**
-- List of tool result content blocks in the same order as tool_calls.
-
-
-### `prepare_messages`
-
-```python
-prepare_messages(messages: str | Sequence[str | SamplingMessage]) -> list[SamplingMessage]
-```
-
-
-Convert various message formats to a list of SamplingMessage objects.
-
-
-### `prepare_tools`
-
-```python
-prepare_tools(tools: Sequence[SamplingTool | FunctionTool | TransformedTool | Callable[..., Any]] | None) -> list[SamplingTool] | None
-```
-
-
-Convert tools to SamplingTool objects.
-
-Accepts SamplingTool instances, FunctionTool instances, TransformedTool instances,
-or plain callable functions. FunctionTool and TransformedTool are converted using
-from_callable_tool(), while plain functions use from_function().
-
-**Args:**
-- `tools`: Sequence of tools to prepare. Can be SamplingTool, FunctionTool,
-TransformedTool, or plain callable functions.
-
-**Returns:**
-- List of SamplingTool instances, or None if tools is None.
-
-
-### `extract_tool_calls`
-
-```python
-extract_tool_calls(response: CreateMessageResult | CreateMessageResultWithTools) -> list[ToolUseContent]
-```
-
-
-Extract tool calls from a response.
-
-
-### `create_final_response_tool`
-
-```python
-create_final_response_tool(result_type: type) -> SamplingTool
-```
-
-
-Create a synthetic 'final_response' tool for structured output.
-
-This tool is used to capture structured responses from the LLM.
-The tool's schema is derived from the result_type.
-
-
-### `sample_step_impl`
-
-```python
-sample_step_impl(context: Context, messages: str | Sequence[str | SamplingMessage]) -> SampleStep
-```
-
-
-Implementation of Context.sample_step().
-
-Make a single LLM sampling call. This is a stateless function that makes
-exactly one LLM call and optionally executes any requested tools.
-
-
-### `sample_impl`
-
-```python
-sample_impl(context: Context, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT]
-```
-
-
-Implementation of Context.sample().
-
-Send a sampling request to the client and await the response. This method
-runs to completion automatically, executing a tool loop until the LLM
-provides a final text response.
-
-
-## Classes
-
-### `SamplingResult`
-
-
-Result of a sampling operation.
-
-**Attributes:**
-- `text`: The text representation of the result (raw text or JSON for structured).
-- `result`: The typed result (str for text, parsed object for structured output).
-- `history`: All messages exchanged during sampling.
-
-
-### `SampleStep`
-
-
-Result of a single sampling call.
-
-Represents what the LLM returned in this step plus the message history.
-
-
-**Methods:**
-
-#### `is_tool_use`
-
-```python
-is_tool_use(self) -> bool
-```
-
-True if the LLM is requesting tool execution.
-
-
-#### `text`
-
-```python
-text(self) -> str | None
-```
-
-Extract text from the response, if available.
-
-
-#### `tool_calls`
-
-```python
-tool_calls(self) -> list[ToolUseContent]
-```
-
-Get the list of tool calls from the response.
-
diff --git a/docs/python-sdk/fastmcp-server-sampling-sampling_tool.mdx b/docs/python-sdk/fastmcp-server-sampling-sampling_tool.mdx
deleted file mode 100644
index cac91d36e..000000000
--- a/docs/python-sdk/fastmcp-server-sampling-sampling_tool.mdx
+++ /dev/null
@@ -1,101 +0,0 @@
----
-title: sampling_tool
-sidebarTitle: sampling_tool
----
-
-# `fastmcp.server.sampling.sampling_tool`
-
-
-SamplingTool for use during LLM sampling requests.
-
-## Classes
-
-### `SamplingTool`
-
-
-A tool that can be used during LLM sampling.
-
-SamplingTools bundle a tool's schema (name, description, parameters) with
-an executor function, enabling servers to execute agentic workflows where
-the LLM can request tool calls during sampling.
-
-In most cases, pass functions directly to ctx.sample():
-
- def search(query: str) -> str:
- '''Search the web.'''
- return web_search(query)
-
- result = await context.sample(
- messages="Find info about Python",
- tools=[search], # Plain functions work directly
- )
-
-Create a SamplingTool explicitly when you need custom name/description:
-
- tool = SamplingTool.from_function(search, name="web_search")
-
-
-**Methods:**
-
-#### `run`
-
-```python
-run(self, arguments: dict[str, Any] | None = None) -> Any
-```
-
-Execute the tool with the given arguments.
-
-**Args:**
-- `arguments`: Dictionary of arguments to pass to the tool function.
-
-**Returns:**
-- The result of executing the tool function.
-
-
-#### `from_function`
-
-```python
-from_function(cls, fn: Callable[..., Any]) -> SamplingTool
-```
-
-Create a SamplingTool from a function.
-
-The function's signature is analyzed to generate a JSON schema for
-the tool's parameters. Type hints are used to determine parameter types.
-
-**Args:**
-- `fn`: The function to create a tool from.
-- `name`: Optional name override. Defaults to the function's name.
-- `description`: Optional description override. Defaults to the function's docstring.
-- `sequential`: If True, this tool requires sequential execution and prevents
-parallel execution of all tools in the batch. Set to True for tools
-with shared state, file writes, or other operations that cannot run
-concurrently. Defaults to False.
-
-**Returns:**
-- A SamplingTool wrapping the function.
-
-**Raises:**
-- `ValueError`: If the function is a lambda without a name override.
-
-
-#### `from_callable_tool`
-
-```python
-from_callable_tool(cls, tool: FunctionTool | TransformedTool) -> SamplingTool
-```
-
-Create a SamplingTool from a FunctionTool or TransformedTool.
-
-Reuses existing server tools in sampling contexts. For TransformedTool,
-the tool's .run() method is used to ensure proper argument transformation,
-and the ToolResult is automatically unwrapped.
-
-**Args:**
-- `tool`: A FunctionTool or TransformedTool to convert.
-- `name`: Optional name override. Defaults to tool.name.
-- `description`: Optional description override. Defaults to tool.description.
-
-**Raises:**
-- `TypeError`: If the tool is not a FunctionTool or TransformedTool.
-
diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx
index dbd0ad15a..12524e5b8 100644
--- a/docs/python-sdk/fastmcp-server-server.mdx
+++ b/docs/python-sdk/fastmcp-server-server.mdx
@@ -10,7 +10,7 @@ FastMCP - A more ergonomic interface for MCP servers.
## Functions
-### `default_lifespan`
+### `default_lifespan`
```python
default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any]
@@ -26,10 +26,10 @@ Default lifespan context manager that does nothing.
- An empty dictionary as the lifespan result.
-### `create_proxy`
+### `create_proxy`
```python
-create_proxy(target: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
+create_proxy(target: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | SDKServer | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
```
@@ -46,6 +46,17 @@ use `FastMCPProxy` or `ProxyProvider` directly from `fastmcp.server.providers.pr
- A URL string or AnyUrl
- A Path to a server script
- An MCPConfig or dict
+- `mode`: Protocol-era negotiation for auto-created proxy clients (a
+non-Client target). By default (``None``) the backend MIRRORS the
+front connection's negotiated era per request, so the whole chain
+speaks one era end-to-end\: a modern front reaches a modern backend
+(a guard tool's `InputRequiredResult` (SEP-2322) round-trips) and a
+handshake front reaches a handshake backend (server-initiated
+sampling / elicitation / roots push-forwarding works). Pass an
+explicit mode (e.g. ``"auto"`` or a version string) to pin the
+backend era regardless of the front; this overrides mirroring and is
+appropriate when the backend only speaks one era. Ignored when
+`target` is already a `Client` (which carries its own mode).
- `**settings`: Additional settings passed to FastMCPProxy (name, etc.)
**Returns:**
@@ -54,53 +65,53 @@ use `FastMCPProxy` or `ProxyProvider` directly from `fastmcp.server.providers.pr
## Classes
-### `StateValue`
+### `StateValue`
Wrapper for stored context state values.
-### `FastMCP`
+### `FastMCP`
**Methods:**
-#### `name`
+#### `name`
```python
name(self) -> str
```
-#### `instructions`
+#### `instructions`
```python
instructions(self) -> str | None
```
-#### `instructions`
+#### `instructions`
```python
instructions(self, value: str | None) -> None
```
-#### `version`
+#### `version`
```python
version(self) -> str | None
```
-#### `website_url`
+#### `website_url`
```python
website_url(self) -> str | None
```
-#### `icons`
+#### `icons`
```python
-icons(self) -> list[mcp.types.Icon]
+icons(self) -> list[mcp_types.Icon]
```
-#### `local_provider`
+#### `local_provider`
```python
local_provider(self) -> LocalProvider
@@ -115,13 +126,42 @@ Use this to remove components:
mcp.local_provider.remove_prompt("my_prompt")
-#### `add_middleware`
+#### `add_middleware`
```python
add_middleware(self, middleware: Middleware) -> None
```
-#### `add_provider`
+#### `add_extension`
+
+```python
+add_extension(self, extension: ServerExtension) -> None
+```
+
+Register a server extension (SEP-2133).
+
+An extension contributes a negotiated capability, additive request
+methods, a `tools/call` interceptor, and an optional lifespan — each
+with access to FastMCP-level constructs (the component registry,
+`Context`, auth scope). Its capability is advertised only while it is
+registered.
+
+The extension is bound to this server (so its handlers and interceptor
+can reach it), its method bindings are wired onto the low-level server,
+and it is recorded for capability advertisement, interception, and
+lifespan entry. Registering two extensions with the same identifier is
+an error, as is registering after the server's lifespan has started —
+the extension's lifespan could no longer run, leaving it silently
+half-active.
+
+Extensions are served by the server they are registered on. A mounted
+child's extensions do not propagate to the root: the root serves the
+wire, so only root-registered extensions advertise capabilities and
+answer methods (matching the lifespan, which also defers to the root).
+Register extensions on the server you run.
+
+
+#### `add_provider`
```python
add_provider(self, provider: Provider) -> None
@@ -141,7 +181,7 @@ always take precedence over providers.
- Prompts become "namespace_promptname"
-#### `get_tasks`
+#### `get_tasks`
```python
get_tasks(self) -> Sequence[FastMCPComponent]
@@ -153,7 +193,7 @@ Overrides AggregateProvider.get_tasks() to apply server-level transforms
after aggregation. AggregateProvider handles provider-level namespacing.
-#### `add_transform`
+#### `add_transform`
```python
add_transform(self, transform: Transform) -> None
@@ -168,31 +208,7 @@ They transform tools, resources, and prompts from ALL providers.
- `transform`: The transform to add.
-#### `add_tool_transformation`
-
-```python
-add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfig) -> None
-```
-
-Add a tool transformation.
-
-.. deprecated::
- Use ``add_transform(ToolTransform({...}))`` instead.
-
-
-#### `remove_tool_transformation`
-
-```python
-remove_tool_transformation(self, _tool_name: str) -> None
-```
-
-Remove a tool transformation.
-
-.. deprecated::
- Tool transformations are now immutable. Use enable/disable controls instead.
-
-
-#### `list_tools`
+#### `list_tools`
```python
list_tools(self) -> Sequence[Tool]
@@ -200,12 +216,12 @@ list_tools(self) -> Sequence[Tool]
List all enabled tools from providers.
-Overrides Provider.list_tools() to add visibility filtering, auth filtering,
+Overrides Provider.list_tools() to add enabled filtering, auth filtering,
and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
-#### `get_tool`
+#### `get_tool`
```python
get_tool(self, name: str, version: VersionSpec | None = None) -> Tool | None
@@ -213,10 +229,13 @@ get_tool(self, name: str, version: VersionSpec | None = None) -> Tool | None
Get a tool by name, filtering disabled tools.
-Overrides Provider.get_tool() to add visibility filtering after all
+Overrides Provider.get_tool() to filter disabled tools after all
transforms (including session-level) have been applied. This ensures
session transforms can override provider-level disables.
+When the highest version is disabled and no explicit version was
+requested, falls back to the next-highest enabled version.
+
**Args:**
- `name`: The tool name.
- `version`: Version filter (None returns highest version).
@@ -225,7 +244,7 @@ session transforms can override provider-level disables.
- The tool if found and enabled, None otherwise.
-#### `list_resources`
+#### `list_resources`
```python
list_resources(self) -> Sequence[Resource]
@@ -238,7 +257,7 @@ and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
-#### `get_resource`
+#### `get_resource`
```python
get_resource(self, uri: str, version: VersionSpec | None = None) -> Resource | None
@@ -249,6 +268,9 @@ Get a resource by URI, filtering disabled resources.
Overrides Provider.get_resource() to add visibility filtering after all
transforms (including session-level) have been applied.
+When the highest version is disabled and no explicit version was
+requested, falls back to the next-highest enabled version.
+
**Args:**
- `uri`: The resource URI.
- `version`: Version filter (None returns highest version).
@@ -257,7 +279,7 @@ transforms (including session-level) have been applied.
- The resource if found and enabled, None otherwise.
-#### `list_resource_templates`
+#### `list_resource_templates`
```python
list_resource_templates(self) -> Sequence[ResourceTemplate]
@@ -270,7 +292,7 @@ auth filtering, and middleware execution. Returns all versions (no deduplication
Protocol handlers deduplicate for MCP wire format.
-#### `get_resource_template`
+#### `get_resource_template`
```python
get_resource_template(self, uri: str, version: VersionSpec | None = None) -> ResourceTemplate | None
@@ -281,6 +303,9 @@ Get a resource template by URI, filtering disabled templates.
Overrides Provider.get_resource_template() to add visibility filtering after
all transforms (including session-level) have been applied.
+When the highest version is disabled and no explicit version was
+requested, falls back to the next-highest enabled version.
+
**Args:**
- `uri`: The template URI.
- `version`: Version filter (None returns highest version).
@@ -289,7 +314,7 @@ all transforms (including session-level) have been applied.
- The template if found and enabled, None otherwise.
-#### `list_prompts`
+#### `list_prompts`
```python
list_prompts(self) -> Sequence[Prompt]
@@ -302,7 +327,7 @@ and middleware execution. Returns all versions (no deduplication).
Protocol handlers deduplicate for MCP wire format.
-#### `get_prompt`
+#### `get_prompt`
```python
get_prompt(self, name: str, version: VersionSpec | None = None) -> Prompt | None
@@ -313,6 +338,9 @@ Get a prompt by name, filtering disabled prompts.
Overrides Provider.get_prompt() to add visibility filtering after all
transforms (including session-level) have been applied.
+When the highest version is disabled and no explicit version was
+requested, falls back to the next-highest enabled version.
+
**Args:**
- `name`: The prompt name.
- `version`: Version filter (None returns highest version).
@@ -321,24 +349,12 @@ transforms (including session-level) have been applied.
- The prompt if found and enabled, None otherwise.
-#### `call_tool`
+#### `call_tool`
```python
call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult
```
-#### `call_tool`
-
-```python
-call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult
-```
-
-#### `call_tool`
-
-```python
-call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult | mcp.types.CreateTaskResult
-```
-
Call a tool by name.
This is the public API for executing tools. By default, middleware is applied.
@@ -349,13 +365,14 @@ This is the public API for executing tools. By default, middleware is applied.
- `version`: Specific version to call. If None, calls highest version.
- `run_middleware`: If True (default), apply the middleware chain.
Set to False when called from middleware to avoid re-applying.
-- `task_meta`: If provided, execute as a background task and return
-CreateTaskResult. If None (default), execute synchronously and
-return ToolResult.
**Returns:**
-- ToolResult when task_meta is None.
-- CreateTaskResult when task_meta is provided.
+- ToolResult.
+
+A guard tool that requests client input (SEP-2322 multi-round-trip)
+returns an ``InputRequiredToolResult`` (a ``ToolResult`` subclass); it
+flows back through the middleware chain as an ordinary result and the
+wire handler unwraps it into an ``InputRequiredResult`` on the response.
**Raises:**
- `NotFoundError`: If tool not found or disabled
@@ -363,24 +380,12 @@ return ToolResult.
- `ValidationError`: If arguments fail validation
-#### `read_resource`
+#### `read_resource`
```python
read_resource(self, uri: str) -> ResourceResult
```
-#### `read_resource`
-
-```python
-read_resource(self, uri: str) -> mcp.types.CreateTaskResult
-```
-
-#### `read_resource`
-
-```python
-read_resource(self, uri: str) -> ResourceResult | mcp.types.CreateTaskResult
-```
-
Read a resource by URI.
This is the public API for reading resources. By default, middleware is applied.
@@ -391,37 +396,21 @@ Checks concrete resources first, then templates.
- `version`: Specific version to read. If None, reads highest version.
- `run_middleware`: If True (default), apply the middleware chain.
Set to False when called from middleware to avoid re-applying.
-- `task_meta`: If provided, execute as a background task and return
-CreateTaskResult. If None (default), execute synchronously and
-return ResourceResult.
**Returns:**
-- ResourceResult when task_meta is None.
-- CreateTaskResult when task_meta is provided.
+- ResourceResult.
**Raises:**
- `NotFoundError`: If resource not found or disabled
- `ResourceError`: If resource read fails
-#### `render_prompt`
+#### `render_prompt`
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult
```
-#### `render_prompt`
-
-```python
-render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult
-```
-
-#### `render_prompt`
-
-```python
-render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult | mcp.types.CreateTaskResult
-```
-
Render a prompt by name.
This is the public API for rendering prompts. By default, middleware is applied.
@@ -433,20 +422,16 @@ Use get_prompt() to retrieve the prompt definition without rendering.
- `version`: Specific version to render. If None, renders highest version.
- `run_middleware`: If True (default), apply the middleware chain.
Set to False when called from middleware to avoid re-applying.
-- `task_meta`: If provided, execute as a background task and return
-CreateTaskResult. If None (default), execute synchronously and
-return PromptResult.
**Returns:**
-- PromptResult when task_meta is None.
-- CreateTaskResult when task_meta is provided.
+- PromptResult.
**Raises:**
- `NotFoundError`: If prompt not found or disabled
- `PromptError`: If prompt rendering fails
-#### `add_tool`
+#### `add_tool`
```python
add_tool(self, tool: Tool | Callable[..., Any]) -> Tool
@@ -464,38 +449,19 @@ with the Context type annotation. See the @tool decorator for examples.
- The tool instance that was added to the server.
-#### `remove_tool`
-
-```python
-remove_tool(self, name: str, version: str | None = None) -> None
-```
-
-Remove tool(s) from the server.
-
-.. deprecated::
- Use ``mcp.local_provider.remove_tool(name)`` instead.
-
-**Args:**
-- `name`: The name of the tool to remove.
-- `version`: If None, removes ALL versions. If specified, removes only that version.
-
-**Raises:**
-- `NotFoundError`: If no matching tool is found.
-
-
-#### `tool`
+#### `tool`
```python
tool(self, name_or_fn: F) -> F
```
-#### `tool`
+#### `tool`
```python
tool(self, name_or_fn: str | None = None) -> Callable[[F], F]
```
-#### `tool`
+#### `tool`
```python
tool(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]
@@ -521,8 +487,6 @@ This decorator supports multiple calling patterns:
- `tags`: Optional set of tags for categorizing the tool
- `output_schema`: Optional JSON schema for the tool's output
- `annotations`: Optional annotations about the tool's behavior
-- `exclude_args`: Optional list of argument names to exclude from the tool schema.
-Deprecated\: Use `Depends()` for dependency injection instead.
- `meta`: Optional meta information about the tool
**Examples:**
@@ -551,7 +515,7 @@ server.tool(my_function, name="custom_name")
```
-#### `add_resource`
+#### `add_resource`
```python
add_resource(self, resource: Resource | Callable[..., Any]) -> Resource | ResourceTemplate
@@ -566,7 +530,7 @@ Add a resource to the server.
- The resource instance that was added to the server.
-#### `add_template`
+#### `add_template`
```python
add_template(self, template: ResourceTemplate) -> ResourceTemplate
@@ -581,7 +545,7 @@ Add a resource template to the server.
- The template instance that was added to the server.
-#### `resource`
+#### `resource`
```python
resource(self, uri: str) -> Callable[[F], F]
@@ -640,7 +604,7 @@ async def get_weather(city: str) -> str:
```
-#### `add_prompt`
+#### `add_prompt`
```python
add_prompt(self, prompt: Prompt | Callable[..., Any]) -> Prompt
@@ -655,19 +619,19 @@ Add a prompt to the server.
- The prompt instance that was added to the server.
-#### `prompt`
+#### `prompt`
```python
prompt(self, name_or_fn: F) -> F
```
-#### `prompt`
+#### `prompt`
```python
prompt(self, name_or_fn: str | None = None) -> Callable[[F], F]
```
-#### `prompt`
+#### `prompt`
```python
prompt(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt | partial[Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt]
@@ -744,19 +708,97 @@ Decorator to register a prompt.
```
-#### `mount`
+#### `add_completion_handler`
```python
-mount(self, server: FastMCP[LifespanResultT], namespace: str | None = None, as_proxy: bool | None = None, tool_names: dict[str, str] | None = None, prefix: str | None = None) -> None
+add_completion_handler(self, handler: CompletionHandler) -> None
+```
+
+Register the server's argument-completion handler.
+
+A server has a single completion handler that answers every
+`completion/complete` request, switching on the reference (a prompt or
+resource template) and the argument being completed. Registering it also
+registers the low-level `completion/complete` handler, which is what
+makes the SDK declare the completions capability — so the capability is
+advertised exactly when the server can answer. Calling this again
+replaces the handler.
+
+**Args:**
+- `handler`: A callable taking the reference, the
+`CompletionArgument`, and the optional `CompletionContext`, and
+returning candidate values (a `Completion`, a list of strings,
+or None). May be sync or async.
+
+
+#### `completion`
+
+```python
+completion(self, handler: CompletionHandler) -> CompletionHandler
+```
+
+#### `completion`
+
+```python
+completion(self) -> Callable[[CompletionHandler], CompletionHandler]
+```
+
+#### `completion`
+
+```python
+completion(self, handler: CompletionHandler | None = None) -> CompletionHandler | Callable[[CompletionHandler], CompletionHandler]
+```
+
+Decorator to register the server's argument-completion handler.
+
+The handler answers `completion/complete` requests for prompt arguments
+and resource-template parameters. It receives the reference being
+completed, the argument (its name and the partial value typed so far),
+and the context of arguments already supplied, and returns candidate
+values. Return a list of strings, a `Completion` (to include pagination
+hints), or None when the reference/argument is not one it handles — an
+unhandled reference yields an empty completion, not an error.
+
+Registering a handler declares the completions capability; a server with
+none does not advertise it. This works identically on the handshake and
+modern protocol eras.
+
+Supports both `@mcp.completion` and `@mcp.completion()`.
+
+Example:
+
+ ```python
+ from fastmcp import FastMCP
+ from mcp_types import Completion, PromptReference
+
+ mcp = FastMCP("Completion Server")
+
+ @mcp.prompt
+ def poem(theme: str) -> str:
+ return f"Write a poem about {theme}"
+
+ @mcp.completion
+ def complete(ref, argument, context):
+ if isinstance(ref, PromptReference) and ref.name == "poem":
+ if argument.name == "theme":
+ options = ["nature", "love", "adventure"]
+ return [o for o in options if o.startswith(argument.value)]
+ return None
+ ```
+
+
+#### `mount`
+
+```python
+mount(self, server: FastMCP[LifespanResultT], namespace: str | None = None, tool_names: dict[str, str] | None = None) -> None
```
Mount another FastMCP server on this server with an optional namespace.
-Unlike importing (with import_server), mounting establishes a dynamic connection
-between servers. When a client interacts with a mounted server's objects through
-the parent server, requests are forwarded to the mounted server in real-time.
-This means changes to the mounted server are immediately reflected when accessed
-through the parent.
+Mounting establishes a dynamic connection between servers. When a client
+interacts with a mounted server's objects through the parent server, requests
+are forwarded to the mounted server in real-time. This means changes to the
+mounted server are immediately reflected when accessed through the parent.
When a server is mounted with a namespace:
- Tools from the mounted server are accessible with namespaced names.
@@ -782,69 +824,26 @@ middleware chain is invoked for all operations (tool calls, resource reads, prom
- `server`: The FastMCP server to mount.
- `namespace`: Optional namespace to use for the mounted server's objects. If None,
the server's objects are accessible with their original names.
-- `as_proxy`: Deprecated. Mounted servers now always have their lifespan and
-middleware invoked. To create a proxy server, use create_proxy()
-explicitly before mounting.
- `tool_names`: Optional mapping of original tool names to custom names. Use this
to override namespaced names. Keys are the original tool names from the
mounted server.
-- `prefix`: Deprecated. Use namespace instead.
-#### `import_server`
+#### `from_openapi`
```python
-import_server(self, server: FastMCP[LifespanResultT], prefix: str | None = None) -> None
-```
-
-Import the MCP objects from another FastMCP server into this one,
-optionally with a given prefix.
-
-.. deprecated::
- Use :meth:`mount` instead. ``import_server`` will be removed in a
- future version.
-
-Note that when a server is *imported*, its objects are immediately
-registered to the importing server. This is a one-time operation and
-future changes to the imported server will not be reflected in the
-importing server. Server-level configurations and lifespans are not imported.
-
-When a server is imported with a prefix:
-- The tools are imported with prefixed names
- Example: If server has a tool named "get_weather", it will be
- available as "prefix_get_weather"
-- The resources are imported with prefixed URIs using the new format
- Example: If server has a resource with URI "weather://forecast", it will
- be available as "weather://prefix/forecast"
-- The templates are imported with prefixed URI templates using the new format
- Example: If server has a template with URI "weather://location/{id}", it will
- be available as "weather://prefix/location/{id}"
-- The prompts are imported with prefixed names
- Example: If server has a prompt named "weather_prompt", it will be available as
- "prefix_weather_prompt"
-
-When a server is imported without a prefix (prefix=None), its tools, resources,
-templates, and prompts are imported with their original names.
-
-**Args:**
-- `server`: The FastMCP server to import
-- `prefix`: Optional prefix to use for the imported server's objects. If None,
-objects are imported with their original names.
-
-
-#### `from_openapi`
-
-```python
-from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient | None = None, name: str = 'OpenAPI Server', route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, validate_output: bool = True, **settings: Any) -> Self
+from_openapi(cls, openapi_spec: dict[str, Any], client: httpx2.AsyncClient | None = None, name: str = 'OpenAPI Server', route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, validate_output: bool = True, **settings: Any) -> Self
```
Create a FastMCP server from an OpenAPI specification.
**Args:**
- `openapi_spec`: OpenAPI schema as a dictionary
-- `client`: Optional httpx AsyncClient for making HTTP requests.
+- `client`: Optional httpx2 AsyncClient for making HTTP requests.
If not provided, a default client is created using the first
server URL from the OpenAPI spec with a 30-second timeout.
+Legacy httpx clients are temporarily accepted with a deprecation
+warning.
- `name`: Name for the MCP server
- `route_maps`: Optional list of RouteMap objects defining route mappings
- `route_map_fn`: Optional callable for advanced route type mapping
@@ -861,7 +860,7 @@ response structure while still returning structured JSON.
- A FastMCP server with an OpenAPIProvider attached.
-#### `from_fastapi`
+#### `from_fastapi`
```python
from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] | None = None, route_map_fn: OpenAPIRouteMapFn | None = None, mcp_component_fn: OpenAPIComponentFn | None = None, mcp_names: dict[str, str] | None = None, httpx_client_kwargs: dict[str, Any] | None = None, tags: set[str] | None = None, **settings: Any) -> Self
@@ -876,7 +875,7 @@ Create a FastMCP server from a FastAPI application.
- `route_map_fn`: Optional callable for advanced route type mapping
- `mcp_component_fn`: Optional callable for component customization
- `mcp_names`: Optional dictionary mapping operationId to component names
-- `httpx_client_kwargs`: Optional kwargs passed to httpx.AsyncClient.
+- `httpx_client_kwargs`: Optional kwargs passed to httpx2.AsyncClient.
Use this to configure timeout and other client settings.
- `tags`: Optional set of tags to add to all components
- `**settings`: Additional settings passed to FastMCP
@@ -885,25 +884,7 @@ Use this to configure timeout and other client settings.
- A FastMCP server with an OpenAPIProvider attached.
-#### `as_proxy`
-
-```python
-as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
-```
-
-Create a FastMCP proxy server for the given backend.
-
-.. deprecated::
- Use :func:`fastmcp.server.create_proxy` instead.
- This method will be removed in a future version.
-
-The `backend` argument can be either an existing `fastmcp.client.Client`
-instance or any value accepted as the `transport` argument of
-`fastmcp.client.Client`. This mirrors the convenience of the
-`fastmcp.client.Client` constructor.
-
-
-#### `generate_name`
+#### `generate_name`
```python
generate_name(cls, name: str | None = None) -> str
diff --git a/docs/python-sdk/fastmcp-server-session_scoped_event_store.mdx b/docs/python-sdk/fastmcp-server-session_scoped_event_store.mdx
new file mode 100644
index 000000000..b595a435b
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-session_scoped_event_store.mdx
@@ -0,0 +1,31 @@
+---
+title: session_scoped_event_store
+sidebarTitle: session_scoped_event_store
+---
+
+# `fastmcp.server.session_scoped_event_store`
+
+
+Lightweight session scoping for Streamable HTTP event stores.
+
+## Classes
+
+### `SessionScopedEventStore`
+
+
+EventStore adapter that isolates stream IDs to one transport session.
+
+
+**Methods:**
+
+#### `store_event`
+
+```python
+store_event(self, stream_id: StreamId, message: JSONRPCMessage | None) -> EventId
+```
+
+#### `replay_events_after`
+
+```python
+replay_events_after(self, last_event_id: EventId, send_callback: EventCallback) -> StreamId | None
+```
diff --git a/docs/python-sdk/fastmcp-server-sessions.mdx b/docs/python-sdk/fastmcp-server-sessions.mdx
new file mode 100644
index 000000000..0caea6b13
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-sessions.mdx
@@ -0,0 +1,319 @@
+---
+title: sessions
+sidebarTitle: sessions
+---
+
+# `fastmcp.server.sessions`
+
+
+Stateless session state: server-side per-user and per-session storage.
+
+Modern (2026-07-28) MCP connections are stateless by construction — every
+request builds a fresh connection whose in-memory state is discarded when the
+request returns. This module gives tools two explicit ways to keep state across
+calls, both backed by the server's existing state store and both isolated by the
+authenticated principal rather than by any client-declared identifier.
+
+- `Session`: async `get`/`set`/`delete`/`clear` over a single dict stored under
+ one key, scoped to a `(principal, session_id)` pair. This is the state-accessor
+ object a handler works with — the value the standalone `get_session(id)`
+ returns and the value injected for a `UserSession` parameter.
+- `session: UserSession` (injected): a per-user bucket, dependency-injected like
+ `ctx: Context` and keyed by the request's authenticated principal. Requires
+ auth. `UserSession` is the injection annotation; the injected value is a
+ `Session`. It is always available under auth — no `create_session`, no
+ provider, no validation.
+- `session_id: SessionId` (argument): a required string the agent supplies,
+ resolved with the standalone `await get_session(session_id)`. The id is
+ minted
+ by `create_session`; an id that was never created (or was created under a
+ different principal) is rejected. This validation is the whole guarantee — an
+ unminted id never resolves, so nothing enforces provider registration.
+- `SessionProvider`: a `Provider` contributing `create_session` / `end_session`
+ tools. Register it with `mcp.add_provider(SessionProvider())` so a tool that
+ takes `session_id` has a way to mint ids; without it, no id can be created, so
+ those tools simply cannot resolve a session.
+
+Isolation is the authenticated principal, not the session id. State keyed by
+`(principal, session_id)` means a request under principal B can never address
+principal A's keys, no matter what `session_id` it passes; the id only organizes
+sessions within a principal. Without auth there is no principal wall — a session
+id is a bearer capability and sessions are not a boundary between clients.
+
+
+## Functions
+
+### `current_principal`
+
+```python
+current_principal() -> str | None
+```
+
+
+The authenticated principal for the current request as a compact JSON string.
+
+Returns the `(client_id, issuer, subject)` triple encoded as compact JSON, or
+`None` on an unauthenticated request. Two users of one OAuth client are
+distinct principals whenever the token verifier supplies a subject.
+
+
+### `session_storage_key`
+
+```python
+session_storage_key(principal: str | None, session_id: str) -> str
+```
+
+
+The single storage key holding a session's state dict.
+
+Keyed by `(principal, session_id)`: the principal is the isolation wall, the
+id organizes sessions within it. A session's whole state lives under this one
+key as a dict, so one key means one store TTL per session and `end` is a
+single delete.
+
+
+### `session_id_parameter_names`
+
+```python
+session_id_parameter_names(fn: Callable[..., object]) -> tuple[str, ...]
+```
+
+
+Names of a function's parameters annotated with `SessionId`.
+
+Scans resolved type hints for `Annotated[str, _SessionIdMarker()]` metadata.
+Returns an empty tuple when the hints cannot be resolved (the function then
+simply carries no auto-populated session-id description).
+
+`functools.partial` is unwrapped first, since `get_type_hints` rejects a
+partial object — FastMCP supports registering a partial as a tool, and its
+schema is still built from the underlying function, so its `SessionId`
+parameters must be detected here too. Parameters the partial has already
+bound — positionally or by keyword — are dropped, matching the tool's actual
+argument surface (the partial's own signature already reflects this).
+
+
+### `CurrentSession`
+
+```python
+CurrentSession() -> Session
+```
+
+
+Inject the per-user `Session` for the current authenticated principal.
+
+Rarely written explicitly — a `session: UserSession` parameter is rewritten
+to this. Provided for parity with `CurrentContext()` when an explicit default
+is preferred.
+
+
+### `OptionalCurrentSession`
+
+```python
+OptionalCurrentSession() -> Session | None
+```
+
+
+Inject the per-user `Session`, or `None` when the request is unauthenticated.
+
+Rarely written explicitly — a `session: UserSession | None = None` parameter
+is rewritten to this. Provided for parity with `OptionalCurrentContext()`.
+
+
+### `create_session`
+
+```python
+create_session() -> str
+```
+
+
+Create a new session and return its identifier.
+
+Mints an unguessable `uuid4`, records an initial session owned by the current
+principal, and returns the id as a string. Store it and pass it back as a
+`session_id` argument on later calls to persist state across a session — only
+an id created this way resolves. State is keyed by the authenticated
+principal, so the id organizes sessions within a user; on an unauthenticated
+connection the id is the only thing standing between callers, which is why it
+is unguessable.
+
+
+### `end_session`
+
+```python
+end_session(session_id: SessionId) -> str
+```
+
+
+End a session and delete all of its state.
+
+Validates the id like any other resolution (an unknown or foreign id is
+rejected), then deletes the session's key so the id no longer resolves.
+
+
+## Classes
+
+### `SessionAuthError`
+
+
+An injected `session: UserSession` was requested with no authenticated principal.
+
+Per-user session injection keys off the request's authenticated principal, so
+it is only meaningful under auth. A tool that needs cross-call state without
+auth should take a `session_id: SessionId` argument instead.
+
+
+### `InvalidSession`
+
+
+A session id did not resolve to a session created under the current principal.
+
+Raised by `get_session(session_id)` when the id was never created, or was
+created under a different principal. The public message is deliberately
+generic — the specific reason (which id, which principal) is logged at debug
+level, not returned to the caller, so an attacker cannot distinguish "unknown
+id" from "belongs to someone else".
+
+
+### `Session`
+
+
+Async accessors over one `(principal, session_id)` bucket of state.
+
+A session's state is a single dict stored under one key. That dict holds user
+state in a `state` sub-dict and a small creation marker alongside it, so a
+created-but-empty session is still distinguishable from a missing one.
+`get`/`set`/`delete` read-modify-write the sub-dict; `clear` empties the
+sub-dict but keeps the session valid; `end` deletes the whole key. Writes
+never impose a TTL — retention is entirely the server store's (configure it on
+the store you pass to `FastMCP(session_state_store=...)`).
+
+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.
+
+
+**Methods:**
+
+#### `id`
+
+```python
+id(self) -> str | None
+```
+
+The session's identifier, or `None` for an injected per-user session.
+
+For a session resolved from a `session_id` argument (or minted by
+`create_session`) this is that id. An injected `UserSession` has no
+distinct id — its bucket is the authenticated user — so it is `None`; the
+internal principal-derived key is deliberately not exposed here.
+
+
+#### `get`
+
+```python
+get(self, key: str, default: Any = None) -> Any
+```
+
+Return the value for `key`, or `default` when it is not set.
+
+
+#### `set`
+
+```python
+set(self, key: str, value: Any) -> None
+```
+
+Store `value` under `key` in this session (read-modify-write).
+
+Preserves the creation marker: only the user-state sub-dict is touched.
+
+
+#### `delete`
+
+```python
+delete(self, key: str) -> None
+```
+
+Remove `key` from this session, if present (preserves the marker).
+
+
+#### `clear`
+
+```python
+clear(self) -> None
+```
+
+Empty the session's user state but keep the session valid.
+
+The user-state sub-dict is reset to empty while the creation marker stays
+in place, so a cleared session still resolves through `get_session`.
+To invalidate a session entirely, use `end` (what `end_session` calls).
+
+
+#### `end`
+
+```python
+end(self) -> None
+```
+
+Invalidate the session — delete its one key and all of its state.
+
+After this the id no longer resolves through `get_session`. This is
+what `end_session` calls; `clear` only empties state and keeps the session.
+
+
+### `UserSession`
+
+
+Annotation marker for the injected per-user session.
+
+A `session: UserSession` parameter is **dependency-injected** like
+`ctx: Context`: keyed by the request's authenticated principal, excluded from
+the input schema, and requiring auth (it raises `SessionAuthError` with no
+principal). It doubles as the injection *annotation* and the injected
+type — the value a handler receives is a `UserSession`, which subclasses
+`Session`, so `await session.get(...)`, `.set`, `.delete`, and `.clear` all
+work exactly as on any other `Session`.
+
+Unlike `session_id: SessionId`, the per-user bucket needs no `create_session`,
+no `SessionProvider`, and no validation — it is always available under auth,
+keyed directly by the caller's identity.
+
+```python
+from fastmcp.server.sessions import UserSession
+
+@mcp.tool
+async def remember(fact: str, session: UserSession) -> str:
+ await session.set("fact", fact)
+ return "noted"
+```
+
+Subclasses `Session` only so the framework's type-based injection detector can
+key off it; it adds no behavior of its own.
+
+
+### `SessionProvider`
+
+
+Provider contributing the session lifecycle tools.
+
+Register it whenever a tool declares a `session_id: SessionId` argument:
+
+```python
+from fastmcp.server.sessions import SessionProvider
+
+mcp.add_provider(SessionProvider())
+```
+
+It registers two tools:
+
+- `create_session()` mints an unguessable `uuid4`, records the session, and
+ returns the id.
+- `end_session(session_id)` invalidates that session and deletes its state.
+
+It owns no storage (session state lives in the server's configured
+`session_state_store`) and imposes no TTL (retention is the store's). It
+exists to mint and end owned session ids. Registration is not enforced: with
+no provider, no id can be created, so every `get_session(...)` rejects —
+a `session_id` tool without a provider simply cannot resolve a session.
+
diff --git a/docs/python-sdk/fastmcp-server-tasks-__init__.mdx b/docs/python-sdk/fastmcp-server-tasks-__init__.mdx
deleted file mode 100644
index 9c9f5e88e..000000000
--- a/docs/python-sdk/fastmcp-server-tasks-__init__.mdx
+++ /dev/null
@@ -1,12 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.server.tasks`
-
-
-MCP SEP-1686 background tasks support.
-
-This module implements protocol-level background task execution for MCP servers.
-
diff --git a/docs/python-sdk/fastmcp-server-tasks-capabilities.mdx b/docs/python-sdk/fastmcp-server-tasks-capabilities.mdx
deleted file mode 100644
index 03b1102dd..000000000
--- a/docs/python-sdk/fastmcp-server-tasks-capabilities.mdx
+++ /dev/null
@@ -1,29 +0,0 @@
----
-title: capabilities
-sidebarTitle: capabilities
----
-
-# `fastmcp.server.tasks.capabilities`
-
-
-SEP-1686 task capabilities declaration.
-
-## Functions
-
-### `get_task_capabilities`
-
-```python
-get_task_capabilities() -> ServerTasksCapability | None
-```
-
-
-Return the SEP-1686 task capabilities.
-
-Returns task capabilities as a first-class ServerCapabilities field,
-declaring support for list, cancel, and request operations per SEP-1686.
-
-Returns None if pydocket is not installed (no task support).
-
-Note: prompts/resources are passed via extra_data since the SDK types
-don't include them yet (FastMCP supports them ahead of the spec).
-
diff --git a/docs/python-sdk/fastmcp-server-tasks-config.mdx b/docs/python-sdk/fastmcp-server-tasks-config.mdx
deleted file mode 100644
index 0dc2bf4ba..000000000
--- a/docs/python-sdk/fastmcp-server-tasks-config.mdx
+++ /dev/null
@@ -1,96 +0,0 @@
----
-title: config
-sidebarTitle: config
----
-
-# `fastmcp.server.tasks.config`
-
-
-TaskConfig for MCP SEP-1686 background task execution modes.
-
-This module defines the configuration for how tools, resources, and prompts
-handle task-augmented execution as specified in SEP-1686.
-
-
-## Classes
-
-### `TaskMeta`
-
-
-Metadata for task-augmented execution requests.
-
-When passed to call_tool/read_resource/get_prompt, signals that
-the operation should be submitted as a background task.
-
-**Attributes:**
-- `ttl`: Client-requested TTL in milliseconds. If None, uses server default.
-- `fn_key`: Docket routing key. Auto-derived from component name if None.
-
-
-### `TaskConfig`
-
-
-Configuration for MCP background task execution (SEP-1686).
-
-Controls how a component handles task-augmented requests:
-
-- "forbidden": Component does not support task execution. Clients must not
- request task augmentation; server returns -32601 if they do.
-- "optional": Component supports both synchronous and task execution.
- Client may request task augmentation or call normally.
-- "required": Component requires task execution. Clients must request task
- augmentation; server returns -32601 if they don't.
-
-
-**Methods:**
-
-#### `from_bool`
-
-```python
-from_bool(cls, value: bool) -> TaskConfig
-```
-
-Convert boolean task flag to TaskConfig.
-
-**Args:**
-- `value`: True for "optional" mode, False for "forbidden" mode.
-
-**Returns:**
-- TaskConfig with appropriate mode.
-
-
-#### `supports_tasks`
-
-```python
-supports_tasks(self) -> bool
-```
-
-Check if this component supports task execution.
-
-**Returns:**
-- True if mode is "optional" or "required", False if "forbidden".
-
-
-#### `validate_function`
-
-```python
-validate_function(self, fn: Callable[..., Any], name: str) -> None
-```
-
-Validate that function is compatible with this task config.
-
-Task execution requires:
-1. fastmcp[tasks] to be installed (pydocket)
-2. Async functions
-
-Raises ImportError if mode is "optional" or "required" but pydocket
-is not installed. Raises ValueError if function is synchronous.
-
-**Args:**
-- `fn`: The function to validate (handles callable classes and staticmethods).
-- `name`: Name for error messages.
-
-**Raises:**
-- `ImportError`: If task execution is enabled but pydocket not installed.
-- `ValueError`: If task execution is enabled but function is sync.
-
diff --git a/docs/python-sdk/fastmcp-server-tasks-elicitation.mdx b/docs/python-sdk/fastmcp-server-tasks-elicitation.mdx
deleted file mode 100644
index cc6f3dea2..000000000
--- a/docs/python-sdk/fastmcp-server-tasks-elicitation.mdx
+++ /dev/null
@@ -1,96 +0,0 @@
----
-title: elicitation
-sidebarTitle: elicitation
----
-
-# `fastmcp.server.tasks.elicitation`
-
-
-Background task elicitation support (SEP-1686).
-
-This module provides elicitation capabilities for background tasks running
-in Docket workers. Unlike regular MCP requests, background tasks don't have
-an active request context, so elicitation requires special handling:
-
-1. Set task status to "input_required" via Redis
-2. Send notifications/tasks/status with elicitation metadata
-3. Wait for client to send input via tasks/sendInput
-4. Resume task execution with the provided input
-
-This uses the public MCP SDK APIs where possible, with minimal use of
-internal APIs for background task coordination.
-
-
-## Functions
-
-### `elicit_for_task`
-
-```python
-elicit_for_task(task_id: str, session: ServerSession | None, message: str, schema: dict[str, Any], fastmcp: FastMCP) -> mcp.types.ElicitResult
-```
-
-
-Send an elicitation request from a background task.
-
-This function handles the complexity of eliciting user input when running
-in a Docket worker context where there's no active MCP request.
-
-**Args:**
-- `task_id`: The background task ID
-- `session`: The MCP ServerSession for this task
-- `message`: The message to display to the user
-- `schema`: The JSON schema for the expected response
-- `fastmcp`: The FastMCP server instance
-
-**Returns:**
-- ElicitResult containing the user's response
-
-**Raises:**
-- `RuntimeError`: If Docket is not available
-- `McpError`: If the elicitation request fails
-
-
-### `relay_elicitation`
-
-```python
-relay_elicitation(session: ServerSession, session_id: str, task_id: str, elicitation: dict[str, Any], fastmcp: FastMCP) -> None
-```
-
-
-Relay elicitation from a background task worker to the client.
-
-Called by the notification subscriber when it detects an input_required
-notification with elicitation metadata. Sends a standard elicitation/create
-request to the client session, then uses handle_task_input() to push the
-response to Redis so the blocked worker can resume.
-
-**Args:**
-- `session`: MCP ServerSession
-- `session_id`: Session identifier
-- `task_id`: Background task ID
-- `elicitation`: Elicitation metadata (message, requestedSchema)
-- `fastmcp`: FastMCP server instance
-
-
-### `handle_task_input`
-
-```python
-handle_task_input(task_id: str, session_id: str, action: str, content: dict[str, Any] | None, fastmcp: FastMCP) -> bool
-```
-
-
-Handle input sent to a background task via tasks/sendInput.
-
-This is called when a client sends input in response to an elicitation
-request from a background task.
-
-**Args:**
-- `task_id`: The background task ID
-- `session_id`: The MCP session ID
-- `action`: The elicitation action ("accept", "decline", "cancel")
-- `content`: The response content (for "accept" action)
-- `fastmcp`: The FastMCP server instance
-
-**Returns:**
-- True if the input was successfully stored, False otherwise
-
diff --git a/docs/python-sdk/fastmcp-server-tasks-handlers.mdx b/docs/python-sdk/fastmcp-server-tasks-handlers.mdx
deleted file mode 100644
index 31f228f14..000000000
--- a/docs/python-sdk/fastmcp-server-tasks-handlers.mdx
+++ /dev/null
@@ -1,41 +0,0 @@
----
-title: handlers
-sidebarTitle: handlers
----
-
-# `fastmcp.server.tasks.handlers`
-
-
-SEP-1686 task execution handlers.
-
-Handles queuing tool/prompt/resource executions to Docket as background tasks.
-
-
-## Functions
-
-### `submit_to_docket`
-
-```python
-submit_to_docket(task_type: Literal['tool', 'resource', 'template', 'prompt'], key: str, component: Tool | Resource | ResourceTemplate | Prompt, arguments: dict[str, Any] | None = None, task_meta: TaskMeta | None = None) -> mcp.types.CreateTaskResult
-```
-
-
-Submit any component to Docket for background execution (SEP-1686).
-
-Unified handler for all component types. Called by component's internal
-methods (_run, _read, _render) when task metadata is present and mode allows.
-
-Queues the component's method to Docket, stores raw return values,
-and converts to MCP types on retrieval.
-
-**Args:**
-- `task_type`: Component type for task key construction
-- `key`: The component key as seen by MCP layer (with namespace prefix)
-- `component`: The component instance (Tool, Resource, ResourceTemplate, Prompt)
-- `arguments`: Arguments/params (None for Resource which has no args)
-- `task_meta`: Task execution metadata. If task_meta.ttl is provided, it
-overrides the server default (docket.execution_ttl).
-
-**Returns:**
-- Task stub with proper Task object
-
diff --git a/docs/python-sdk/fastmcp-server-tasks-keys.mdx b/docs/python-sdk/fastmcp-server-tasks-keys.mdx
deleted file mode 100644
index a274d3c1d..000000000
--- a/docs/python-sdk/fastmcp-server-tasks-keys.mdx
+++ /dev/null
@@ -1,89 +0,0 @@
----
-title: keys
-sidebarTitle: keys
----
-
-# `fastmcp.server.tasks.keys`
-
-
-Task key management for SEP-1686 background tasks.
-
-Task keys encode security scoping and metadata in the Docket key format:
- `{session_id}:{client_task_id}:{task_type}:{component_identifier}`
-
-This format provides:
-- Session-based security scoping (prevents cross-session access)
-- Task type identification (tool/prompt/resource)
-- Component identification (name or URI for result conversion)
-
-
-## Functions
-
-### `build_task_key`
-
-```python
-build_task_key(session_id: str, client_task_id: str, task_type: str, component_identifier: str) -> str
-```
-
-
-Build Docket task key with embedded metadata.
-
-Format: `{session_id}:{client_task_id}:{task_type}:{component_identifier}`
-
-The component_identifier is URI-encoded to handle special characters (colons, slashes, etc.).
-
-**Args:**
-- `session_id`: Session ID for security scoping
-- `client_task_id`: Client-provided task ID
-- `task_type`: Type of task ("tool", "prompt", "resource")
-- `component_identifier`: Tool name, prompt name, or resource URI
-
-**Returns:**
-- Encoded task key for Docket
-
-**Examples:**
-
->>> build_task_key("session123", "task456", "tool", "my_tool")
-'session123:task456:tool:my_tool'
->>> build_task_key("session123", "task456", "resource", "file://data.txt")
-'session123:task456:resource:file%3A%2F%2Fdata.txt'
-
-
-### `parse_task_key`
-
-```python
-parse_task_key(task_key: str) -> dict[str, str]
-```
-
-
-Parse Docket task key to extract metadata.
-
-**Args:**
-- `task_key`: Encoded task key from Docket
-
-**Returns:**
-- Dict with keys: session_id, client_task_id, task_type, component_identifier
-
-**Examples:**
-
->>> parse_task_key("session123:task456:tool:my_tool")
-`{'session_id': 'session123', 'client_task_id': 'task456', 'task_type': 'tool', 'component_identifier': 'my_tool'}`
->>> parse_task_key("session123:task456:resource:file%3A%2F%2Fdata.txt")
-`{'session_id': 'session123', 'client_task_id': 'task456', 'task_type': 'resource', 'component_identifier': 'file://data.txt'}`
-
-
-### `get_client_task_id_from_key`
-
-```python
-get_client_task_id_from_key(task_key: str) -> str
-```
-
-
-Extract just the client task ID from a task key.
-
-**Args:**
-- `task_key`: Full encoded task key
-
-**Returns:**
-- Client-provided task ID (second segment)
-
diff --git a/docs/python-sdk/fastmcp-server-tasks-notifications.mdx b/docs/python-sdk/fastmcp-server-tasks-notifications.mdx
deleted file mode 100644
index 217b4b6ac..000000000
--- a/docs/python-sdk/fastmcp-server-tasks-notifications.mdx
+++ /dev/null
@@ -1,113 +0,0 @@
----
-title: notifications
-sidebarTitle: notifications
----
-
-# `fastmcp.server.tasks.notifications`
-
-
-Distributed notification queue for background task events (SEP-1686).
-
-Enables distributed Docket workers to send MCP notifications to clients
-without holding session references. Workers push to a Redis queue,
-the MCP server process subscribes and forwards to the client's session.
-
-Pattern: Fire-and-forward with retry
-- One queue per session_id
-- LPUSH/BRPOP for reliable ordered delivery
-- Retry up to 3 times on delivery failure, then discard
-- TTL-based expiration for stale messages
-
-Note: Docket's execution.subscribe() handles task state/progress events via
-Redis Pub/Sub. This module handles elicitation-specific notifications that
-require reliable delivery (input_required prompts, cancel signals).
-
-
-## Functions
-
-### `push_notification`
-
-```python
-push_notification(session_id: str, notification: dict[str, Any], docket: Docket) -> None
-```
-
-
-Push notification to session's queue (called from Docket worker).
-
-Used for elicitation-specific notifications (input_required, cancel)
-that need reliable delivery across distributed processes.
-
-**Args:**
-- `session_id`: Target session's identifier
-- `notification`: MCP notification dict (method, params, _meta)
-- `docket`: Docket instance for Redis access
-
-
-### `notification_subscriber_loop`
-
-```python
-notification_subscriber_loop(session_id: str, session: ServerSession, docket: Docket, fastmcp: FastMCP) -> None
-```
-
-
-Subscribe to notification queue and forward to session.
-
-Runs in the MCP server process. Bridges distributed workers to clients.
-
-This loop:
-1. Maintains a heartbeat (active subscriber marker for debugging)
-2. Blocks on BRPOP waiting for notifications
-3. Forwards notifications to the client's session
-4. Retries failed deliveries, then discards (no dead-letter queue)
-
-**Args:**
-- `session_id`: Session identifier to subscribe to
-- `session`: MCP ServerSession for sending notifications
-- `docket`: Docket instance for Redis access
-- `fastmcp`: FastMCP server instance (for elicitation relay)
-
-
-### `ensure_subscriber_running`
-
-```python
-ensure_subscriber_running(session_id: str, session: ServerSession, docket: Docket, fastmcp: FastMCP) -> None
-```
-
-
-Start notification subscriber if not already running (idempotent).
-
-Subscriber is created on first task submission and cleaned up on disconnect.
-Safe to call multiple times for the same session.
-
-**Args:**
-- `session_id`: Session identifier
-- `session`: MCP ServerSession
-- `docket`: Docket instance
-- `fastmcp`: FastMCP server instance (for elicitation relay)
-
-
-### `stop_subscriber`
-
-```python
-stop_subscriber(session_id: str) -> None
-```
-
-
-Stop notification subscriber for a session.
-
-Called when session disconnects. Pending messages remain in queue
-for delivery if client reconnects (with TTL expiration).
-
-**Args:**
-- `session_id`: Session identifier
-
-
-### `get_subscriber_count`
-
-```python
-get_subscriber_count() -> int
-```
-
-
-Get number of active subscribers (for monitoring).
-
diff --git a/docs/python-sdk/fastmcp-server-tasks-requests.mdx b/docs/python-sdk/fastmcp-server-tasks-requests.mdx
deleted file mode 100644
index 5cde802fc..000000000
--- a/docs/python-sdk/fastmcp-server-tasks-requests.mdx
+++ /dev/null
@@ -1,91 +0,0 @@
----
-title: requests
-sidebarTitle: requests
----
-
-# `fastmcp.server.tasks.requests`
-
-
-SEP-1686 task request handlers.
-
-Handles MCP task protocol requests: tasks/get, tasks/result, tasks/list, tasks/cancel.
-These handlers query and manage existing tasks (contrast with handlers.py which creates tasks).
-
-This module requires fastmcp[tasks] (pydocket). It is only imported when docket is available.
-
-
-## Functions
-
-### `tasks_get_handler`
-
-```python
-tasks_get_handler(server: FastMCP, params: dict[str, Any]) -> GetTaskResult
-```
-
-
-Handle MCP 'tasks/get' request (SEP-1686).
-
-**Args:**
-- `server`: FastMCP server instance
-- `params`: Request params containing taskId
-
-**Returns:**
-- Task status response with spec-compliant fields
-
-
-### `tasks_result_handler`
-
-```python
-tasks_result_handler(server: FastMCP, params: dict[str, Any]) -> Any
-```
-
-
-Handle MCP 'tasks/result' request (SEP-1686).
-
-Converts raw task return values to MCP types based on task type.
-
-**Args:**
-- `server`: FastMCP server instance
-- `params`: Request params containing taskId
-
-**Returns:**
-- MCP result (CallToolResult, GetPromptResult, or ReadResourceResult)
-
-
-### `tasks_list_handler`
-
-```python
-tasks_list_handler(server: FastMCP, params: dict[str, Any]) -> ListTasksResult
-```
-
-
-Handle MCP 'tasks/list' request (SEP-1686).
-
-Note: With client-side tracking, this returns minimal info.
-
-**Args:**
-- `server`: FastMCP server instance
-- `params`: Request params (cursor, limit)
-
-**Returns:**
-- Response with tasks list and pagination
-
-
-### `tasks_cancel_handler`
-
-```python
-tasks_cancel_handler(server: FastMCP, params: dict[str, Any]) -> CancelTaskResult
-```
-
-
-Handle MCP 'tasks/cancel' request (SEP-1686).
-
-Cancels a running task, transitioning it to cancelled state.
-
-**Args:**
-- `server`: FastMCP server instance
-- `params`: Request params containing taskId
-
-**Returns:**
-- Task status response showing cancelled state
-
diff --git a/docs/python-sdk/fastmcp-server-tasks-routing.mdx b/docs/python-sdk/fastmcp-server-tasks-routing.mdx
deleted file mode 100644
index 435d62775..000000000
--- a/docs/python-sdk/fastmcp-server-tasks-routing.mdx
+++ /dev/null
@@ -1,37 +0,0 @@
----
-title: routing
-sidebarTitle: routing
----
-
-# `fastmcp.server.tasks.routing`
-
-
-Task routing helper for MCP components.
-
-Provides unified task mode enforcement and docket routing logic.
-
-
-## Functions
-
-### `check_background_task`
-
-```python
-check_background_task(component: Tool | Resource | ResourceTemplate | Prompt, task_type: TaskType, arguments: dict[str, Any] | None = None, task_meta: TaskMeta | None = None) -> mcp.types.CreateTaskResult | None
-```
-
-
-Check task mode and submit to background if requested.
-
-**Args:**
-- `component`: The MCP component
-- `task_type`: Type of task ("tool", "resource", "template", "prompt")
-- `arguments`: Arguments for tool/prompt/template execution
-- `task_meta`: Task execution metadata. If provided, execute as background task.
-
-**Returns:**
-- CreateTaskResult if submitted to docket, None for sync execution
-
-**Raises:**
-- `McpError`: If mode="required" but no task metadata, or mode="forbidden"
- but task metadata is present
-
diff --git a/docs/python-sdk/fastmcp-server-tasks-subscriptions.mdx b/docs/python-sdk/fastmcp-server-tasks-subscriptions.mdx
deleted file mode 100644
index 2fd2e3cd4..000000000
--- a/docs/python-sdk/fastmcp-server-tasks-subscriptions.mdx
+++ /dev/null
@@ -1,38 +0,0 @@
----
-title: subscriptions
-sidebarTitle: subscriptions
----
-
-# `fastmcp.server.tasks.subscriptions`
-
-
-Task subscription helpers for sending MCP notifications (SEP-1686).
-
-Subscribes to Docket execution state changes and sends notifications/tasks/status
-to clients when their tasks change state.
-
-This module requires fastmcp[tasks] (pydocket). It is only imported when docket is available.
-
-
-## Functions
-
-### `subscribe_to_task_updates`
-
-```python
-subscribe_to_task_updates(task_id: str, task_key: str, session: ServerSession, docket: Docket, poll_interval_ms: int = 5000) -> None
-```
-
-
-Subscribe to Docket execution events and send MCP notifications.
-
-Per SEP-1686 lines 436-444, servers MAY send notifications/tasks/status
-when task state changes. This is an optional optimization that reduces
-client polling frequency.
-
-**Args:**
-- `task_id`: Client-visible task ID (server-generated UUID)
-- `task_key`: Internal Docket execution key (includes session, type, component)
-- `session`: MCP ServerSession for sending notifications
-- `docket`: Docket instance for subscribing to execution events
-- `poll_interval_ms`: Poll interval in milliseconds to include in notifications
-
diff --git a/docs/python-sdk/fastmcp-server-telemetry.mdx b/docs/python-sdk/fastmcp-server-telemetry.mdx
index 09d2b0d76..874fcf1e9 100644
--- a/docs/python-sdk/fastmcp-server-telemetry.mdx
+++ b/docs/python-sdk/fastmcp-server-telemetry.mdx
@@ -10,7 +10,7 @@ Server-side telemetry helpers.
## Functions
-### `get_auth_span_attributes`
+### `get_auth_span_attributes`
```python
get_auth_span_attributes() -> dict[str, str]
@@ -20,7 +20,7 @@ get_auth_span_attributes() -> dict[str, str]
Get auth attributes for the current request, if authenticated.
-### `get_session_span_attributes`
+### `get_session_span_attributes`
```python
get_session_span_attributes() -> dict[str, str]
@@ -30,22 +30,83 @@ get_session_span_attributes() -> dict[str, str]
Get session attributes for the current request.
-### `server_span`
+### `get_protocol_span_attributes`
```python
-server_span(name: str, method: str, server_name: str, component_type: str, component_key: str, resource_uri: str | None = None) -> Generator[Span, None, None]
+get_protocol_span_attributes() -> dict[str, str]
```
-Create a SERVER span with standard MCP attributes and auth context.
+Get the negotiated MCP protocol version for the current request.
+
+Mirrors the `mcp.protocol.version` attribute the SDK's own
+`OpenTelemetryMiddleware` sets — FastMCP drops that middleware to avoid a
+duplicate SERVER span, so this restores the attribute on FastMCP's span.
+
+
+### `record_span_exception`
+
+```python
+record_span_exception(span: Span, e: Exception) -> None
+```
+
+
+Record an exception and error status on a span.
+
+
+### `seam_span`
+
+```python
+seam_span(method: str, server_name: str) -> Generator[Span, None, None]
+```
+
+
+Open the per-request SERVER span at the FastMCP middleware seam.
+
+The span is named after the method and carries the base MCP attributes
+(`mcp.method.name`, `fastmcp.server.name`, auth/session context) so
+seam-only methods (`logging/setLevel`, `tasks/*`, `ping`, `initialize`, ...)
+are fully attributed even though they never reach the high-level path. It is
+marked with `SEAM_SPAN_MARKER` so a later `server_span` call in the
+high-level path enriches this span with component attributes instead of
+opening a second one. Exceptions raised anywhere below the seam — including
+rejections *before* the high-level path (auth, not-found, middleware vetoes)
+that would otherwise produce no SERVER span at all — are recorded here.
+
+In `propagation_only` mode no span is opened at all — this is the one place
+that has to know the difference, because the seam is where the incoming
+`_meta` parent context is applied for the whole request.
+
+
+### `server_span`
+
+```python
+server_span(name: str, method: str, server_name: str, component_type: str, component_key: str, resource_uri: str | None = None, tool_name: str | None = None, prompt_name: str | None = None) -> Generator[Span, None, None]
+```
+
+
+Emit or enrich a SERVER span with standard MCP attributes and auth context.
+
+When the current active span is the request's seam span (opened by
+`FastMCPServerMiddleware` and marked with `SEAM_SPAN_MARKER`), this sets the
+component attributes on that span and yields it *without* starting a second
+span — so failures rejected before this point and the successful high-level
+call share one richly-attributed SERVER span. Otherwise (non-seam contexts,
+e.g. in-process `mcp.call_tool()` calls that bypass the dispatcher) it opens a
+new SERVER span as before.
Automatically records any exception on the span and sets error status.
+In `propagation_only` mode no span is opened or enriched. The seam has
+normally already attached the incoming parent context for this request;
+doing it again here is a no-op, and covers the in-process callers that
+bypass the dispatcher and so never reach the seam at all.
-### `delegate_span`
+
+### `delegate_span`
```python
-delegate_span(name: str, provider_type: str, component_key: str) -> Generator[Span, None, None]
+delegate_span(name: str, provider_type: str, component_key: str, method: str | None = None) -> Generator[Span, None, None]
```
diff --git a/docs/python-sdk/fastmcp-server-transforms-catalog.mdx b/docs/python-sdk/fastmcp-server-transforms-catalog.mdx
deleted file mode 100644
index 1dd2cfe4a..000000000
--- a/docs/python-sdk/fastmcp-server-transforms-catalog.mdx
+++ /dev/null
@@ -1,220 +0,0 @@
----
-title: catalog
-sidebarTitle: catalog
----
-
-# `fastmcp.server.transforms.catalog`
-
-
-Base class for transforms that need to read the real component catalog.
-
-Some transforms replace ``list_tools()`` output with synthetic components
-(e.g. a search interface) while still needing access to the *real*
-(auth-filtered) catalog at call time. ``CatalogTransform`` provides the
-bypass machinery so subclasses can call ``get_tool_catalog()`` without
-triggering their own replacement logic.
-
-Re-entrancy problem
--------------------
-
-When a synthetic tool handler calls ``get_tool_catalog()``, that calls
-``ctx.fastmcp.list_tools()`` which re-enters the transform pipeline —
-including *this* transform's ``list_tools()``. If the subclass overrides
-``list_tools()`` directly, the re-entrant call would hit the subclass's
-replacement logic again (returning synthetic tools instead of the real
-catalog). A ``super()`` call can't prevent this because Python can't
-short-circuit a method after ``super()`` returns.
-
-Solution: ``CatalogTransform`` owns ``list_tools()`` and uses a
-per-instance ``ContextVar`` to detect re-entrant calls. During bypass,
-it passes through to the base ``Transform.list_tools()`` (a no-op).
-Otherwise, it delegates to ``transform_tools()`` — the subclass hook
-where replacement logic lives. Same pattern for resources, prompts,
-and resource templates.
-
-This is *not* the same as the ``Provider._list_tools()`` convention
-(which produces raw components with no arguments). ``transform_tools()``
-receives the current catalog and returns a transformed version. The
-distinct name avoids confusion between the two patterns.
-
-Usage::
-
- class MyTransform(CatalogTransform):
- async def transform_tools(self, tools):
- return [self._make_search_tool()]
-
- def _make_search_tool(self):
- async def search(ctx: Context = None):
- real_tools = await self.get_tool_catalog(ctx)
- ...
- return Tool.from_function(fn=search, name="search")
-
-
-## Classes
-
-### `CatalogTransform`
-
-
-Transform that needs access to the real component catalog.
-
-Subclasses override ``transform_tools()`` / ``transform_resources()``
-/ ``transform_prompts()`` / ``transform_resource_templates()``
-instead of the ``list_*()`` methods. The base class owns
-``list_*()`` and handles re-entrant bypass automatically — subclasses
-never see re-entrant calls from ``get_*_catalog()``.
-
-The ``get_*_catalog()`` methods fetch the real (auth-filtered) catalog
-by temporarily setting a bypass flag so that this transform's
-``list_*()`` passes through without calling the subclass hook.
-
-
-**Methods:**
-
-#### `list_tools`
-
-```python
-list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
-```
-
-#### `list_resources`
-
-```python
-list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]
-```
-
-#### `list_resource_templates`
-
-```python
-list_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]
-```
-
-#### `list_prompts`
-
-```python
-list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]
-```
-
-#### `transform_tools`
-
-```python
-transform_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
-```
-
-Transform the tool catalog.
-
-Override this method to replace, filter, or augment the tool listing.
-The default implementation passes through unchanged.
-
-Do NOT override ``list_tools()`` directly — the base class uses it
-to handle re-entrant bypass when ``get_tool_catalog()`` reads the
-real catalog.
-
-
-#### `transform_resources`
-
-```python
-transform_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]
-```
-
-Transform the resource catalog.
-
-Override this method to replace, filter, or augment the resource listing.
-The default implementation passes through unchanged.
-
-Do NOT override ``list_resources()`` directly — the base class uses it
-to handle re-entrant bypass when ``get_resource_catalog()`` reads the
-real catalog.
-
-
-#### `transform_resource_templates`
-
-```python
-transform_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]
-```
-
-Transform the resource template catalog.
-
-Override this method to replace, filter, or augment the template listing.
-The default implementation passes through unchanged.
-
-Do NOT override ``list_resource_templates()`` directly — the base class
-uses it to handle re-entrant bypass when
-``get_resource_template_catalog()`` reads the real catalog.
-
-
-#### `transform_prompts`
-
-```python
-transform_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]
-```
-
-Transform the prompt catalog.
-
-Override this method to replace, filter, or augment the prompt listing.
-The default implementation passes through unchanged.
-
-Do NOT override ``list_prompts()`` directly — the base class uses it
-to handle re-entrant bypass when ``get_prompt_catalog()`` reads the
-real catalog.
-
-
-#### `get_tool_catalog`
-
-```python
-get_tool_catalog(self, ctx: Context) -> Sequence[Tool]
-```
-
-Fetch the real tool catalog, bypassing this transform.
-
-**Args:**
-- `ctx`: The current request context.
-- `run_middleware`: Whether to run middleware on the inner call.
-Defaults to True because this is typically called from a
-tool handler where list_tools middleware has not yet run.
-
-
-#### `get_resource_catalog`
-
-```python
-get_resource_catalog(self, ctx: Context) -> Sequence[Resource]
-```
-
-Fetch the real resource catalog, bypassing this transform.
-
-**Args:**
-- `ctx`: The current request context.
-- `run_middleware`: Whether to run middleware on the inner call.
-Defaults to True because this is typically called from a
-tool handler where list_resources middleware has not yet run.
-
-
-#### `get_prompt_catalog`
-
-```python
-get_prompt_catalog(self, ctx: Context) -> Sequence[Prompt]
-```
-
-Fetch the real prompt catalog, bypassing this transform.
-
-**Args:**
-- `ctx`: The current request context.
-- `run_middleware`: Whether to run middleware on the inner call.
-Defaults to True because this is typically called from a
-tool handler where list_prompts middleware has not yet run.
-
-
-#### `get_resource_template_catalog`
-
-```python
-get_resource_template_catalog(self, ctx: Context) -> Sequence[ResourceTemplate]
-```
-
-Fetch the real resource template catalog, bypassing this transform.
-
-**Args:**
-- `ctx`: The current request context.
-- `run_middleware`: Whether to run middleware on the inner call.
-Defaults to True because this is typically called from a
-tool handler where list_resource_templates middleware has
-not yet run.
-
diff --git a/docs/python-sdk/fastmcp-server-transforms-namespace.mdx b/docs/python-sdk/fastmcp-server-transforms-namespace.mdx
deleted file mode 100644
index 9f590356d..000000000
--- a/docs/python-sdk/fastmcp-server-transforms-namespace.mdx
+++ /dev/null
@@ -1,96 +0,0 @@
----
-title: namespace
-sidebarTitle: namespace
----
-
-# `fastmcp.server.transforms.namespace`
-
-
-Namespace transform for prefixing component names.
-
-## Classes
-
-### `Namespace`
-
-
-Prefixes component names with a namespace.
-
-- Tools: name → namespace_name
-- Prompts: name → namespace_name
-- Resources: protocol://path → protocol://namespace/path
-- Resource Templates: same as resources
-
-
-**Methods:**
-
-#### `list_tools`
-
-```python
-list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
-```
-
-Prefix tool names with namespace.
-
-
-#### `get_tool`
-
-```python
-get_tool(self, name: str, call_next: GetToolNext) -> Tool | None
-```
-
-Get tool by namespaced name.
-
-
-#### `list_resources`
-
-```python
-list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]
-```
-
-Add namespace path segment to resource URIs.
-
-
-#### `get_resource`
-
-```python
-get_resource(self, uri: str, call_next: GetResourceNext) -> Resource | None
-```
-
-Get resource by namespaced URI.
-
-
-#### `list_resource_templates`
-
-```python
-list_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]
-```
-
-Add namespace path segment to template URIs.
-
-
-#### `get_resource_template`
-
-```python
-get_resource_template(self, uri: str, call_next: GetResourceTemplateNext) -> ResourceTemplate | None
-```
-
-Get resource template by namespaced URI.
-
-
-#### `list_prompts`
-
-```python
-list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]
-```
-
-Prefix prompt names with namespace.
-
-
-#### `get_prompt`
-
-```python
-get_prompt(self, name: str, call_next: GetPromptNext) -> Prompt | None
-```
-
-Get prompt by namespaced name.
-
diff --git a/docs/python-sdk/fastmcp-server-transforms-prompts_as_tools.mdx b/docs/python-sdk/fastmcp-server-transforms-prompts_as_tools.mdx
deleted file mode 100644
index f1656c263..000000000
--- a/docs/python-sdk/fastmcp-server-transforms-prompts_as_tools.mdx
+++ /dev/null
@@ -1,59 +0,0 @@
----
-title: prompts_as_tools
-sidebarTitle: prompts_as_tools
----
-
-# `fastmcp.server.transforms.prompts_as_tools`
-
-
-Transform that exposes prompts as tools.
-
-This transform generates tools for listing and getting prompts, enabling
-clients that only support tools to access prompt functionality.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.transforms import PromptsAsTools
-
- mcp = FastMCP("Server")
- mcp.add_transform(PromptsAsTools(mcp))
- # Now has list_prompts and get_prompt tools
- ```
-
-
-## Classes
-
-### `PromptsAsTools`
-
-
-Transform that adds tools for listing and getting prompts.
-
-Generates two tools:
-- `list_prompts`: Lists all prompts from the provider
-- `get_prompt`: Gets a specific prompt with optional arguments
-
-The transform captures a provider reference at construction and queries it
-for prompts when the generated tools are called. When used with FastMCP,
-the provider's auth and visibility filtering is automatically applied.
-
-
-**Methods:**
-
-#### `list_tools`
-
-```python
-list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
-```
-
-Add prompt tools to the tool list.
-
-
-#### `get_tool`
-
-```python
-get_tool(self, name: str, call_next: GetToolNext) -> Tool | None
-```
-
-Get a tool by name, including generated prompt tools.
-
diff --git a/docs/python-sdk/fastmcp-server-transforms-resources_as_tools.mdx b/docs/python-sdk/fastmcp-server-transforms-resources_as_tools.mdx
deleted file mode 100644
index 3b46e875c..000000000
--- a/docs/python-sdk/fastmcp-server-transforms-resources_as_tools.mdx
+++ /dev/null
@@ -1,59 +0,0 @@
----
-title: resources_as_tools
-sidebarTitle: resources_as_tools
----
-
-# `fastmcp.server.transforms.resources_as_tools`
-
-
-Transform that exposes resources as tools.
-
-This transform generates tools for listing and reading resources, enabling
-clients that only support tools to access resource functionality.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.transforms import ResourcesAsTools
-
- mcp = FastMCP("Server")
- mcp.add_transform(ResourcesAsTools(mcp))
- # Now has list_resources and read_resource tools
- ```
-
-
-## Classes
-
-### `ResourcesAsTools`
-
-
-Transform that adds tools for listing and reading resources.
-
-Generates two tools:
-- `list_resources`: Lists all resources and templates from the provider
-- `read_resource`: Reads a resource by URI
-
-The transform captures a provider reference at construction and queries it
-for resources when the generated tools are called. When used with FastMCP,
-the provider's auth and visibility filtering is automatically applied.
-
-
-**Methods:**
-
-#### `list_tools`
-
-```python
-list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
-```
-
-Add resource tools to the tool list.
-
-
-#### `get_tool`
-
-```python
-get_tool(self, name: str, call_next: GetToolNext) -> Tool | None
-```
-
-Get a tool by name, including generated resource tools.
-
diff --git a/docs/python-sdk/fastmcp-server-transforms-search-__init__.mdx b/docs/python-sdk/fastmcp-server-transforms-search-__init__.mdx
deleted file mode 100644
index 80b71d226..000000000
--- a/docs/python-sdk/fastmcp-server-transforms-search-__init__.mdx
+++ /dev/null
@@ -1,23 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.server.transforms.search`
-
-
-Search transforms for tool discovery.
-
-Search transforms collapse a large tool catalog into a search interface,
-letting LLMs discover tools on demand instead of seeing the full list.
-
-Example:
- ```python
- from fastmcp import FastMCP
- from fastmcp.server.transforms.search import RegexSearchTransform
-
- mcp = FastMCP("Server")
- mcp.add_transform(RegexSearchTransform())
- # list_tools now returns only search_tools + call_tool
- ```
-
diff --git a/docs/python-sdk/fastmcp-server-transforms-search-base.mdx b/docs/python-sdk/fastmcp-server-transforms-search-base.mdx
deleted file mode 100644
index 7ecd3b5f2..000000000
--- a/docs/python-sdk/fastmcp-server-transforms-search-base.mdx
+++ /dev/null
@@ -1,105 +0,0 @@
----
-title: base
-sidebarTitle: base
----
-
-# `fastmcp.server.transforms.search.base`
-
-
-Base class for search transforms.
-
-Search transforms replace ``list_tools()`` output with a small set of
-synthetic tools — a search tool and a call-tool proxy — so LLMs can
-discover tools on demand instead of receiving the full catalog.
-
-All concrete search transforms (``RegexSearchTransform``,
-``BM25SearchTransform``, etc.) inherit from ``BaseSearchTransform`` and
-implement ``_make_search_tool()`` and ``_search()`` to provide their
-specific search strategy.
-
-Example::
-
- from fastmcp import FastMCP
- from fastmcp.server.transforms.search import RegexSearchTransform
-
- mcp = FastMCP("Server")
-
- @mcp.tool
- def add(a: int, b: int) -> int: ...
-
- @mcp.tool
- def multiply(x: float, y: float) -> float: ...
-
- # Clients now see only ``search_tools`` and ``call_tool``.
- # The original tools are discoverable via search.
- mcp.add_transform(RegexSearchTransform())
-
-
-## Functions
-
-### `serialize_tools_for_output_json`
-
-```python
-serialize_tools_for_output_json(tools: Sequence[Tool]) -> list[dict[str, Any]]
-```
-
-
-Serialize tools to the same dict format as ``list_tools`` output.
-
-
-### `serialize_tools_for_output_markdown`
-
-```python
-serialize_tools_for_output_markdown(tools: Sequence[Tool]) -> str
-```
-
-
-Serialize tools to compact markdown, using ~65-70% fewer tokens than JSON.
-
-
-## Classes
-
-### `BaseSearchTransform`
-
-
-Replace the tool listing with a search interface.
-
-When this transform is active, ``list_tools()`` returns only:
-
-* Any tools listed in ``always_visible`` (pinned).
-* A **search tool** that finds tools matching a query.
-* A **call_tool** proxy that executes tools discovered via search.
-
-Hidden tools remain callable — ``get_tool()`` delegates unknown
-names downstream, so direct calls and the call-tool proxy both work.
-
-Search results respect the full auth pipeline: middleware, visibility
-transforms, and component-level auth checks all apply.
-
-**Args:**
-- `max_results`: Maximum number of tools returned per search.
-- `always_visible`: Tool names that stay in the ``list_tools``
-output alongside the synthetic search/call tools.
-- `search_tool_name`: Name of the generated search tool.
-- `call_tool_name`: Name of the generated call-tool proxy.
-
-
-**Methods:**
-
-#### `transform_tools`
-
-```python
-transform_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
-```
-
-Replace the catalog with pinned + synthetic search/call tools.
-
-
-#### `get_tool`
-
-```python
-get_tool(self, name: str, call_next: GetToolNext) -> Tool | None
-```
-
-Intercept synthetic tool names; delegate everything else.
-
diff --git a/docs/python-sdk/fastmcp-server-transforms-search-bm25.mdx b/docs/python-sdk/fastmcp-server-transforms-search-bm25.mdx
deleted file mode 100644
index d5264f46a..000000000
--- a/docs/python-sdk/fastmcp-server-transforms-search-bm25.mdx
+++ /dev/null
@@ -1,20 +0,0 @@
----
-title: bm25
-sidebarTitle: bm25
----
-
-# `fastmcp.server.transforms.search.bm25`
-
-
-BM25-based search transform.
-
-## Classes
-
-### `BM25SearchTransform`
-
-
-Search transform using BM25 Okapi relevance ranking.
-
-Maintains an in-memory index that is lazily rebuilt when the tool
-catalog changes (detected via a hash of tool names).
-
diff --git a/docs/python-sdk/fastmcp-server-transforms-search-regex.mdx b/docs/python-sdk/fastmcp-server-transforms-search-regex.mdx
deleted file mode 100644
index e36c8d25e..000000000
--- a/docs/python-sdk/fastmcp-server-transforms-search-regex.mdx
+++ /dev/null
@@ -1,20 +0,0 @@
----
-title: regex
-sidebarTitle: regex
----
-
-# `fastmcp.server.transforms.search.regex`
-
-
-Regex-based search transform.
-
-## Classes
-
-### `RegexSearchTransform`
-
-
-Search transform using regex pattern matching.
-
-Tools are matched against their name, description, and parameter
-information using ``re.search`` with ``re.IGNORECASE``.
-
diff --git a/docs/python-sdk/fastmcp-server-transforms-tool_transform.mdx b/docs/python-sdk/fastmcp-server-transforms-tool_transform.mdx
deleted file mode 100644
index d911f4819..000000000
--- a/docs/python-sdk/fastmcp-server-transforms-tool_transform.mdx
+++ /dev/null
@@ -1,40 +0,0 @@
----
-title: tool_transform
-sidebarTitle: tool_transform
----
-
-# `fastmcp.server.transforms.tool_transform`
-
-
-Transform for applying tool transformations.
-
-## Classes
-
-### `ToolTransform`
-
-
-Applies tool transformations to modify tool schemas.
-
-Wraps ToolTransformConfig to apply argument renames, schema changes,
-hidden arguments, and other transformations at the transform level.
-
-
-**Methods:**
-
-#### `list_tools`
-
-```python
-list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
-```
-
-Apply transforms to matching tools.
-
-
-#### `get_tool`
-
-```python
-get_tool(self, name: str, call_next: GetToolNext) -> Tool | None
-```
-
-Get tool by transformed name.
-
diff --git a/docs/python-sdk/fastmcp-server-transforms-version_filter.mdx b/docs/python-sdk/fastmcp-server-transforms-version_filter.mdx
deleted file mode 100644
index 7902c0a0a..000000000
--- a/docs/python-sdk/fastmcp-server-transforms-version_filter.mdx
+++ /dev/null
@@ -1,89 +0,0 @@
----
-title: version_filter
-sidebarTitle: version_filter
----
-
-# `fastmcp.server.transforms.version_filter`
-
-
-Version filter transform for filtering components by version range.
-
-## Classes
-
-### `VersionFilter`
-
-
-Filters components by version range.
-
-When applied to a provider or server, components within the version range
-are visible, and unversioned components are included by default. Within
-that filtered set, the highest version of each component is exposed to
-clients (standard deduplication behavior). Set
-``include_unversioned=False`` to exclude unversioned components.
-
-Parameters mirror comparison operators for clarity:
-
- # Versions < 3.0 (v1 and v2)
- server.add_transform(VersionFilter(version_lt="3.0"))
-
- # Versions >= 2.0 and < 3.0 (only v2.x)
- server.add_transform(VersionFilter(version_gte="2.0", version_lt="3.0"))
-
-Works with any version string - PEP 440 (1.0, 2.0) or dates (2025-01-01).
-
-**Args:**
-- `version_gte`: Versions >= this value pass through.
-- `version_lt`: Versions < this value pass through.
-- `include_unversioned`: Whether unversioned components (``version=None``)
-should pass through the filter. Defaults to True.
-
-
-**Methods:**
-
-#### `list_tools`
-
-```python
-list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
-```
-
-#### `get_tool`
-
-```python
-get_tool(self, name: str, call_next: GetToolNext) -> Tool | None
-```
-
-#### `list_resources`
-
-```python
-list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]
-```
-
-#### `get_resource`
-
-```python
-get_resource(self, uri: str, call_next: GetResourceNext) -> Resource | None
-```
-
-#### `list_resource_templates`
-
-```python
-list_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]
-```
-
-#### `get_resource_template`
-
-```python
-get_resource_template(self, uri: str, call_next: GetResourceTemplateNext) -> ResourceTemplate | None
-```
-
-#### `list_prompts`
-
-```python
-list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]
-```
-
-#### `get_prompt`
-
-```python
-get_prompt(self, name: str, call_next: GetPromptNext) -> Prompt | None
-```
diff --git a/docs/python-sdk/fastmcp-server-transforms-visibility.mdx b/docs/python-sdk/fastmcp-server-transforms-visibility.mdx
deleted file mode 100644
index 772cddc97..000000000
--- a/docs/python-sdk/fastmcp-server-transforms-visibility.mdx
+++ /dev/null
@@ -1,261 +0,0 @@
----
-title: visibility
-sidebarTitle: visibility
----
-
-# `fastmcp.server.transforms.visibility`
-
-
-Visibility transform for marking component visibility state.
-
-Each Visibility instance marks components via internal metadata. Multiple
-visibility transforms can be stacked - later transforms override earlier ones.
-Final filtering happens at the Provider level.
-
-
-## Functions
-
-### `is_enabled`
-
-```python
-is_enabled(component: FastMCPComponent) -> bool
-```
-
-
-Check if component is enabled.
-
-Returns True if:
-- No visibility mark exists (default is enabled)
-- Visibility mark is True
-
-Returns False if visibility mark is False.
-
-**Args:**
-- `component`: Component to check.
-
-**Returns:**
-- True if component should be enabled/visible to clients.
-
-
-### `get_visibility_rules`
-
-```python
-get_visibility_rules(context: Context) -> list[dict[str, Any]]
-```
-
-
-Load visibility rule dicts from session state.
-
-
-### `save_visibility_rules`
-
-```python
-save_visibility_rules(context: Context, rules: list[dict[str, Any]]) -> None
-```
-
-
-Save visibility rule dicts to session state and send notifications.
-
-**Args:**
-- `context`: The context to save rules for.
-- `rules`: The visibility rules to save.
-- `components`: Optional hint about which component types are affected.
-If None, sends notifications for all types (safe default).
-If provided, only sends notifications for specified types.
-
-
-### `create_visibility_transforms`
-
-```python
-create_visibility_transforms(rules: list[dict[str, Any]]) -> list[Visibility]
-```
-
-
-Convert rule dicts to Visibility transforms.
-
-
-### `get_session_transforms`
-
-```python
-get_session_transforms(context: Context) -> list[Visibility]
-```
-
-
-Get session-specific Visibility transforms from state store.
-
-
-### `enable_components`
-
-```python
-enable_components(context: Context) -> None
-```
-
-
-Enable components matching criteria for this session only.
-
-Session rules override global transforms. Rules accumulate - each call
-adds a new rule to the session. Later marks override earlier ones
-(Visibility transform semantics).
-
-Sends notifications to this session only: ToolListChangedNotification,
-ResourceListChangedNotification, and PromptListChangedNotification.
-
-**Args:**
-- `context`: The context for this session.
-- `names`: Component names or URIs to match.
-- `keys`: Component keys to match (e.g., {"tool\:my_tool@v1"}).
-- `version`: Component version spec to match.
-- `tags`: Tags to match (component must have at least one).
-- `components`: Component types to match (e.g., {"tool", "prompt"}).
-- `match_all`: If True, matches all components regardless of other criteria.
-
-
-### `disable_components`
-
-```python
-disable_components(context: Context) -> None
-```
-
-
-Disable components matching criteria for this session only.
-
-Session rules override global transforms. Rules accumulate - each call
-adds a new rule to the session. Later marks override earlier ones
-(Visibility transform semantics).
-
-Sends notifications to this session only: ToolListChangedNotification,
-ResourceListChangedNotification, and PromptListChangedNotification.
-
-**Args:**
-- `context`: The context for this session.
-- `names`: Component names or URIs to match.
-- `keys`: Component keys to match (e.g., {"tool\:my_tool@v1"}).
-- `version`: Component version spec to match.
-- `tags`: Tags to match (component must have at least one).
-- `components`: Component types to match (e.g., {"tool", "prompt"}).
-- `match_all`: If True, matches all components regardless of other criteria.
-
-
-### `reset_visibility`
-
-```python
-reset_visibility(context: Context) -> None
-```
-
-
-Clear all session visibility rules.
-
-Use this to reset session visibility back to global defaults.
-
-Sends notifications to this session only: ToolListChangedNotification,
-ResourceListChangedNotification, and PromptListChangedNotification.
-
-**Args:**
-- `context`: The context for this session.
-
-
-### `apply_session_transforms`
-
-```python
-apply_session_transforms(components: Sequence[ComponentT]) -> Sequence[ComponentT]
-```
-
-
-Apply session-specific visibility transforms to components.
-
-This helper applies session-level enable/disable rules by marking
-components with their visibility state. Session transforms override
-global transforms due to mark-based semantics (later marks win).
-
-**Args:**
-- `components`: The components to apply session transforms to.
-
-**Returns:**
-- The components with session transforms applied.
-
-
-## Classes
-
-### `Visibility`
-
-
-Sets visibility state on matching components.
-
-Does NOT filter inline - just marks components with visibility state.
-Later transforms in the chain can override earlier marks.
-Final filtering happens at the Provider level after all transforms run.
-
-
-**Methods:**
-
-#### `list_tools`
-
-```python
-list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
-```
-
-Mark tools by visibility state.
-
-
-#### `get_tool`
-
-```python
-get_tool(self, name: str, call_next: GetToolNext) -> Tool | None
-```
-
-Mark tool if found.
-
-
-#### `list_resources`
-
-```python
-list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]
-```
-
-Mark resources by visibility state.
-
-
-#### `get_resource`
-
-```python
-get_resource(self, uri: str, call_next: GetResourceNext) -> Resource | None
-```
-
-Mark resource if found.
-
-
-#### `list_resource_templates`
-
-```python
-list_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]
-```
-
-Mark resource templates by visibility state.
-
-
-#### `get_resource_template`
-
-```python
-get_resource_template(self, uri: str, call_next: GetResourceTemplateNext) -> ResourceTemplate | None
-```
-
-Mark resource template if found.
-
-
-#### `list_prompts`
-
-```python
-list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]
-```
-
-Mark prompts by visibility state.
-
-
-#### `get_prompt`
-
-```python
-get_prompt(self, name: str, call_next: GetPromptNext) -> Prompt | None
-```
-
-Mark prompt if found.
-
diff --git a/docs/python-sdk/fastmcp-server-transforms-__init__.mdx b/docs/python-sdk/fastmcp-server-transforms.mdx
similarity index 69%
rename from docs/python-sdk/fastmcp-server-transforms-__init__.mdx
rename to docs/python-sdk/fastmcp-server-transforms.mdx
index f98150302..7e6d19054 100644
--- a/docs/python-sdk/fastmcp-server-transforms-__init__.mdx
+++ b/docs/python-sdk/fastmcp-server-transforms.mdx
@@ -1,6 +1,6 @@
---
-title: __init__
-sidebarTitle: __init__
+title: transforms
+sidebarTitle: transforms
---
# `fastmcp.server.transforms`
@@ -28,31 +28,31 @@ Example:
## Classes
-### `GetToolNext`
+### `GetToolNext`
Protocol for get_tool call_next functions.
-### `GetResourceNext`
+### `GetResourceNext`
Protocol for get_resource call_next functions.
-### `GetResourceTemplateNext`
+### `GetResourceTemplateNext`
Protocol for get_resource_template call_next functions.
-### `GetPromptNext`
+### `GetPromptNext`
Protocol for get_prompt call_next functions.
-### `Transform`
+### `Transform`
Base class for component transformations.
@@ -64,7 +64,7 @@ with `call_next` to chain lookups.
**Methods:**
-#### `list_tools`
+#### `list_tools`
```python
list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
@@ -79,7 +79,7 @@ List tools with transformation applied.
- Transformed sequence of tools.
-#### `get_tool`
+#### `get_tool`
```python
get_tool(self, name: str, call_next: GetToolNext) -> Tool | None
@@ -96,7 +96,7 @@ Get a tool by name.
- The tool if found, None otherwise.
-#### `list_resources`
+#### `list_resources`
```python
list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]
@@ -111,7 +111,7 @@ List resources with transformation applied.
- Transformed sequence of resources.
-#### `get_resource`
+#### `get_resource`
```python
get_resource(self, uri: str, call_next: GetResourceNext) -> Resource | None
@@ -128,7 +128,7 @@ Get a resource by URI.
- The resource if found, None otherwise.
-#### `list_resource_templates`
+#### `list_resource_templates`
```python
list_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]
@@ -143,7 +143,7 @@ List resource templates with transformation applied.
- Transformed sequence of resource templates.
-#### `get_resource_template`
+#### `get_resource_template`
```python
get_resource_template(self, uri: str, call_next: GetResourceTemplateNext) -> ResourceTemplate | None
@@ -160,7 +160,7 @@ Get a resource template by URI.
- The resource template if found, None otherwise.
-#### `list_prompts`
+#### `list_prompts`
```python
list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]
@@ -175,7 +175,7 @@ List prompts with transformation applied.
- Transformed sequence of prompts.
-#### `get_prompt`
+#### `get_prompt`
```python
get_prompt(self, name: str, call_next: GetPromptNext) -> Prompt | None
diff --git a/docs/python-sdk/fastmcp-settings.mdx b/docs/python-sdk/fastmcp-settings.mdx
index eeb1156f8..a0d999d27 100644
--- a/docs/python-sdk/fastmcp-settings.mdx
+++ b/docs/python-sdk/fastmcp-settings.mdx
@@ -7,13 +7,7 @@ sidebarTitle: settings
## Classes
-### `DocketSettings`
-
-
-Docket worker configuration.
-
-
-### `Settings`
+### `Settings`
FastMCP settings.
@@ -21,7 +15,7 @@ FastMCP settings.
**Methods:**
-#### `get_setting`
+#### `get_setting`
```python
get_setting(self, attr: str) -> Any
@@ -31,7 +25,7 @@ Get a setting. If the setting contains one or more `__`, it will be
treated as a nested setting.
-#### `set_setting`
+#### `set_setting`
```python
set_setting(self, attr: str, value: Any) -> None
@@ -41,7 +35,7 @@ Set a setting. If the setting contains one or more `__`, it will be
treated as a nested setting.
-#### `normalize_log_level`
+#### `normalize_log_level`
```python
normalize_log_level(cls, v)
diff --git a/docs/python-sdk/fastmcp-telemetry.mdx b/docs/python-sdk/fastmcp-telemetry.mdx
index 757e41fb9..3cb06ca12 100644
--- a/docs/python-sdk/fastmcp-telemetry.mdx
+++ b/docs/python-sdk/fastmcp-telemetry.mdx
@@ -31,7 +31,52 @@ Example usage with SDK:
## Functions
-### `get_tracer`
+### `telemetry_mode`
+
+```python
+telemetry_mode() -> 'TelemetryMode'
+```
+
+
+Resolve the effective telemetry mode for the current context.
+
+This is `fastmcp.settings.telemetry_mode`, except that an active
+`suppress_fastmcp_telemetry()` block downgrades `native` to
+`propagation_only`. Suppression never upgrades or overrides `off`: `off`
+means FastMCP touches nothing, and a narrower request to skip FastMCP's
+spans cannot re-enable the context propagation `off` deliberately omits.
+
+
+### `native_spans_enabled`
+
+```python
+native_spans_enabled() -> bool
+```
+
+
+Whether FastMCP should create its own spans right now.
+
+
+### `suppress_fastmcp_telemetry`
+
+```python
+suppress_fastmcp_telemetry() -> Iterator[None]
+```
+
+
+Suppress FastMCP's own spans without disabling trace propagation.
+
+Scoped equivalent of `telemetry_mode="propagation_only"`, for callers that
+embed FastMCP inside their own instrumented stack and want to own the MCP
+span hierarchy for a specific block. Narrower than OpenTelemetry's global
+instrumentation suppression: only FastMCP's spans are skipped, so nested
+instrumentation (HTTP clients, databases) keeps emitting, and trace context
+still flows through `_meta` so those spans are parented correctly.
+
+Has no effect when `telemetry_mode` is already `off`.
+
+
+### `get_tracer`
```python
get_tracer(version: str | None = None) -> Tracer
@@ -40,14 +85,24 @@ get_tracer(version: str | None = None) -> Tracer
Get the FastMCP tracer for creating spans.
+Instrumentation is on by default. FastMCP uses only the OpenTelemetry API,
+so span creation is a no-op with negligible overhead unless an OpenTelemetry
+SDK and exporter are configured. When `fastmcp.settings.telemetry_mode` is
+`propagation_only` or `off` — or the caller is inside a
+`suppress_fastmcp_telemetry()` block — this returns a pass-through tracer
+that creates no spans and leaves the current OTel context untouched even
+when an SDK is configured.
+
**Args:**
- `version`: Optional version string for the instrumentation
**Returns:**
-- A tracer instance. Returns a no-op tracer if no SDK is configured.
+- A tracer instance. Returns a non-attaching pass-through tracer when
+- FastMCP's own spans are disabled; span creation is otherwise a no-op
+- unless an SDK is configured.
-### `inject_trace_context`
+### `inject_trace_context`
```python
inject_trace_context(meta: dict[str, Any] | None = None) -> dict[str, Any] | None
@@ -64,7 +119,7 @@ Inject current trace context into a meta dict for MCP request propagation.
- or None if no trace context to inject and meta was None
-### `record_span_error`
+### `record_span_error`
```python
record_span_error(span: Span, exception: BaseException) -> None
@@ -74,7 +129,57 @@ record_span_error(span: Span, exception: BaseException) -> None
Record an exception on a span and set error status.
-### `extract_trace_context`
+### `restore_dropped_attributes`
+
+```python
+restore_dropped_attributes(span: Span, attrs: Mapping[str, otel_types.AttributeValue]) -> None
+```
+
+
+Restore FastMCP attributes a non-forwarding sampler dropped entirely.
+
+`Tracer.start_span` builds the span from `SamplingResult.attributes`, not
+the `attributes=` kwarg it was given for creation — a custom `Sampler`
+whose `SamplingResult.attributes` defaults to `None` silently discards
+every attribute FastMCP passed at creation time. Call this immediately
+after span creation to recover from that case.
+
+The restore only fires when the span has *no* attributes at all AND the
+SDK hasn't evicted anything (`dropped_attributes == 0`):
+
+- A bare, non-forwarding sampler (the regression this exists to fix)
+ leaves the span with an empty attribute mapping, so everything is
+ restored.
+- A sampler that supplied any attributes of its own — whether by
+ forwarding ours untouched, redacting or replacing some of our values,
+ or substituting its own attributes entirely (e.g. to strip component
+ names or resource URIs for privacy or cardinality control) — leaves
+ the span non-empty, so it is left alone entirely. This is what makes
+ the gate precise: a sampler that deliberately supplies only its own
+ attributes must not have them clobbered by a restore that assumes
+ "no FastMCP keys" means "sampler forwarding failed."
+- A sampler that forwards most of our attributes but deliberately drops
+ one is still non-empty, so it's covered by the same "leave alone"
+ branch — a dropped key here is indistinguishable from the SDK's
+ bounded attribute map evicting it, and reinserting it would just push
+ the map's bound and evict a *different* retained key, churning which
+ attributes survive without changing how many are lost. No attempt is
+ made to restore individual missing keys; the gate is all-or-nothing.
+- A low `OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT` that evicts every attribute a
+ forwarding sampler passed through is indistinguishable, from the
+ span's attribute state alone, from a bare non-forwarding sampler —
+ both leave an empty mapping. `dropped_attributes == 0` is what tells
+ them apart: eviction always increments it, so that case is correctly
+ excluded from the restore and the SDK's bounded map is left as
+ computed.
+
+Callers are expected to guard this with `if span.is_recording():`; it
+does no work worth skipping for non-recording spans, but the check is
+kept at call sites so it reads alongside the sibling `is_recording()`
+guards already in those functions.
+
+
+### `extract_trace_context`
```python
extract_trace_context(meta: dict[str, Any] | None) -> Context
diff --git a/docs/python-sdk/fastmcp-tools-__init__.mdx b/docs/python-sdk/fastmcp-tools-__init__.mdx
deleted file mode 100644
index 5b7c8b04d..000000000
--- a/docs/python-sdk/fastmcp-tools-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.tools`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-tools-function_parsing.mdx b/docs/python-sdk/fastmcp-tools-function_parsing.mdx
deleted file mode 100644
index f9cd7f28e..000000000
--- a/docs/python-sdk/fastmcp-tools-function_parsing.mdx
+++ /dev/null
@@ -1,21 +0,0 @@
----
-title: function_parsing
-sidebarTitle: function_parsing
----
-
-# `fastmcp.tools.function_parsing`
-
-
-Function introspection and schema generation for FastMCP tools.
-
-## Classes
-
-### `ParsedFunction`
-
-**Methods:**
-
-#### `from_function`
-
-```python
-from_function(cls, fn: Callable[..., Any], exclude_args: list[str] | None = None, validate: bool = True, wrap_non_object_output_schema: bool = True) -> ParsedFunction
-```
diff --git a/docs/python-sdk/fastmcp-tools-function_tool.mdx b/docs/python-sdk/fastmcp-tools-function_tool.mdx
deleted file mode 100644
index d25c6ecf8..000000000
--- a/docs/python-sdk/fastmcp-tools-function_tool.mdx
+++ /dev/null
@@ -1,108 +0,0 @@
----
-title: function_tool
-sidebarTitle: function_tool
----
-
-# `fastmcp.tools.function_tool`
-
-
-Standalone @tool decorator for FastMCP.
-
-## Functions
-
-### `tool`
-
-```python
-tool(name_or_fn: str | Callable[..., Any] | None = None) -> Any
-```
-
-
-Standalone decorator to mark a function as an MCP tool.
-
-Returns the original function with metadata attached. Register with a server
-using mcp.add_tool().
-
-
-## Classes
-
-### `DecoratedTool`
-
-
-Protocol for functions decorated with @tool.
-
-
-### `ToolMeta`
-
-
-Metadata attached to functions by the @tool decorator.
-
-
-### `FunctionTool`
-
-**Methods:**
-
-#### `to_mcp_tool`
-
-```python
-to_mcp_tool(self, **overrides: Any) -> mcp.types.Tool
-```
-
-Convert the FastMCP tool to an MCP tool.
-
-Extends the base implementation to add task execution mode if enabled.
-
-
-#### `from_function`
-
-```python
-from_function(cls, fn: Callable[..., Any]) -> FunctionTool
-```
-
-Create a FunctionTool from a function.
-
-**Args:**
-- `fn`: The function to wrap
-- `metadata`: ToolMeta object with all configuration. If provided,
-individual parameters must not be passed.
-- `name, title, etc.`: Individual parameters for backwards compatibility.
-Cannot be used together with metadata parameter.
-
-
-#### `run`
-
-```python
-run(self, arguments: dict[str, Any]) -> ToolResult
-```
-
-Run the tool with arguments.
-
-
-#### `register_with_docket`
-
-```python
-register_with_docket(self, docket: Docket) -> None
-```
-
-Register this tool with docket for background execution.
-
-FunctionTool registers the underlying function, which has the user's
-Depends parameters for docket to resolve.
-
-
-#### `add_to_docket`
-
-```python
-add_to_docket(self, docket: Docket, arguments: dict[str, Any], **kwargs: Any) -> Execution
-```
-
-Schedule this tool for background execution via docket.
-
-FunctionTool splats the arguments dict since .fn expects **kwargs.
-
-**Args:**
-- `docket`: The Docket instance
-- `arguments`: Tool arguments
-- `fn_key`: Function lookup key in Docket registry (defaults to self.key)
-- `task_key`: Redis storage key for the result
-- `**kwargs`: Additional kwargs passed to docket.add()
-
diff --git a/docs/python-sdk/fastmcp-tools-tool.mdx b/docs/python-sdk/fastmcp-tools-tool.mdx
deleted file mode 100644
index 0394bf4a5..000000000
--- a/docs/python-sdk/fastmcp-tools-tool.mdx
+++ /dev/null
@@ -1,116 +0,0 @@
----
-title: tool
-sidebarTitle: tool
----
-
-# `fastmcp.tools.tool`
-
-## Functions
-
-### `default_serializer`
-
-```python
-default_serializer(data: Any) -> str
-```
-
-## Classes
-
-### `ToolResult`
-
-**Methods:**
-
-#### `to_mcp_result`
-
-```python
-to_mcp_result(self) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]] | CallToolResult
-```
-
-### `Tool`
-
-
-Internal tool registration info.
-
-
-**Methods:**
-
-#### `to_mcp_tool`
-
-```python
-to_mcp_tool(self, **overrides: Any) -> MCPTool
-```
-
-Convert the FastMCP tool to an MCP tool.
-
-
-#### `from_function`
-
-```python
-from_function(cls, fn: Callable[..., Any]) -> FunctionTool
-```
-
-Create a Tool from a function.
-
-
-#### `run`
-
-```python
-run(self, arguments: dict[str, Any]) -> ToolResult
-```
-
-Run the tool with arguments.
-
-This method is not implemented in the base Tool class and must be
-implemented by subclasses.
-
-`run()` can EITHER return a list of ContentBlocks, or a tuple of
-(list of ContentBlocks, dict of structured output).
-
-
-#### `convert_result`
-
-```python
-convert_result(self, raw_value: Any) -> ToolResult
-```
-
-Convert a raw result to ToolResult.
-
-Handles ToolResult passthrough and converts raw values using the tool's
-attributes (serializer, output_schema) for proper conversion.
-
-
-#### `register_with_docket`
-
-```python
-register_with_docket(self, docket: Docket) -> None
-```
-
-Register this tool with docket for background execution.
-
-
-#### `add_to_docket`
-
-```python
-add_to_docket(self, docket: Docket, arguments: dict[str, Any], **kwargs: Any) -> Execution
-```
-
-Schedule this tool for background execution via docket.
-
-**Args:**
-- `docket`: The Docket instance
-- `arguments`: Tool arguments
-- `fn_key`: Function lookup key in Docket registry (defaults to self.key)
-- `task_key`: Redis storage key for the result
-- `**kwargs`: Additional kwargs passed to docket.add()
-
-
-#### `from_tool`
-
-```python
-from_tool(cls, tool: Tool | Callable[..., Any]) -> TransformedTool
-```
-
-#### `get_span_attributes`
-
-```python
-get_span_attributes(self) -> dict[str, Any]
-```
diff --git a/docs/python-sdk/fastmcp-tools-tool_transform.mdx b/docs/python-sdk/fastmcp-tools-tool_transform.mdx
deleted file mode 100644
index 84a1b66cf..000000000
--- a/docs/python-sdk/fastmcp-tools-tool_transform.mdx
+++ /dev/null
@@ -1,311 +0,0 @@
----
-title: tool_transform
-sidebarTitle: tool_transform
----
-
-# `fastmcp.tools.tool_transform`
-
-## Functions
-
-### `forward`
-
-```python
-forward(**kwargs: Any) -> ToolResult
-```
-
-
-Forward to parent tool with argument transformation applied.
-
-This function can only be called from within a transformed tool's custom
-function. It applies argument transformation (renaming, validation) before
-calling the parent tool.
-
-For example, if the parent tool has args `x` and `y`, but the transformed
-tool has args `a` and `b`, and an `transform_args` was provided that maps `x` to
-`a` and `y` to `b`, then `forward(a=1, b=2)` will call the parent tool with
-`x=1` and `y=2`.
-
-**Args:**
-- `**kwargs`: Arguments to forward to the parent tool (using transformed names).
-
-**Returns:**
-- The ToolResult from the parent tool execution.
-
-**Raises:**
-- `RuntimeError`: If called outside a transformed tool context.
-- `TypeError`: If provided arguments don't match the transformed schema.
-
-
-### `forward_raw`
-
-```python
-forward_raw(**kwargs: Any) -> ToolResult
-```
-
-
-Forward directly to parent tool without transformation.
-
-This function bypasses all argument transformation and validation, calling the parent
-tool directly with the provided arguments. Use this when you need to call the parent
-with its original parameter names and structure.
-
-For example, if the parent tool has args `x` and `y`, then `forward_raw(x=1,
-y=2)` will call the parent tool with `x=1` and `y=2`.
-
-**Args:**
-- `**kwargs`: Arguments to pass directly to the parent tool (using original names).
-
-**Returns:**
-- The ToolResult from the parent tool execution.
-
-**Raises:**
-- `RuntimeError`: If called outside a transformed tool context.
-
-
-### `apply_transformations_to_tools`
-
-```python
-apply_transformations_to_tools(tools: dict[str, Tool], transformations: dict[str, ToolTransformConfig]) -> dict[str, Tool]
-```
-
-
-Apply a list of transformations to a list of tools. Tools that do not have any transformations
-are left unchanged.
-
-Note: tools dict is keyed by prefixed key (e.g., "tool:my_tool"),
-but transformations are keyed by tool name (e.g., "my_tool").
-
-
-## Classes
-
-### `ArgTransform`
-
-
-Configuration for transforming a parent tool's argument.
-
-This class allows fine-grained control over how individual arguments are transformed
-when creating a new tool from an existing one. You can rename arguments, change their
-descriptions, add default values, or hide them from clients while passing constants.
-
-**Attributes:**
-- `name`: New name for the argument. Use None to keep original name, or ... for no change.
-- `description`: New description for the argument. Use None to remove description, or ... for no change.
-- `default`: New default value for the argument. Use ... for no change.
-- `default_factory`: Callable that returns a default value. Cannot be used with default.
-- `type`: New type for the argument. Use ... for no change.
-- `hide`: If True, hide this argument from clients but pass a constant value to parent.
-- `required`: If True, make argument required (remove default). Use ... for no change.
-- `examples`: Examples for the argument. Use ... for no change.
-
-**Examples:**
-
-Rename argument 'old_name' to 'new_name'
-```python
-ArgTransform(name="new_name")
-```
-
-Change description only
-```python
-ArgTransform(description="Updated description")
-```
-
-Add a default value (makes argument optional)
-```python
-ArgTransform(default=42)
-```
-
-Add a default factory (makes argument optional)
-```python
-ArgTransform(default_factory=lambda: time.time())
-```
-
-Change the type
-```python
-ArgTransform(type=str)
-```
-
-Hide the argument entirely from clients
-```python
-ArgTransform(hide=True)
-```
-
-Hide argument but pass a constant value to parent
-```python
-ArgTransform(hide=True, default="constant_value")
-```
-
-Hide argument but pass a factory-generated value to parent
-```python
-ArgTransform(hide=True, default_factory=lambda: uuid.uuid4().hex)
-```
-
-Make an optional parameter required (removes any default)
-```python
-ArgTransform(required=True)
-```
-
-Combine multiple transformations
-```python
-ArgTransform(name="new_name", description="New desc", default=None, type=int)
-```
-
-
-### `ArgTransformConfig`
-
-
-A model for requesting a single argument transform.
-
-
-**Methods:**
-
-#### `to_arg_transform`
-
-```python
-to_arg_transform(self) -> ArgTransform
-```
-
-Convert the argument transform to a FastMCP argument transform.
-
-
-### `TransformedTool`
-
-
-A tool that is transformed from another tool.
-
-This class represents a tool that has been created by transforming another tool.
-It supports argument renaming, schema modification, custom function injection,
-structured output control, and provides context for the forward() and forward_raw() functions.
-
-The transformation can be purely schema-based (argument renaming, dropping, etc.)
-or can include a custom function that uses forward() to call the parent tool
-with transformed arguments. Output schemas and structured outputs are automatically
-inherited from the parent tool but can be overridden or disabled.
-
-**Attributes:**
-- `parent_tool`: The original tool that this tool was transformed from.
-- `fn`: The function to execute when this tool is called (either the forwarding
-function for pure transformations or a custom user function).
-- `forwarding_fn`: Internal function that handles argument transformation and
-validation when forward() is called from custom functions.
-
-
-**Methods:**
-
-#### `run`
-
-```python
-run(self, arguments: dict[str, Any]) -> ToolResult
-```
-
-Run the tool with context set for forward() functions.
-
-This method executes the tool's function while setting up the context
-that allows forward() and forward_raw() to work correctly within custom
-functions.
-
-**Args:**
-- `arguments`: Dictionary of arguments to pass to the tool's function.
-
-**Returns:**
-- ToolResult object containing content and optional structured output.
-
-
-#### `from_tool`
-
-```python
-from_tool(cls, tool: Tool | Callable[..., Any], name: str | None = None, version: str | NotSetT | None = NotSet, title: str | NotSetT | None = NotSet, description: str | NotSetT | None = NotSet, tags: set[str] | None = None, transform_fn: Callable[..., Any] | None = None, transform_args: dict[str, ArgTransform] | None = None, annotations: ToolAnnotations | NotSetT | None = NotSet, output_schema: dict[str, Any] | NotSetT | None = NotSet, serializer: Callable[[Any], str] | NotSetT | None = NotSet, meta: dict[str, Any] | NotSetT | None = NotSet) -> TransformedTool
-```
-
-Create a transformed tool from a parent tool.
-
-**Args:**
-- `tool`: The parent tool to transform.
-- `transform_fn`: Optional custom function. Can use forward() and forward_raw()
-to call the parent tool. Functions with **kwargs receive transformed
-argument names.
-- `name`: New name for the tool. Defaults to parent tool's name.
-- `version`: New version for the tool. Defaults to parent tool's version.
-- `title`: New title for the tool. Defaults to parent tool's title.
-- `transform_args`: Optional transformations for parent tool arguments.
-Only specified arguments are transformed, others pass through unchanged\:
-- Simple rename (str)
-- Complex transformation (rename/description/default/drop) (ArgTransform)
-- Drop the argument (None)
-- `description`: New description. Defaults to parent's description.
-- `tags`: New tags. Defaults to parent's tags.
-- `annotations`: New annotations. Defaults to parent's annotations.
-- `output_schema`: Control output schema for structured outputs\:
-- None (default)\: Inherit from transform_fn if available, then parent tool
-- dict\: Use custom output schema
-- False\: Disable output schema and structured outputs
-- `serializer`: Deprecated. Return ToolResult from your tools for full control over serialization.
-- `meta`: Control meta information\:
-- NotSet (default)\: Inherit from parent tool
-- dict\: Use custom meta information
-- None\: Remove meta information
-
-**Returns:**
-- TransformedTool with the specified transformations.
-
-**Examples:**
-
-# Transform specific arguments only
-```python
-Tool.from_tool(parent, transform_args={"old": "new"}) # Others unchanged
-```
-
-# Custom function with partial transforms
-```python
-async def custom(x: int, y: int) -> str:
- result = await forward(x=x, y=y)
- return f"Custom: {result}"
-
-Tool.from_tool(parent, transform_fn=custom, transform_args={"a": "x", "b": "y"})
-```
-
-# Using **kwargs (gets all args, transformed and untransformed)
-```python
-async def flexible(**kwargs) -> str:
- result = await forward(**kwargs)
- return f"Got: {kwargs}"
-
-Tool.from_tool(parent, transform_fn=flexible, transform_args={"a": "x"})
-```
-
-# Control structured outputs and schemas
-```python
-# Custom output schema
-Tool.from_tool(parent, output_schema={
- "type": "object",
- "properties": {"status": {"type": "string"}}
-})
-
-# Disable structured outputs
-Tool.from_tool(parent, output_schema=None)
-
-# Return ToolResult for full control
-async def custom_output(**kwargs) -> ToolResult:
- result = await forward(**kwargs)
- return ToolResult(
- content=[TextContent(text="Summary")],
- structured_content={"processed": True}
- )
-```
-
-
-### `ToolTransformConfig`
-
-
-Provides a way to transform a tool.
-
-
-**Methods:**
-
-#### `apply`
-
-```python
-apply(self, tool: Tool) -> TransformedTool
-```
-
-Create a TransformedTool from a provided tool and this transformation configuration.
-
diff --git a/docs/python-sdk/fastmcp-types.mdx b/docs/python-sdk/fastmcp-types.mdx
new file mode 100644
index 000000000..1e7df0e90
--- /dev/null
+++ b/docs/python-sdk/fastmcp-types.mdx
@@ -0,0 +1,27 @@
+---
+title: types
+sidebarTitle: types
+---
+
+# `fastmcp.types`
+
+
+Reusable type annotations for FastMCP tool parameters.
+
+These types can be used in tool function signatures to influence how
+parameters are presented in UIs (e.g. `fastmcp dev apps`) and
+serialized in JSON Schema.
+
+Example:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.types import Textarea
+
+mcp = FastMCP("demo")
+
+@mcp.tool()
+def run_query(sql: Textarea) -> str:
+ ...
+```
+
diff --git a/docs/python-sdk/fastmcp-utilities-asgi_transport.mdx b/docs/python-sdk/fastmcp-utilities-asgi_transport.mdx
new file mode 100644
index 000000000..25fd6e2f8
--- /dev/null
+++ b/docs/python-sdk/fastmcp-utilities-asgi_transport.mdx
@@ -0,0 +1,92 @@
+---
+title: asgi_transport
+sidebarTitle: asgi_transport
+---
+
+# `fastmcp.utilities.asgi_transport`
+
+
+An in-process, full-duplex HTTP transport for driving ASGI applications from httpx.
+
+Ported from the MCP Python SDK's test suite (`tests/interaction/transports/_bridge.py`,
+MIT licensed).
+
+`httpx2.ASGITransport` runs the application to completion and only then hands the buffered
+response to the caller, so a server that streams its response — as the streamable HTTP
+transport's SSE responses do — can never converse with the client mid-request: a
+server-initiated request nested inside a still-open call deadlocks.
+`StreamingASGITransport` removes that limitation by running the application as a background
+task and forwarding every `http.response.body` chunk to the client the moment it is sent.
+Everything happens on the one event loop: no sockets, no threads, no sleeps.
+
+The behavioural contract:
+
+- The request body is buffered before the application is invoked (MCP requests are small
+ JSON documents); the response streams chunk by chunk.
+- Closing the response — or the whole client — delivers `http.disconnect` to the
+ application, exactly as a real server sees when its peer goes away.
+- An exception the application raises before sending `http.response.start` fails the
+ originating request with that same exception. After the response has started, a failure
+ is visible to the client only through the response itself (status code, truncated body) —
+ the same signal a real server over a real socket would give.
+
+The transport owns an anyio task group for the application tasks; it is opened and closed by
+`httpx2.AsyncClient`'s own context manager, so the client must be used as a context manager.
+Closing the transport cancels every running application task by default; set
+`cancel_on_close=False` to wait for the application's own disconnect handling instead, which
+is what the legacy SSE transport relies on for resource cleanup.
+
+
+## Functions
+
+### `run_asgi_lifespan`
+
+```python
+run_asgi_lifespan(app: ASGIApp) -> AsyncIterator[None]
+```
+
+
+Run an ASGI application's lifespan, driving the protocol as a real server does.
+
+The application's lifespan runs inside a dedicated task for the whole duration of
+the context. This matters because a lifespan typically owns cancel scopes and task
+groups — anyio requires those to be exited by the task that entered them, which
+rules out entering the lifespan on one task and leaving it on another (as a pytest
+fixture's setup and teardown phases may do).
+
+**Args:**
+- `app`: The ASGI application whose lifespan should run.
+
+**Raises:**
+- `RuntimeError`: If the application reports `lifespan.startup.failed`, or reports
+`lifespan.shutdown.failed` (or crashes during shutdown) while the context
+body itself completed successfully. A failure inside the body takes
+precedence and propagates unchanged.
+
+
+## Classes
+
+### `StreamingASGITransport`
+
+
+Drive an ASGI application in-process, streaming each response as it is produced.
+
+This is an `httpx2` transport, so it plugs into anything that accepts an
+`httpx2.AsyncClient` — including FastMCP's client transports via their
+`httpx_client_factory` argument.
+
+**Args:**
+- `app`: The ASGI application to drive (e.g. `FastMCP.http_app()`).
+- `cancel_on_close`: When True (the default), closing the transport cancels every
+application task still running, so harness teardown can never hang. Set to
+False to wait for the application's own disconnect handling to complete
+instead, which the legacy SSE server transport relies on for cleanup.
+
+
+**Methods:**
+
+#### `handle_async_request`
+
+```python
+handle_async_request(self, request: httpx2.Request) -> httpx2.Response
+```
diff --git a/docs/python-sdk/fastmcp-utilities-async_utils.mdx b/docs/python-sdk/fastmcp-utilities-async_utils.mdx
index cfd0cd7ab..4d6a996e9 100644
--- a/docs/python-sdk/fastmcp-utilities-async_utils.mdx
+++ b/docs/python-sdk/fastmcp-utilities-async_utils.mdx
@@ -10,7 +10,21 @@ Async utilities for FastMCP.
## Functions
-### `call_sync_fn_in_threadpool`
+### `is_coroutine_function`
+
+```python
+is_coroutine_function(fn: Any) -> bool
+```
+
+
+Check if a callable is a coroutine function, unwrapping functools.partial.
+
+``inspect.iscoroutinefunction`` returns ``False`` for
+``functools.partial`` objects wrapping an async function on Python < 3.12.
+This helper unwraps any layers of ``partial`` before checking.
+
+
+### `call_sync_fn_in_threadpool`
```python
call_sync_fn_in_threadpool(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> Any
@@ -23,10 +37,10 @@ Uses anyio.to_thread.run_sync which properly propagates contextvars,
making this safe for functions that depend on context (like dependency injection).
-### `gather`
+### `gather`
```python
-gather(*awaitables: Awaitable[T]) -> list[T] | list[T | BaseException]
+gather(awaitables: Iterable[Awaitable[T]]) -> list[T] | list[T | BaseException]
```
@@ -34,8 +48,25 @@ Run awaitables concurrently and return results in order.
Uses anyio TaskGroup for structured concurrency.
+``awaitables`` is consumed lazily, one item at a time, right before each
+is handed to the task group. Callers with a dynamic number of awaitables
+should pass a generator expression (e.g. ``gather(f(x) for x in xs)``)
+rather than a list or list comprehension: a list comprehension calls
+every ``f(x)`` up front, creating a batch of coroutine objects before
+this function even starts, whereas a generator expression creates each
+coroutine only as this function's own scheduling loop asks for it. That
+matters because coroutine creation and scheduling can be interrupted
+between any two bytecode instructions by a synchronous signal handler
+(for example pytest-timeout's SIGALRM-based per-test timeout). If that
+happens while a whole batch of coroutines is sitting unscheduled, they
+are silently abandoned and eventually trigger a "coroutine was never
+awaited" warning attributed to whatever unrelated code happens to be
+running when the garbage collector gets to them. Lazy consumption keeps
+the window in which a created-but-unscheduled coroutine can exist as
+small as possible.
+
**Args:**
-- `*awaitables`: Awaitables to run concurrently
+- `awaitables`: Iterable of awaitables to run concurrently.
- `return_exceptions`: If True, exceptions are returned in results.
If False, first exception cancels all and raises.
diff --git a/docs/python-sdk/fastmcp-utilities-auth.mdx b/docs/python-sdk/fastmcp-utilities-auth.mdx
index c2d23b9a5..fbd14b943 100644
--- a/docs/python-sdk/fastmcp-utilities-auth.mdx
+++ b/docs/python-sdk/fastmcp-utilities-auth.mdx
@@ -10,7 +10,7 @@ Authentication utility helpers.
## Functions
-### `decode_jwt_header`
+### `decode_jwt_header`
```python
decode_jwt_header(token: str) -> dict[str, Any]
@@ -31,7 +31,7 @@ Useful for extracting the key ID (kid) for JWKS lookup.
- `ValueError`: If token is not a valid JWT format
-### `decode_jwt_payload`
+### `decode_jwt_payload`
```python
decode_jwt_payload(token: str) -> dict[str, Any]
@@ -52,7 +52,7 @@ Use only for tokens received directly from trusted sources (e.g., IdP token endp
- `ValueError`: If token is not a valid JWT format
-### `parse_scopes`
+### `parse_scopes`
```python
parse_scopes(value: Any) -> list[str] | None
diff --git a/docs/python-sdk/fastmcp-utilities-authorization.mdx b/docs/python-sdk/fastmcp-utilities-authorization.mdx
new file mode 100644
index 000000000..0f64a0a4a
--- /dev/null
+++ b/docs/python-sdk/fastmcp-utilities-authorization.mdx
@@ -0,0 +1,163 @@
+---
+title: authorization
+sidebarTitle: authorization
+---
+
+# `fastmcp.utilities.authorization`
+
+
+Authorization checks for FastMCP components.
+
+Auth checks are callables that receive an ``AuthContext`` and return True to
+allow access or False to deny it. They can also raise ``AuthorizationError`` to
+deny with a custom message; other exceptions are masked and treated as denial.
+
+
+## Functions
+
+### `require_scopes`
+
+```python
+require_scopes(*scopes: str) -> AuthCheck
+```
+
+
+Require all of the given OAuth scopes.
+
+
+### `require_roles`
+
+```python
+require_roles(*roles: str) -> AuthCheck
+```
+
+
+Require all of the given roles, read from the token's claims.
+
+Roles and groups are not part of OIDC, so every identity provider puts them
+somewhere different: `realm_access.roles` on Keycloak, `roles` on Microsoft
+Entra, `cognito:groups` on AWS Cognito, `permissions` or a namespaced custom
+claim on Auth0. `extract` receives the token's claims and returns the
+caller's roles, which keeps that provider-specific knowledge at the call
+site instead of guessing it here.
+
+```python
+from fastmcp.server.auth import require_roles
+
+keycloak = require_roles("admin", extract=lambda c: c["realm_access"]["roles"])
+cognito = require_roles("admins", extract=lambda c: c["cognito:groups"])
+```
+
+A token missing the claim entirely is denied rather than treated as an
+error, so `extract` may index into the claims without guarding. An
+extractor returning a bare string is treated as one role, since a provider
+that stores a single role as a scalar is common.
+
+Unlike `require_scopes`, this check cannot signal a shortfall: OAuth has no
+way to request a role, so there is no `insufficient_scope` challenge to
+emit. A role denial is therefore reported as a plain `AuthorizationError`,
+and it suppresses any scope shortfall alongside it — a caller blocked by
+their role must not be told to go obtain a scope that would not help.
+Scope shortfalls are still reported normally whenever the role check
+passes.
+
+**Args:**
+- `*roles`: Roles the caller must hold. All are required (AND logic).
+- `extract`: Callable mapping the token's claims to the caller's roles.
+
+**Raises:**
+- `ValueError`: If no roles are given, which would allow any authenticated
+caller and is more likely a mistake than an intent.
+
+
+### `restrict_tag`
+
+```python
+restrict_tag(tag: str) -> AuthCheck
+```
+
+
+Require scopes when the accessed component has a specific tag.
+
+
+### `scope_requirements`
+
+```python
+scope_requirements(checks: AuthCheck | list[AuthCheck], ctx: AuthContext) -> list[str] | None
+```
+
+
+Scopes a check list requires but the token lacks, without running it.
+
+Returns ``None`` when the list contains any opaque (non-scope) check. Such a
+check might deny for a reason unrelated to scopes, and evaluating it here
+would run authorization logic — with whatever side effects it carries —
+outside its normal place in the chain. Since its verdict is unknown, its
+siblings' scopes must not be disclosed either, so the whole list is withheld.
+
+When every check is scope-aware, the result is their combined shortfall,
+computed purely from the token and component (an empty list means the list is
+already satisfied). This lets a shortfall be aggregated across authorization
+layers without evaluating anything that would otherwise be skipped.
+
+
+### `run_auth_checks_with_shortfall`
+
+```python
+run_auth_checks_with_shortfall(checks: AuthCheck | list[AuthCheck], ctx: AuthContext) -> tuple[bool, list[str]]
+```
+
+
+Run auth checks with AND logic, classifying the denial cause.
+
+Returns ``(authorized, missing_scopes)``. ``missing_scopes`` names every
+scope the caller must obtain to satisfy *all* scope requirements at once:
+the union of the shortfalls across every scope-aware check, not just the
+first one to fail. Reporting only the first would strand a caller in a
+step-up loop — it obtains that scope, retries, and is denied again for the
+next — so the union is what makes a single re-authorization converge.
+
+The challenge is withheld entirely (an empty list, which the caller surfaces
+as a plain ``AuthorizationError``) unless every non-scope check passes. A
+custom policy denial — a tenant check, say — must never be reported as an
+``insufficient_scope`` shortfall, and must never name the scopes of a
+component the caller could not otherwise reach. To guarantee that, the
+opaque checks are all evaluated before any scope is disclosed; a shortfall
+is only reported once they have all passed.
+
+An ``AuthorizationError`` raised by a check propagates unchanged.
+
+
+### `run_auth_checks`
+
+```python
+run_auth_checks(checks: AuthCheck | list[AuthCheck], ctx: AuthContext) -> bool
+```
+
+
+Run auth checks with AND logic, stopping at the first failure.
+
+
+## Classes
+
+### `AuthContext`
+
+
+Context passed to auth check callables.
+
+**Attributes:**
+- `token`: The current access token, or None if unauthenticated.
+- `component`: The tool, resource, resource template, or prompt being accessed.
+- `tool`: Backwards-compatible alias for component when it is a Tool.
+
+
+**Methods:**
+
+#### `tool`
+
+```python
+tool(self) -> Tool | None
+```
+
+Backwards-compatible access to the component as a Tool.
+
diff --git a/docs/python-sdk/fastmcp-utilities-cli.mdx b/docs/python-sdk/fastmcp-utilities-cli.mdx
index 52f2addff..6b5e73295 100644
--- a/docs/python-sdk/fastmcp-utilities-cli.mdx
+++ b/docs/python-sdk/fastmcp-utilities-cli.mdx
@@ -7,7 +7,7 @@ sidebarTitle: cli
## Functions
-### `is_already_in_uv_subprocess`
+### `is_already_in_uv_subprocess`
```python
is_already_in_uv_subprocess() -> bool
@@ -17,7 +17,7 @@ is_already_in_uv_subprocess() -> bool
Check if we're already running in a FastMCP uv subprocess.
-### `load_and_merge_config`
+### `load_and_merge_config`
```python
load_and_merge_config(server_spec: str | None, **cli_overrides) -> tuple[MCPServerConfig, str]
@@ -37,7 +37,7 @@ run, inspect, and dev commands.
- Tuple of (MCPServerConfig, resolved_server_spec)
-### `log_server_banner`
+### `log_server_banner`
```python
log_server_banner(server: FastMCP[Any]) -> None
diff --git a/docs/python-sdk/fastmcp-utilities-components.mdx b/docs/python-sdk/fastmcp-utilities-components.mdx
index eba2517c2..eb71e0a88 100644
--- a/docs/python-sdk/fastmcp-utilities-components.mdx
+++ b/docs/python-sdk/fastmcp-utilities-components.mdx
@@ -7,7 +7,7 @@ sidebarTitle: components
## Functions
-### `get_fastmcp_metadata`
+### `get_fastmcp_metadata`
```python
get_fastmcp_metadata(meta: dict[str, Any] | None) -> FastMCPMeta
@@ -22,9 +22,9 @@ namespace for compatibility with older FastMCP servers.
## Classes
-### `FastMCPMeta`
+### `FastMCPMeta`
-### `FastMCPComponent`
+### `FastMCPComponent`
Base class for FastMCP tools, prompts, resources, and resource templates.
@@ -32,7 +32,7 @@ Base class for FastMCP tools, prompts, resources, and resource templates.
**Methods:**
-#### `make_key`
+#### `make_key`
```python
make_key(cls, identifier: str) -> str
@@ -47,7 +47,7 @@ Construct the lookup key for this component type.
- A prefixed key like "tool:name" or "resource:uri"
-#### `key`
+#### `key`
```python
key(self) -> str
@@ -64,8 +64,15 @@ The @ suffix is ALWAYS present to enable unambiguous parsing of keys
Subclasses should override this to use their specific identifier.
Base implementation uses name.
+Prefer `.key` over ad-hoc `name or uri or uri_template` logic for any
+cross-component identity work (dedupe, grouping, collision detection,
+lookup tables). It encodes type, identifier, and version, so variants
+of the same component don't falsely collide with each other, and
+cross-type identifiers (e.g. a tool and a resource both named "foo")
+can't clash.
-#### `get_meta`
+
+#### `get_meta`
```python
get_meta(self) -> dict[str, Any]
@@ -80,7 +87,7 @@ Returns a dict that always includes a `fastmcp` key containing:
Internal keys (prefixed with `_`) are stripped from the fastmcp namespace.
-#### `enable`
+#### `enable`
```python
enable(self) -> None
@@ -89,7 +96,7 @@ enable(self) -> None
Removed in 3.0. Use server.enable(keys=[...]) instead.
-#### `disable`
+#### `disable`
```python
disable(self) -> None
@@ -98,7 +105,7 @@ disable(self) -> None
Removed in 3.0. Use server.disable(keys=[...]) instead.
-#### `copy`
+#### `copy`
```python
copy(self) -> Self
@@ -107,36 +114,7 @@ copy(self) -> Self
Create a copy of the component.
-#### `register_with_docket`
-
-```python
-register_with_docket(self, docket: Docket) -> None
-```
-
-Register this component with docket for background execution.
-
-No-ops if task_config.mode is "forbidden". Subclasses override to
-register their callable (self.run, self.read, self.render, or self.fn).
-
-
-#### `add_to_docket`
-
-```python
-add_to_docket(self, docket: Docket, *args: Any, **kwargs: Any) -> Execution
-```
-
-Schedule this component for background execution via docket.
-
-Subclasses override this to handle their specific calling conventions:
-- Tool: add_to_docket(docket, arguments: dict, **kwargs)
-- Resource: add_to_docket(docket, **kwargs)
-- ResourceTemplate: add_to_docket(docket, params: dict, **kwargs)
-- Prompt: add_to_docket(docket, arguments: dict | None, **kwargs)
-
-The **kwargs are passed through to docket.add() (e.g., key=task_key).
-
-
-#### `get_span_attributes`
+#### `get_span_attributes`
```python
get_span_attributes(self) -> dict[str, Any]
diff --git a/docs/python-sdk/fastmcp-utilities-docstring_parsing.mdx b/docs/python-sdk/fastmcp-utilities-docstring_parsing.mdx
new file mode 100644
index 000000000..bceb0256e
--- /dev/null
+++ b/docs/python-sdk/fastmcp-utilities-docstring_parsing.mdx
@@ -0,0 +1,39 @@
+---
+title: docstring_parsing
+sidebarTitle: docstring_parsing
+---
+
+# `fastmcp.utilities.docstring_parsing`
+
+
+Extract descriptions from function docstrings.
+
+Uses griffelib to parse Google, NumPy, and Sphinx-style docstrings. The
+interface is intentionally narrow — a single function returning a
+`ParsedDocstring` — so the implementation can be swapped without touching
+callers.
+
+
+## Functions
+
+### `parse_docstring`
+
+```python
+parse_docstring(fn: Callable[..., Any]) -> ParsedDocstring
+```
+
+
+Parse a function's docstring into a summary and parameter descriptions.
+
+Tries Google, NumPy, and Sphinx parsers in order, using the first one that
+successfully extracts parameter descriptions. If none do, returns the full
+docstring as the description with no parameter descriptions.
+
+
+## Classes
+
+### `ParsedDocstring`
+
+
+The extracted description and per-parameter descriptions from a docstring.
+
diff --git a/docs/python-sdk/fastmcp-utilities-exceptions.mdx b/docs/python-sdk/fastmcp-utilities-exceptions.mdx
index 5794f185e..129ad5a67 100644
--- a/docs/python-sdk/fastmcp-utilities-exceptions.mdx
+++ b/docs/python-sdk/fastmcp-utilities-exceptions.mdx
@@ -7,13 +7,53 @@ sidebarTitle: exceptions
## Functions
-### `iter_exc`
+### `is_http_status_error`
+
+```python
+is_http_status_error(exc: BaseException) -> bool
+```
+
+
+Return whether an exception is an httpx2 or legacy-httpx status error.
+
+
+### `get_http_status_code`
+
+```python
+get_http_status_code(exc: BaseException) -> int | None
+```
+
+
+Return the response status code from a recognized HTTP status error.
+
+
+### `is_timeout_error`
+
+```python
+is_timeout_error(exc: BaseException) -> bool
+```
+
+
+Return whether an exception is an httpx2 or legacy-httpx timeout.
+
+
+### `is_request_error`
+
+```python
+is_request_error(exc: BaseException) -> bool
+```
+
+
+Return whether an exception is an httpx2 or legacy-httpx request error.
+
+
+### `iter_exc`
```python
iter_exc(group: BaseExceptionGroup)
```
-### `get_catch_handlers`
+### `get_catch_handlers`
```python
get_catch_handlers() -> Mapping[type[BaseException] | Iterable[type[BaseException]], Callable[[BaseExceptionGroup[Any]], Any]]
diff --git a/docs/python-sdk/fastmcp-utilities-http.mdx b/docs/python-sdk/fastmcp-utilities-http.mdx
index d274477c5..644128612 100644
--- a/docs/python-sdk/fastmcp-utilities-http.mdx
+++ b/docs/python-sdk/fastmcp-utilities-http.mdx
@@ -7,10 +7,10 @@ sidebarTitle: http
## Functions
-### `find_available_port`
+### `find_available_port`
```python
-find_available_port() -> int
+find_available_port(host: str = '127.0.0.1') -> int
```
diff --git a/docs/python-sdk/fastmcp-utilities-inspect.mdx b/docs/python-sdk/fastmcp-utilities-inspect.mdx
index a813eb51e..d2aca51d1 100644
--- a/docs/python-sdk/fastmcp-utilities-inspect.mdx
+++ b/docs/python-sdk/fastmcp-utilities-inspect.mdx
@@ -10,7 +10,7 @@ Utilities for inspecting FastMCP instances.
## Functions
-### `inspect_fastmcp_v2`
+### `inspect_fastmcp_v2`
```python
inspect_fastmcp_v2(mcp: FastMCP[Any]) -> FastMCPInfo
@@ -26,10 +26,10 @@ Extract information from a FastMCP v2.x instance.
- FastMCPInfo dataclass containing the extracted information
-### `inspect_fastmcp_v1`
+### `inspect_fastmcp_v1`
```python
-inspect_fastmcp_v1(mcp: FastMCP1x) -> FastMCPInfo
+inspect_fastmcp_v1(mcp: SDKServer) -> FastMCPInfo
```
@@ -42,10 +42,10 @@ Extract information from a FastMCP v1.x instance using a Client.
- FastMCPInfo dataclass containing the extracted information
-### `inspect_fastmcp`
+### `inspect_fastmcp`
```python
-inspect_fastmcp(mcp: FastMCP[Any] | FastMCP1x) -> FastMCPInfo
+inspect_fastmcp(mcp: FastMCP[Any] | SDKServer) -> FastMCPInfo
```
@@ -61,7 +61,7 @@ and uses the appropriate extraction method.
- FastMCPInfo dataclass containing the extracted information
-### `format_fastmcp_info`
+### `format_fastmcp_info`
```python
format_fastmcp_info(info: FastMCPInfo) -> bytes
@@ -73,10 +73,10 @@ Format FastMCPInfo as FastMCP-specific JSON.
This includes FastMCP-specific fields like tags, enabled, annotations, etc.
-### `format_mcp_info`
+### `format_mcp_info`
```python
-format_mcp_info(mcp: FastMCP[Any] | FastMCP1x) -> bytes
+format_mcp_info(mcp: FastMCP[Any] | SDKServer) -> bytes
```
@@ -86,10 +86,10 @@ Uses Client to get the standard MCP protocol format with camelCase fields.
Includes version metadata at the top level.
-### `format_info`
+### `format_info`
```python
-format_info(mcp: FastMCP[Any] | FastMCP1x, format: InspectFormat | Literal['fastmcp', 'mcp'], info: FastMCPInfo | None = None) -> bytes
+format_info(mcp: FastMCP[Any] | SDKServer, format: InspectFormat | Literal['fastmcp', 'mcp'], info: FastMCPInfo | None = None) -> bytes
```
@@ -106,37 +106,37 @@ Format server information according to the specified format.
## Classes
-### `ToolInfo`
+### `ToolInfo`
Information about a tool.
-### `PromptInfo`
+### `PromptInfo`
Information about a prompt.
-### `ResourceInfo`
+### `ResourceInfo`
Information about a resource.
-### `TemplateInfo`
+### `TemplateInfo`
Information about a resource template.
-### `FastMCPInfo`
+### `FastMCPInfo`
Information extracted from a FastMCP instance.
-### `InspectFormat`
+### `InspectFormat`
Output format for inspect command.
diff --git a/docs/python-sdk/fastmcp-utilities-json_schema.mdx b/docs/python-sdk/fastmcp-utilities-json_schema.mdx
index e08643cb9..654108a63 100644
--- a/docs/python-sdk/fastmcp-utilities-json_schema.mdx
+++ b/docs/python-sdk/fastmcp-utilities-json_schema.mdx
@@ -7,7 +7,34 @@ sidebarTitle: json_schema
## Functions
-### `dereference_refs`
+### `replace_refs`
+
+```python
+replace_refs(*args: Any, **kwargs: Any) -> Any
+```
+
+
+Call jsonref lazily while preserving the module's patchable boundary.
+
+
+### `require_discriminator_property`
+
+```python
+require_discriminator_property(schema: dict[str, Any]) -> dict[str, Any]
+```
+
+
+Keep an OpenAPI discriminator's tag mandatory after the keyword is dropped.
+
+Returns a copy of *schema* with ``discriminator.propertyName`` added to each
+``anyOf``/``oneOf`` variant's ``required`` list. A Pydantic discriminated
+union whose tag has a default omits that tag from ``required``; without this,
+an untagged payload passes the generated schema but fails later in the source
+model with ``union_tag_not_found``. No-op if there is no string
+``propertyName``.
+
+
+### `dereference_refs`
```python
dereference_refs(schema: dict[str, Any]) -> dict[str, Any]
@@ -27,6 +54,11 @@ For self-referencing/circular schemas where full dereferencing is not possible,
this function falls back to resolving only the root-level $ref while preserving
$defs for nested references.
+Only local ``$ref`` values (those starting with ``#``) are resolved.
+Remote URIs (``http://``, ``file://``, etc.) are stripped before
+resolution to prevent SSRF / local-file-inclusion attacks when proxying
+schemas from untrusted servers.
+
**Args:**
- `schema`: JSON schema dict that may contain $ref references
@@ -35,7 +67,7 @@ $defs for nested references.
- when no longer needed
-### `resolve_root_ref`
+### `resolve_root_ref`
```python
resolve_root_ref(schema: dict[str, Any]) -> dict[str, Any]
@@ -57,7 +89,7 @@ the referenced definition while preserving $defs for nested references.
- if no resolution is needed
-### `compress_schema`
+### `compress_schema`
```python
compress_schema(schema: dict[str, Any], prune_params: list[str] | None = None, prune_additional_properties: bool = False, prune_titles: bool = False, dereference: bool = False) -> dict[str, Any]
diff --git a/docs/python-sdk/fastmcp-utilities-json_schema_type.mdx b/docs/python-sdk/fastmcp-utilities-json_schema_type.mdx
index b1137e7de..cd4634901 100644
--- a/docs/python-sdk/fastmcp-utilities-json_schema_type.mdx
+++ b/docs/python-sdk/fastmcp-utilities-json_schema_type.mdx
@@ -22,6 +22,24 @@ for validation with Pydantic. It supports:
- Enums and constants
- Union types
+## Unsupported regex patterns
+
+Pydantic uses a Rust-based regex engine that does not support all regex
+features found in real-world JSON Schemas (particularly those from AWS,
+Azure, and other large OpenAPI providers). Unsupported constructs include
+lookahead/lookbehind assertions (`(?!...)`, `(?<=...)`), Unicode property
+escapes (`\p{Graph}`, `\p{Print}`), and very large compiled patterns.
+
+When a `pattern` constraint cannot be compiled, `json_schema_to_type`
+degrades gracefully:
+
+1. The pattern is **dropped** from the Pydantic `StringConstraints` so
+ the type will not raise a `SchemaError`.
+2. A `UserWarning` is emitted with the unsupported pattern.
+3. The original pattern is preserved in the type metadata as
+ `x-unsupported-pattern` (visible via `TypeAdapter(T).json_schema()`).
+4. Other constraints (`minLength`, `maxLength`) are still enforced.
+
Example:
```python
schema = {
@@ -42,17 +60,18 @@ Example:
## Functions
-### `json_schema_to_type`
+### `json_schema_to_type`
```python
-json_schema_to_type(schema: Mapping[str, Any], name: str | None = None) -> type
+json_schema_to_type(schema: Mapping[str, Any] | bool, name: str | None = None) -> type
```
Convert JSON schema to appropriate Python type with validation.
**Args:**
-- `schema`: A JSON Schema dictionary defining the type structure and validation rules
+- `schema`: A JSON Schema dictionary defining the type structure and validation rules.
+Boolean schemas are also accepted (``True`` = any type, ``False`` = unsatisfiable).
- `name`: Optional name for object schemas. Only allowed when schema type is "object".
If not provided for objects, name will be inferred from schema's "title"
property or default to "Root".
@@ -107,4 +126,4 @@ class Name:
## Classes
-### `JSONSchema`
+### `JSONSchema`
diff --git a/docs/python-sdk/fastmcp-utilities-lifespan.mdx b/docs/python-sdk/fastmcp-utilities-lifespan.mdx
index 912cbfad6..f4dcee33a 100644
--- a/docs/python-sdk/fastmcp-utilities-lifespan.mdx
+++ b/docs/python-sdk/fastmcp-utilities-lifespan.mdx
@@ -10,7 +10,7 @@ Lifespan utilities for combining async context manager lifespans.
## Functions
-### `combine_lifespans`
+### `combine_lifespans`
```python
combine_lifespans(*lifespans: Callable[[AppT], AbstractAsyncContextManager[Mapping[str, Any] | None]]) -> Callable[[AppT], AbstractAsyncContextManager[dict[str, Any]]]
diff --git a/docs/python-sdk/fastmcp-utilities-logging.mdx b/docs/python-sdk/fastmcp-utilities-logging.mdx
index 2b5fea4ae..4dde3d509 100644
--- a/docs/python-sdk/fastmcp-utilities-logging.mdx
+++ b/docs/python-sdk/fastmcp-utilities-logging.mdx
@@ -10,7 +10,7 @@ Logging utilities for FastMCP.
## Functions
-### `get_logger`
+### `get_logger`
```python
get_logger(name: str) -> logging.Logger
@@ -26,7 +26,7 @@ Get a logger nested under FastMCP namespace.
- a configured logger instance
-### `configure_logging`
+### `configure_logging`
```python
configure_logging(level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] | int = 'INFO', logger: logging.Logger | None = None, enable_rich_tracebacks: bool | None = None, **rich_kwargs: Any) -> None
@@ -41,7 +41,7 @@ Configure logging for FastMCP.
- `rich_kwargs`: the parameters to use for creating RichHandler
-### `temporary_log_level`
+### `temporary_log_level`
```python
temporary_log_level(level: str | None, logger: logging.Logger | None = None, enable_rich_tracebacks: bool | None = None, **rich_kwargs: Any)
diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-__init__.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-__init__.mdx
deleted file mode 100644
index d29664280..000000000
--- a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.utilities.mcp_server_config.v1`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx
index f59bb18b1..f8e764a22 100644
--- a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx
+++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base.mdx
@@ -7,7 +7,7 @@ sidebarTitle: base
## Classes
-### `Environment`
+### `Environment`
Base class for environment configuration.
@@ -15,7 +15,7 @@ Base class for environment configuration.
**Methods:**
-#### `build_command`
+#### `build_command`
```python
build_command(self, command: list[str]) -> list[str]
@@ -30,7 +30,7 @@ Build the full command with environment setup.
- Full command ready for subprocess execution
-#### `prepare`
+#### `prepare`
```python
prepare(self, output_dir: Path | None = None) -> None
diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv.mdx
index e5a2d6a11..8e9e5b2db 100644
--- a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv.mdx
+++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv.mdx
@@ -7,7 +7,7 @@ sidebarTitle: uv
## Classes
-### `UVEnvironment`
+### `UVEnvironment`
Configuration for Python environment setup.
@@ -15,7 +15,7 @@ Configuration for Python environment setup.
**Methods:**
-#### `build_command`
+#### `build_command`
```python
build_command(self, command: list[str]) -> list[str]
@@ -31,7 +31,7 @@ Build complete uv run command with environment args and command to execute.
- If no environment configuration is set, returns the command unchanged.
-#### `prepare`
+#### `prepare`
```python
prepare(self, output_dir: Path | None = None) -> None
diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx
index 460f1f83d..dc0dd1276 100644
--- a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx
+++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config.mdx
@@ -15,7 +15,7 @@ command-line arguments.
## Functions
-### `generate_schema`
+### `generate_schema`
```python
generate_schema(output_path: Path | str | None = None) -> dict[str, Any] | None
@@ -38,7 +38,7 @@ validation and auto-completion.
## Classes
-### `Deployment`
+### `Deployment`
Configuration for server deployment and runtime settings.
@@ -46,7 +46,7 @@ Configuration for server deployment and runtime settings.
**Methods:**
-#### `apply_runtime_settings`
+#### `apply_runtime_settings`
```python
apply_runtime_settings(self, config_path: Path | None = None) -> None
@@ -62,7 +62,7 @@ For example: "API_URL": "https://api.${ENVIRONMENT}.example.com"
will substitute the value of the ENVIRONMENT variable at runtime.
-### `MCPServerConfig`
+### `MCPServerConfig`
Configuration for a FastMCP server.
@@ -73,7 +73,7 @@ a FastMCP server in a declarative format.
**Methods:**
-#### `validate_source`
+#### `validate_source`
```python
validate_source(cls, v: dict | Source) -> SourceType
@@ -89,7 +89,7 @@ No string parsing happens here - that's only at CLI boundaries.
MCPServerConfig works only with properly typed objects.
-#### `validate_environment`
+#### `validate_environment`
```python
validate_environment(cls, v: dict | Any) -> EnvironmentType
@@ -100,7 +100,7 @@ Ensure environment has a type field for discrimination.
For backward compatibility, if no type is specified, default to "uv".
-#### `validate_deployment`
+#### `validate_deployment`
```python
validate_deployment(cls, v: dict | Deployment) -> Deployment
@@ -113,7 +113,7 @@ Accepts:
- dict that can be converted to Deployment
-#### `from_file`
+#### `from_file`
```python
from_file(cls, file_path: Path) -> MCPServerConfig
@@ -133,7 +133,7 @@ Load configuration from a JSON file.
- `pydantic.ValidationError`: If the configuration is invalid
-#### `from_cli_args`
+#### `from_cli_args`
```python
from_cli_args(cls, source: FileSystemSource, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: Literal['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] | None = None, python: str | None = None, dependencies: list[str] | None = None, requirements: str | None = None, project: str | None = None, editable: str | None = None, env: dict[str, str] | None = None, cwd: str | None = None, args: list[str] | None = None) -> MCPServerConfig
@@ -164,7 +164,7 @@ goes through a config object.
- MCPServerConfig instance
-#### `find_config`
+#### `find_config`
```python
find_config(cls, start_path: Path | None = None) -> Path | None
@@ -179,7 +179,7 @@ Find a fastmcp.json file in the specified directory.
- Path to the configuration file, or None if not found
-#### `prepare`
+#### `prepare`
```python
prepare(self, skip_source: bool = False, output_dir: Path | None = None) -> None
@@ -195,7 +195,7 @@ When output_dir is None, does ephemeral caching (for backwards compatibility).
- `output_dir`: Directory to create the persistent uv project in (optional)
-#### `prepare_environment`
+#### `prepare_environment`
```python
prepare_environment(self, output_dir: Path | None = None) -> None
@@ -210,7 +210,7 @@ Prepare the Python environment.
Delegates to the environment's prepare() method
-#### `prepare_source`
+#### `prepare_source`
```python
prepare_source(self) -> None
@@ -221,7 +221,7 @@ Prepare the source for loading.
Delegates to the source's prepare() method.
-#### `run_server`
+#### `run_server`
```python
run_server(self, **kwargs: Any) -> None
diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-__init__.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-__init__.mdx
deleted file mode 100644
index 38d102b15..000000000
--- a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-__init__.mdx
+++ /dev/null
@@ -1,8 +0,0 @@
----
-title: __init__
-sidebarTitle: __init__
----
-
-# `fastmcp.utilities.mcp_server_config.v1.sources`
-
-*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx
index 764454b6d..4c5793007 100644
--- a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx
+++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base.mdx
@@ -7,7 +7,7 @@ sidebarTitle: base
## Classes
-### `Source`
+### `Source`
Abstract base class for all source types.
@@ -15,7 +15,7 @@ Abstract base class for all source types.
**Methods:**
-#### `prepare`
+#### `prepare`
```python
prepare(self) -> None
@@ -28,7 +28,7 @@ this method performs that preparation. For sources that don't
need preparation (e.g., local files), this is a no-op.
-#### `load_server`
+#### `load_server`
```python
load_server(self) -> Any
diff --git a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem.mdx
index 3d791f9cd..2abce7f16 100644
--- a/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem.mdx
+++ b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem.mdx
@@ -7,7 +7,7 @@ sidebarTitle: filesystem
## Classes
-### `FileSystemSource`
+### `FileSystemSource`
Source for local Python files.
@@ -15,7 +15,7 @@ Source for local Python files.
**Methods:**
-#### `parse_path_with_object`
+#### `parse_path_with_object`
```python
parse_path_with_object(cls, v: str) -> str
@@ -27,7 +27,7 @@ This validator runs before the model is created, allowing us to
handle the "file.py:object" syntax at the model boundary.
-#### `load_server`
+#### `load_server`
```python
load_server(self) -> Any
diff --git a/docs/python-sdk/fastmcp-utilities-mime.mdx b/docs/python-sdk/fastmcp-utilities-mime.mdx
new file mode 100644
index 000000000..99455c74a
--- /dev/null
+++ b/docs/python-sdk/fastmcp-utilities-mime.mdx
@@ -0,0 +1,35 @@
+---
+title: mime
+sidebarTitle: mime
+---
+
+# `fastmcp.utilities.mime`
+
+
+MIME type constants and helpers for MCP Apps UI resources.
+
+This module has no dependencies on the server or resource packages,
+so it can be safely imported from anywhere.
+
+
+## Functions
+
+### `resolve_ui_mime_type`
+
+```python
+resolve_ui_mime_type(uri: str, explicit_mime_type: str | None) -> str | None
+```
+
+
+Return the appropriate MIME type for a resource URI.
+
+For ``ui://`` scheme resources, defaults to ``UI_MIME_TYPE`` when no
+explicit MIME type is provided.
+
+**Args:**
+- `uri`: The resource URI string
+- `explicit_mime_type`: The MIME type explicitly provided by the user
+
+**Returns:**
+- The resolved MIME type (explicit value, UI default, or None)
+
diff --git a/docs/python-sdk/fastmcp-utilities-openapi-director.mdx b/docs/python-sdk/fastmcp-utilities-openapi-director.mdx
deleted file mode 100644
index 0d61900c6..000000000
--- a/docs/python-sdk/fastmcp-utilities-openapi-director.mdx
+++ /dev/null
@@ -1,36 +0,0 @@
----
-title: director
-sidebarTitle: director
----
-
-# `fastmcp.utilities.openapi.director`
-
-
-Request director using openapi-core for stateless HTTP request building.
-
-## Classes
-
-### `RequestDirector`
-
-
-Builds httpx.Request objects from HTTPRoute and arguments using openapi-core.
-
-
-**Methods:**
-
-#### `build`
-
-```python
-build(self, route: HTTPRoute, flat_args: dict[str, Any], base_url: str = 'http://localhost') -> httpx.Request
-```
-
-Constructs a final httpx.Request object, handling all OpenAPI serialization.
-
-**Args:**
-- `route`: HTTPRoute containing OpenAPI operation details
-- `flat_args`: Flattened arguments from LLM (may include suffixed parameters)
-- `base_url`: Base URL for the request
-
-**Returns:**
-- httpx.Request: Properly formatted HTTP request
-
diff --git a/docs/python-sdk/fastmcp-utilities-openapi-formatters.mdx b/docs/python-sdk/fastmcp-utilities-openapi-formatters.mdx
deleted file mode 100644
index 4d5ad3a90..000000000
--- a/docs/python-sdk/fastmcp-utilities-openapi-formatters.mdx
+++ /dev/null
@@ -1,96 +0,0 @@
----
-title: formatters
-sidebarTitle: formatters
----
-
-# `fastmcp.utilities.openapi.formatters`
-
-
-Parameter formatting functions for OpenAPI operations.
-
-## Functions
-
-### `format_array_parameter`
-
-```python
-format_array_parameter(values: list, parameter_name: str, is_query_parameter: bool = False) -> str | list
-```
-
-
-Format an array parameter according to OpenAPI specifications.
-
-**Args:**
-- `values`: List of values to format
-- `parameter_name`: Name of the parameter (for error messages)
-- `is_query_parameter`: If True, can return list for explode=True behavior
-
-**Returns:**
-- String (comma-separated) or list (for query params with explode=True)
-
-
-### `format_deep_object_parameter`
-
-```python
-format_deep_object_parameter(param_value: dict, parameter_name: str) -> dict[str, str]
-```
-
-
-Format a dictionary parameter for deep-object style serialization.
-
-According to OpenAPI 3.0 spec, deepObject style with explode=true serializes
-object properties as separate query parameters with bracket notation.
-
-For example, `{"id": "123", "type": "user"}` becomes
-`param[id]=123¶m[type]=user`.
-
-**Args:**
-- `param_value`: Dictionary value to format
-- `parameter_name`: Name of the parameter
-
-**Returns:**
-- Dictionary with bracketed parameter names as keys
-
-
-### `generate_example_from_schema`
-
-```python
-generate_example_from_schema(schema: JsonSchema | None) -> Any
-```
-
-
-Generate a simple example value from a JSON schema dictionary.
-Very basic implementation focusing on types.
-
-
-### `format_json_for_description`
-
-```python
-format_json_for_description(data: Any, indent: int = 2) -> str
-```
-
-
-Formats Python data as a JSON string block for Markdown.
-
-
-### `format_description_with_responses`
-
-```python
-format_description_with_responses(base_description: str, responses: dict[str, Any], parameters: list[ParameterInfo] | None = None, request_body: RequestBodyInfo | None = None) -> str
-```
-
-
-Formats the base description string with response, parameter, and request body information.
-
-**Args:**
-- `base_description`: The initial description to be formatted.
-- `responses`: A dictionary of response information, keyed by status code.
-- `parameters`: A list of parameter information,
-including path and query parameters. Each parameter includes details such as name,
-location, whether it is required, and a description.
-- `request_body`: Information about the request body,
-including its description, whether it is required, and its content schema.
-
-**Returns:**
-- The formatted description string with additional details about responses, parameters,
-- and the request body.
-
diff --git a/docs/python-sdk/fastmcp-utilities-openapi-json_schema_converter.mdx b/docs/python-sdk/fastmcp-utilities-openapi-json_schema_converter.mdx
deleted file mode 100644
index abd1d6869..000000000
--- a/docs/python-sdk/fastmcp-utilities-openapi-json_schema_converter.mdx
+++ /dev/null
@@ -1,62 +0,0 @@
----
-title: json_schema_converter
-sidebarTitle: json_schema_converter
----
-
-# `fastmcp.utilities.openapi.json_schema_converter`
-
-
-
-Clean OpenAPI 3.0 to JSON Schema converter for the experimental parser.
-
-This module provides a systematic approach to converting OpenAPI 3.0 schemas
-to JSON Schema, inspired by py-openapi-schema-to-json-schema but optimized
-for our specific use case.
-
-
-## Functions
-
-### `convert_openapi_schema_to_json_schema`
-
-```python
-convert_openapi_schema_to_json_schema(schema: dict[str, Any], openapi_version: str | None = None, remove_read_only: bool = False, remove_write_only: bool = False, convert_one_of_to_any_of: bool = True) -> dict[str, Any]
-```
-
-
-Convert an OpenAPI schema to JSON Schema format.
-
-This is a clean, systematic approach that:
-1. Removes OpenAPI-specific fields
-2. Converts nullable fields to type arrays (for OpenAPI 3.0 only)
-3. Converts oneOf to anyOf for overlapping union handling
-4. Recursively processes nested schemas
-5. Optionally removes readOnly/writeOnly properties
-
-**Args:**
-- `schema`: OpenAPI schema dictionary
-- `openapi_version`: OpenAPI version for optimization
-- `remove_read_only`: Whether to remove readOnly properties
-- `remove_write_only`: Whether to remove writeOnly properties
-- `convert_one_of_to_any_of`: Whether to convert oneOf to anyOf
-
-**Returns:**
-- JSON Schema-compatible dictionary
-
-
-### `convert_schema_definitions`
-
-```python
-convert_schema_definitions(schema_definitions: dict[str, Any] | None, openapi_version: str | None = None, **kwargs) -> dict[str, Any]
-```
-
-
-Convert a dictionary of OpenAPI schema definitions to JSON Schema.
-
-**Args:**
-- `schema_definitions`: Dictionary of schema definitions
-- `openapi_version`: OpenAPI version for optimization
-- `**kwargs`: Additional arguments passed to convert_openapi_schema_to_json_schema
-
-**Returns:**
-- Dictionary of converted schema definitions
-
diff --git a/docs/python-sdk/fastmcp-utilities-openapi-models.mdx b/docs/python-sdk/fastmcp-utilities-openapi-models.mdx
deleted file mode 100644
index 02a3fbe9e..000000000
--- a/docs/python-sdk/fastmcp-utilities-openapi-models.mdx
+++ /dev/null
@@ -1,35 +0,0 @@
----
-title: models
-sidebarTitle: models
----
-
-# `fastmcp.utilities.openapi.models`
-
-
-Intermediate Representation (IR) models for OpenAPI operations.
-
-## Classes
-
-### `ParameterInfo`
-
-
-Represents a single parameter for an HTTP operation in our IR.
-
-
-### `RequestBodyInfo`
-
-
-Represents the request body for an HTTP operation in our IR.
-
-
-### `ResponseInfo`
-
-
-Represents response information in our IR.
-
-
-### `HTTPRoute`
-
-
-Intermediate Representation for a single OpenAPI operation.
-
diff --git a/docs/python-sdk/fastmcp-utilities-openapi-parser.mdx b/docs/python-sdk/fastmcp-utilities-openapi-parser.mdx
deleted file mode 100644
index 0a9a2a272..000000000
--- a/docs/python-sdk/fastmcp-utilities-openapi-parser.mdx
+++ /dev/null
@@ -1,43 +0,0 @@
----
-title: parser
-sidebarTitle: parser
----
-
-# `fastmcp.utilities.openapi.parser`
-
-
-OpenAPI parsing logic for converting OpenAPI specs to HTTPRoute objects.
-
-## Functions
-
-### `parse_openapi_to_http_routes`
-
-```python
-parse_openapi_to_http_routes(openapi_dict: dict[str, Any]) -> list[HTTPRoute]
-```
-
-
-Parses an OpenAPI schema dictionary into a list of HTTPRoute objects
-using the openapi-pydantic library.
-
-Supports both OpenAPI 3.0.x and 3.1.x versions.
-
-
-## Classes
-
-### `OpenAPIParser`
-
-
-Unified parser for OpenAPI schemas with generic type parameters to handle both 3.0 and 3.1.
-
-
-**Methods:**
-
-#### `parse`
-
-```python
-parse(self) -> list[HTTPRoute]
-```
-
-Parse the OpenAPI schema into HTTP routes.
-
diff --git a/docs/python-sdk/fastmcp-utilities-openapi-schemas.mdx b/docs/python-sdk/fastmcp-utilities-openapi-schemas.mdx
deleted file mode 100644
index fad88ee5f..000000000
--- a/docs/python-sdk/fastmcp-utilities-openapi-schemas.mdx
+++ /dev/null
@@ -1,43 +0,0 @@
----
-title: schemas
-sidebarTitle: schemas
----
-
-# `fastmcp.utilities.openapi.schemas`
-
-
-Schema manipulation utilities for OpenAPI operations.
-
-## Functions
-
-### `clean_schema_for_display`
-
-```python
-clean_schema_for_display(schema: JsonSchema | None) -> JsonSchema | None
-```
-
-
-Clean up a schema dictionary for display by removing internal/complex fields.
-
-
-### `extract_output_schema_from_responses`
-
-```python
-extract_output_schema_from_responses(responses: dict[str, ResponseInfo], schema_definitions: dict[str, Any] | None = None, openapi_version: str | None = None) -> dict[str, Any] | None
-```
-
-
-Extract output schema from OpenAPI responses for use as MCP tool output schema.
-
-This function finds the first successful response (200, 201, 202, 204) with a
-JSON-compatible content type and extracts its schema. If the schema is not an
-object type, it wraps it to comply with MCP requirements.
-
-**Args:**
-- `responses`: Dictionary of ResponseInfo objects keyed by status code
-- `schema_definitions`: Optional schema definitions to include in the output schema
-- `openapi_version`: OpenAPI version string, used to optimize nullable field handling
-
-**Returns:**
-- MCP-compliant output schema with potential wrapping, or None if no suitable schema found
-
diff --git a/docs/python-sdk/fastmcp-utilities-openapi-__init__.mdx b/docs/python-sdk/fastmcp-utilities-openapi.mdx
similarity index 74%
rename from docs/python-sdk/fastmcp-utilities-openapi-__init__.mdx
rename to docs/python-sdk/fastmcp-utilities-openapi.mdx
index df0331f9e..cb8bcf974 100644
--- a/docs/python-sdk/fastmcp-utilities-openapi-__init__.mdx
+++ b/docs/python-sdk/fastmcp-utilities-openapi.mdx
@@ -1,6 +1,6 @@
---
-title: __init__
-sidebarTitle: __init__
+title: openapi
+sidebarTitle: openapi
---
# `fastmcp.utilities.openapi`
diff --git a/docs/python-sdk/fastmcp-utilities-pagination.mdx b/docs/python-sdk/fastmcp-utilities-pagination.mdx
index 0381a36aa..7008f2355 100644
--- a/docs/python-sdk/fastmcp-utilities-pagination.mdx
+++ b/docs/python-sdk/fastmcp-utilities-pagination.mdx
@@ -10,7 +10,7 @@ Pagination utilities for MCP list operations.
## Functions
-### `paginate_sequence`
+### `paginate_sequence`
```python
paginate_sequence(items: Sequence[T], cursor: str | None, page_size: int) -> tuple[list[T], str | None]
@@ -33,7 +33,7 @@ Paginate a sequence of items.
## Classes
-### `CursorState`
+### `CursorState`
Internal representation of pagination cursor state.
@@ -44,7 +44,7 @@ per the MCP spec - they should not parse or modify cursors.
**Methods:**
-#### `encode`
+#### `encode`
```python
encode(self) -> str
@@ -53,7 +53,7 @@ encode(self) -> str
Encode cursor state to an opaque string.
-#### `decode`
+#### `decode`
```python
decode(cls, cursor: str) -> CursorState
diff --git a/docs/python-sdk/fastmcp-utilities-prefab.mdx b/docs/python-sdk/fastmcp-utilities-prefab.mdx
new file mode 100644
index 000000000..b03d7b185
--- /dev/null
+++ b/docs/python-sdk/fastmcp-utilities-prefab.mdx
@@ -0,0 +1,61 @@
+---
+title: prefab
+sidebarTitle: prefab
+---
+
+# `fastmcp.utilities.prefab`
+
+
+Lazy helpers for FastMCP's optional Prefab UI integration.
+
+## Functions
+
+### `prefab_available`
+
+```python
+prefab_available() -> bool
+```
+
+
+Return whether Prefab UI is installed without importing it.
+
+
+### `is_prefab_type`
+
+```python
+is_prefab_type(candidate: Any) -> bool
+```
+
+
+Return whether a type is a Prefab app or component type.
+
+
+### `is_prefab_app`
+
+```python
+is_prefab_app(value: Any) -> bool
+```
+
+
+Return whether a value is a Prefab app.
+
+
+### `is_prefab_component`
+
+```python
+is_prefab_component(value: Any) -> bool
+```
+
+
+Return whether a value is a Prefab component.
+
+
+### `prefab_app_from_component`
+
+```python
+prefab_app_from_component(component: Any) -> Any
+```
+
+
+Wrap a Prefab component in a Prefab app.
+
diff --git a/docs/python-sdk/fastmcp-utilities-skills.mdx b/docs/python-sdk/fastmcp-utilities-skills.mdx
index ccb0c83b8..cdbbbad2a 100644
--- a/docs/python-sdk/fastmcp-utilities-skills.mdx
+++ b/docs/python-sdk/fastmcp-utilities-skills.mdx
@@ -10,7 +10,7 @@ Client utilities for discovering and downloading skills from MCP servers.
## Functions
-### `list_skills`
+### `list_skills`
```python
list_skills(client: Client) -> list[SkillSummary]
@@ -29,7 +29,7 @@ Discovers skills by finding resources with URIs matching the
- List of SkillSummary objects with name, description, and URI
-### `get_skill_manifest`
+### `get_skill_manifest`
```python
get_skill_manifest(client: Client, skill_name: str) -> SkillManifest
@@ -49,7 +49,7 @@ Get the manifest for a specific skill.
- `ValueError`: If manifest cannot be read or parsed
-### `download_skill`
+### `download_skill`
```python
download_skill(client: Client, skill_name: str, target_dir: str | Path) -> Path
@@ -75,7 +75,7 @@ Creates a subdirectory named after the skill containing all files.
- `FileExistsError`: If skill directory exists and overwrite=False
-### `sync_skills`
+### `sync_skills`
```python
sync_skills(client: Client, target_dir: str | Path) -> list[Path]
@@ -95,19 +95,19 @@ Download all available skills from a server.
## Classes
-### `SkillSummary`
+### `SkillSummary`
Summary information about a skill available on a server.
-### `SkillFile`
+### `SkillFile`
Information about a file within a skill.
-### `SkillManifest`
+### `SkillManifest`
Full manifest of a skill including all files.
diff --git a/docs/python-sdk/fastmcp-utilities-tasks.mdx b/docs/python-sdk/fastmcp-utilities-tasks.mdx
new file mode 100644
index 000000000..7a520bb04
--- /dev/null
+++ b/docs/python-sdk/fastmcp-utilities-tasks.mdx
@@ -0,0 +1,62 @@
+---
+title: tasks
+sidebarTitle: tasks
+---
+
+# `fastmcp.utilities.tasks`
+
+
+Task configuration primitives for FastMCP components.
+
+## Classes
+
+### `TaskMeta`
+
+
+Metadata for task-augmented execution requests.
+
+**Attributes:**
+- `ttl`: Client-requested TTL in milliseconds. If None, uses server default.
+- `fn_key`: Docket routing key. Auto-derived from component name if None.
+
+
+### `TaskConfig`
+
+
+Configuration for MCP background task execution.
+
+Controls how a component handles task-augmented requests:
+
+- ``forbidden``: Component does not support task execution.
+- ``optional``: Component supports both synchronous and task execution.
+- ``required``: Component requires task execution.
+
+
+**Methods:**
+
+#### `from_bool`
+
+```python
+from_bool(cls, value: bool) -> TaskConfig
+```
+
+Convert a boolean task flag to a TaskConfig.
+
+
+#### `supports_tasks`
+
+```python
+supports_tasks(self) -> bool
+```
+
+Check if this component supports task execution.
+
+
+#### `validate_function`
+
+```python
+validate_function(self, fn: Callable[..., Any], name: str) -> None
+```
+
+Validate that a function is compatible with this task config.
+
diff --git a/docs/python-sdk/fastmcp-utilities-tests.mdx b/docs/python-sdk/fastmcp-utilities-tests.mdx
index 5a40ff61f..6c1ce3e92 100644
--- a/docs/python-sdk/fastmcp-utilities-tests.mdx
+++ b/docs/python-sdk/fastmcp-utilities-tests.mdx
@@ -7,7 +7,7 @@ sidebarTitle: tests
## Functions
-### `temporary_settings`
+### `temporary_settings`
```python
temporary_settings(**kwargs: Any)
@@ -20,7 +20,7 @@ Temporarily override FastMCP setting values.
- `**kwargs`: The settings to override, including nested settings.
-### `run_server_in_process`
+### `run_server_in_process`
```python
run_server_in_process(server_fn: Callable[..., None], *args: Any, **kwargs: Any) -> Generator[str, None, None]
@@ -43,18 +43,20 @@ not pickleable, so we need a function that creates and runs one.
- The server URL.
-### `run_server_async`
+### `run_server_async`
```python
run_server_async(server: FastMCP, port: int | None = None, transport: Literal['http', 'streamable-http', 'sse'] = 'http', path: str = '/mcp', host: str = '127.0.0.1') -> AsyncGenerator[str, None]
```
-Start a FastMCP server as an asyncio task for in-process async testing.
+Start a FastMCP server on a real port as an asyncio task.
-This is the recommended way to test FastMCP servers. It runs the server
-as an async task in the same process, eliminating subprocess coordination,
-sleeps, and cleanup issues.
+This runs a real uvicorn server in the current process, bound to a real TCP port,
+and yields its URL. Use it when the behaviour under test is genuinely about the
+network — real sockets, TLS, or a server that must be reachable by something other
+than an in-process client. Otherwise prefer `asgi_client` or `asgi_server`, which
+exercise the same HTTP stack without binding a port.
**Args:**
- `server`: FastMCP server instance
@@ -64,9 +66,124 @@ sleeps, and cleanup issues.
- `host`: Host to bind to (default\: "127.0.0.1")
+### `asgi_server`
+
+```python
+asgi_server(server: FastMCP, transport: Literal['http', 'streamable-http', 'sse'] = 'http', path: str | None = None, **http_app_kwargs: Any) -> AsyncGenerator[ASGIServer, None]
+```
+
+
+Serve a FastMCP server's HTTP app in-process, with no socket and no uvicorn.
+
+This is the fastest way to test a FastMCP server over HTTP. The server's real
+Starlette app is built with `http_app()` and its lifespan is started, then every
+request is dispatched directly into the app on the current event loop. That skips
+port binding, uvicorn startup and connection setup entirely, while still exercising
+the full HTTP stack: middleware, authentication, session management and SSE
+streaming all run exactly as they do in production.
+
+Use this as a fixture when several tests share one server but each needs its own
+client. For a single test, `asgi_client` hands you a connected client in one step.
+
+**Args:**
+- `server`: FastMCP server instance.
+- `transport`: Transport type ("http", "streamable-http", or "sse").
+- `path`: URL path for the server (defaults to "/mcp", or "/sse" for SSE).
+- `**http_app_kwargs`: Additional arguments forwarded to `server.http_app()`.
+
+
+### `asgi_client`
+
+```python
+asgi_client(server: FastMCP, transport: Literal['http', 'streamable-http', 'sse'] = 'http', path: str | None = None, **client_kwargs: Any) -> AsyncGenerator[Client, None]
+```
+
+
+Serve a FastMCP server over HTTP in-process and yield a connected `Client`.
+
+This is the shortest path to testing a server over a real HTTP stack. The server's
+Starlette app is built and started, and requests are dispatched straight into it on
+the current event loop — no port, no uvicorn, no subprocess — but middleware,
+authentication, session management and SSE streaming all behave as in production.
+
+Reach for `asgi_server` instead when a fixture must serve several tests that each
+build their own client, or when a test needs raw HTTP access to the app.
+
+**Args:**
+- `server`: FastMCP server instance.
+- `transport`: Transport type ("http", "streamable-http", or "sse").
+- `path`: URL path for the server (defaults to "/mcp", or "/sse" for SSE).
+- `headers`: HTTP headers to send with every request.
+- `auth`: Client authentication, as accepted by the HTTP transports.
+- `**client_kwargs`: Additional arguments forwarded to `Client`.
+
+
## Classes
-### `HeadlessOAuth`
+### `ASGIServer`
+
+
+A FastMCP server's real HTTP app, reachable in-process with no sockets.
+
+Yielded by `asgi_server`. The `url` looks like an ordinary server URL and the app
+behind it is the genuine article — auth middleware, session manager, SSE framing and
+redirects all run — but every request is dispatched straight into the ASGI
+application on the current event loop.
+
+Because nothing is listening on the network, a plain `httpx2.AsyncClient()` cannot
+reach this server. Use `client()` for a FastMCP client, `http_client()` for raw HTTP
+assertions, and `transport()` when you need to build the client transport yourself.
+
+
+**Methods:**
+
+#### `http_client`
+
+```python
+http_client(self, headers: dict[str, str] | None = None, timeout: httpx2.Timeout | None = None, auth: httpx2.Auth | None = None, **kwargs: Any) -> httpx2.AsyncClient
+```
+
+An `httpx2.AsyncClient` bound to the in-process app, for raw HTTP assertions.
+
+Relative URLs resolve against the server's base URL, and absolute URLs on the
+same origin work too, so `client.get(f"{server.url}/health")` reads the same as
+it would against a real server.
+
+The signature matches `McpHttpClientFactory`, so this method can also be handed
+to anything that takes an `httpx_client_factory`.
+
+
+#### `transport`
+
+```python
+transport(self, **kwargs: Any) -> StreamableHttpTransport | SSETransport
+```
+
+A FastMCP client transport wired to the in-process app.
+
+Accepts the same keyword arguments as the underlying transport (`headers`,
+`auth`, ...); `httpx_client_factory` is supplied automatically.
+
+
+#### `client`
+
+```python
+client(self, **client_kwargs: Any) -> Client
+```
+
+An unconnected FastMCP `Client` pointed at the in-process app.
+
+`headers` and `auth` configure the underlying HTTP transport; every other
+keyword argument is passed to `Client` (`timeout`, `elicitation_handler`, ...).
+Use it as a context manager, exactly like any other client.
+
+**Args:**
+- `headers`: HTTP headers to send with every request.
+- `auth`: Client authentication, as accepted by the HTTP transports.
+- `**client_kwargs`: Additional arguments forwarded to `Client`.
+
+
+### `HeadlessOAuth`
OAuth provider that bypasses browser interaction for testing.
@@ -77,7 +194,7 @@ instead of opening a browser and running a callback server. Useful for automated
**Methods:**
-#### `redirect_handler`
+#### `redirect_handler`
```python
redirect_handler(self, authorization_url: str) -> None
@@ -86,11 +203,11 @@ redirect_handler(self, authorization_url: str) -> None
Make HTTP request to authorization URL and store response for callback handler.
-#### `callback_handler`
+#### `callback_handler`
```python
-callback_handler(self) -> tuple[str, str | None]
+callback_handler(self) -> AuthorizationCodeResult
```
-Parse stored response and return (auth_code, state).
+Parse stored response and return the authorization code result.
diff --git a/docs/python-sdk/fastmcp-utilities-timeout.mdx b/docs/python-sdk/fastmcp-utilities-timeout.mdx
index 3a8cb41b9..c9c890e1c 100644
--- a/docs/python-sdk/fastmcp-utilities-timeout.mdx
+++ b/docs/python-sdk/fastmcp-utilities-timeout.mdx
@@ -10,7 +10,7 @@ Timeout normalization utilities.
## Functions
-### `normalize_timeout_to_timedelta`
+### `normalize_timeout_to_timedelta`
```python
normalize_timeout_to_timedelta(value: int | float | datetime.timedelta | None) -> datetime.timedelta | None
@@ -26,7 +26,7 @@ Normalize a timeout value to a timedelta.
- timedelta if value provided, None otherwise
-### `normalize_timeout_to_seconds`
+### `normalize_timeout_to_seconds`
```python
normalize_timeout_to_seconds(value: int | float | datetime.timedelta | None) -> float | None
diff --git a/docs/python-sdk/fastmcp-utilities-token_cache.mdx b/docs/python-sdk/fastmcp-utilities-token_cache.mdx
new file mode 100644
index 000000000..a831d2057
--- /dev/null
+++ b/docs/python-sdk/fastmcp-utilities-token_cache.mdx
@@ -0,0 +1,87 @@
+---
+title: token_cache
+sidebarTitle: token_cache
+---
+
+# `fastmcp.utilities.token_cache`
+
+
+In-memory cache for token verification results.
+
+Provides a generic TTL-based cache for ``AccessToken`` objects, designed to
+reduce repeated network calls during opaque-token verification. Only
+*successful* verifications should be cached; errors and failures must be
+retried on every request.
+
+Example:
+ ```python
+ from fastmcp.utilities.token_cache import TokenCache
+
+ cache = TokenCache(ttl_seconds=300, max_size=10000)
+
+ # On cache miss, call the upstream verifier and store the result.
+ hit, token = cache.get(raw_token)
+ if not hit:
+ token = await _call_upstream(raw_token)
+ if token is not None:
+ cache.set(raw_token, token)
+ ```
+
+
+## Classes
+
+### `TokenCache`
+
+
+TTL-based in-memory cache for ``AccessToken`` objects.
+
+Features:
+- SHA-256 hashed cache keys (fixed size, regardless of token length).
+- Per-entry TTL that respects both the configured ``ttl_seconds`` and the
+ token's own ``expires_at`` claim (whichever is sooner).
+- Bounded size with FIFO eviction when the cache is full.
+- Periodic cleanup of expired entries to prevent unbounded growth.
+- Defensive deep copies on both store and retrieve to prevent
+ callers from mutating cached values.
+
+Caching is disabled when ``ttl_seconds`` is ``None`` or ``0``, or
+when ``max_size`` is ``0``. Negative values raise ``ValueError``.
+
+
+**Methods:**
+
+#### `enabled`
+
+```python
+enabled(self) -> bool
+```
+
+Return whether caching is active.
+
+
+#### `get`
+
+```python
+get(self, token: str) -> tuple[bool, AccessToken | None]
+```
+
+Look up a cached verification result.
+
+**Returns:**
+- ``(True, AccessToken)`` on a cache hit, ``(False, None)`` on a miss
+- or when caching is disabled. The returned ``AccessToken`` is a deep
+- copy that is safe to mutate.
+
+
+#### `set`
+
+```python
+set(self, token: str, result: AccessToken) -> None
+```
+
+Store a *successful* verification result.
+
+Only successful verifications should be cached. Failures (inactive
+tokens, missing scopes, HTTP errors, timeouts) must **not** be cached
+so that transient problems do not produce sticky false negatives.
+
diff --git a/docs/python-sdk/fastmcp-utilities-types.mdx b/docs/python-sdk/fastmcp-utilities-types.mdx
index 6cae8cdc0..6b8ca2ce8 100644
--- a/docs/python-sdk/fastmcp-utilities-types.mdx
+++ b/docs/python-sdk/fastmcp-utilities-types.mdx
@@ -10,13 +10,13 @@ Common types used across FastMCP.
## Functions
-### `get_fn_name`
+### `get_fn_name`
```python
get_fn_name(fn: Callable[..., Any]) -> str
```
-### `get_cached_typeadapter`
+### `get_cached_typeadapter`
```python
get_cached_typeadapter(cls: T) -> TypeAdapter[T]
@@ -29,7 +29,7 @@ However, this isn't feasible for user-generated functions. Instead, we use a
cache to minimize the cost of creating them as much as possible.
-### `issubclass_safe`
+### `issubclass_safe`
```python
issubclass_safe(cls: type, base: type) -> bool
@@ -39,7 +39,7 @@ issubclass_safe(cls: type, base: type) -> bool
Check if cls is a subclass of base, even if cls is a type variable.
-### `is_class_member_of_type`
+### `is_class_member_of_type`
```python
is_class_member_of_type(cls: Any, base: type) -> bool
@@ -52,7 +52,7 @@ Base can be a type, a UnionType, or an Annotated type. Generic types are not
considered members (e.g. T is not a member of list\[T]).
-### `find_kwarg_by_type`
+### `find_kwarg_by_type`
```python
find_kwarg_by_type(fn: Callable, kwarg_type: type) -> str | None
@@ -64,7 +64,7 @@ Find the name of the kwarg that is of type kwarg_type.
Includes union types that contain the kwarg_type, as well as Annotated types.
-### `create_function_without_params`
+### `create_function_without_params`
```python
create_function_without_params(fn: Callable[..., Any], exclude_params: list[str]) -> Callable[..., Any]
@@ -77,7 +77,7 @@ This is used to exclude parameters from type adapter processing when they can't
The excluded parameters are removed from the function's __annotations__ dictionary.
-### `replace_type`
+### `replace_type`
```python
replace_type(type_, type_map: dict[type, type])
@@ -85,14 +85,13 @@ replace_type(type_, type_map: dict[type, type])
Given a (possibly generic, nested, or otherwise complex) type, replaces all
-instances of old_type with new_type.
+instances of keys in type_map with their corresponding values.
This is useful for transforming types when creating tools.
**Args:**
-- `type_`: The type to replace instances of old_type with new_type.
-- `old_type`: The type to replace.
-- `new_type`: The type to replace old_type with.
+- `type_`: The type to transform.
+- `type_map`: A mapping of types to replace (keys are replaced by values).
Examples:
```python
@@ -106,13 +105,13 @@ list[list[str]]
## Classes
-### `FastMCPBaseModel`
+### `FastMCPBaseModel`
Base model for FastMCP models.
-### `Image`
+### `Image`
Helper class for returning images from tools.
@@ -120,16 +119,16 @@ Helper class for returning images from tools.
**Methods:**
-#### `to_image_content`
+#### `to_image_content`
```python
-to_image_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp.types.ImageContent
+to_image_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp_types.ImageContent
```
Convert to MCP ImageContent.
-#### `to_data_uri`
+#### `to_data_uri`
```python
to_data_uri(self, mime_type: str | None = None) -> str
@@ -138,7 +137,7 @@ to_data_uri(self, mime_type: str | None = None) -> str
Get image as a data URI.
-### `Audio`
+### `Audio`
Helper class for returning audio from tools.
@@ -146,13 +145,13 @@ Helper class for returning audio from tools.
**Methods:**
-#### `to_audio_content`
+#### `to_audio_content`
```python
-to_audio_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp.types.AudioContent
+to_audio_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp_types.AudioContent
```
-### `File`
+### `File`
Helper class for returning file data from tools.
@@ -160,10 +159,10 @@ Helper class for returning file data from tools.
**Methods:**
-#### `to_resource_content`
+#### `to_resource_content`
```python
-to_resource_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp.types.EmbeddedResource
+to_resource_content(self, mime_type: str | None = None, annotations: Annotations | None = None) -> mcp_types.EmbeddedResource
```
-### `ContextSamplingFallbackProtocol`
+### `ContextSamplingFallbackProtocol`
diff --git a/docs/python-sdk/fastmcp-utilities-ui.mdx b/docs/python-sdk/fastmcp-utilities-ui.mdx
index eb060d2d3..7687df7ea 100644
--- a/docs/python-sdk/fastmcp-utilities-ui.mdx
+++ b/docs/python-sdk/fastmcp-utilities-ui.mdx
@@ -15,7 +15,7 @@ consent pages, and other user-facing interfaces.
## Functions
-### `create_page`
+### `create_page`
```python
create_page(content: str, title: str = 'FastMCP', additional_styles: str = '', csp_policy: str = "default-src 'none'; style-src 'unsafe-inline'; img-src https: data:; base-uri 'none'") -> str
@@ -35,7 +35,7 @@ If empty string "", the CSP meta tag is omitted entirely.
- Complete HTML page as string
-### `create_logo`
+### `create_logo`
```python
create_logo(icon_url: str | None = None, alt_text: str = 'FastMCP') -> str
@@ -52,7 +52,7 @@ Create logo HTML.
- HTML for logo image tag.
-### `create_status_message`
+### `create_status_message`
```python
create_status_message(message: str, is_success: bool = True) -> str
@@ -69,7 +69,7 @@ Create a status message with icon.
- HTML for status message
-### `create_info_box`
+### `create_info_box`
```python
create_info_box(content: str, is_error: bool = False, centered: bool = False, monospace: bool = False) -> str
@@ -88,7 +88,7 @@ Create an info box.
- HTML for info box
-### `create_detail_box`
+### `create_detail_box`
```python
create_detail_box(rows: list[tuple[str, str]]) -> str
@@ -104,7 +104,7 @@ Create a detail box with key-value pairs.
- HTML for detail box
-### `create_button_group`
+### `create_button_group`
```python
create_button_group(buttons: list[tuple[str, str, str]]) -> str
@@ -120,7 +120,7 @@ Create a group of buttons.
- HTML for button group
-### `create_secure_html_response`
+### `create_secure_html_response`
```python
create_secure_html_response(html: str, status_code: int = 200) -> HTMLResponse
diff --git a/docs/python-sdk/fastmcp-utilities-version_check.mdx b/docs/python-sdk/fastmcp-utilities-version_check.mdx
index b27951eda..6a4377a08 100644
--- a/docs/python-sdk/fastmcp-utilities-version_check.mdx
+++ b/docs/python-sdk/fastmcp-utilities-version_check.mdx
@@ -10,7 +10,7 @@ Version checking utilities for FastMCP.
## Functions
-### `get_latest_version`
+### `get_latest_version`
```python
get_latest_version(include_prereleases: bool = False) -> str | None
@@ -26,7 +26,7 @@ Get the latest version of FastMCP from PyPI, using cache when available.
- The latest version string, or None if unavailable.
-### `check_for_newer_version`
+### `check_for_newer_version`
```python
check_for_newer_version() -> str | None
diff --git a/docs/python-sdk/fastmcp-utilities-versions.mdx b/docs/python-sdk/fastmcp-utilities-versions.mdx
index f5e44f296..1eaa6391a 100644
--- a/docs/python-sdk/fastmcp-utilities-versions.mdx
+++ b/docs/python-sdk/fastmcp-utilities-versions.mdx
@@ -22,7 +22,7 @@ Examples:
## Functions
-### `parse_version_key`
+### `parse_version_key`
```python
parse_version_key(version: str | None) -> VersionKey
@@ -38,10 +38,10 @@ Parse a version string into a sortable key.
- A VersionKey suitable for sorting.
-### `version_sort_key`
+### `version_sort_key`
```python
-version_sort_key(component: FastMCPComponent) -> VersionKey
+version_sort_key(component: FastMCPComponent) -> tuple[VersionKey, str]
```
@@ -49,14 +49,22 @@ Get a sort key for a component based on its version.
Use with sorted() or max() to order components by version.
+The key is a `(VersionKey, raw)` tuple. The `VersionKey` orders by PEP 440
+semantics (or lexicographically for non-PEP 440 strings); the raw version
+string is a deterministic tie-breaker so that two components whose versions
+are PEP 440-equivalent but spelled differently (e.g. `"1"` and `"1.0"`) are
+ordered reproducibly instead of by registration order. The raw tie-breaker
+only affects equivalent-version ties and never the primary version order,
+so range/equality matching (which uses `VersionKey` directly) is unchanged.
+
**Args:**
- `component`: The component to get a sort key for.
**Returns:**
-- A sortable VersionKey.
+- A deterministic, sortable `(VersionKey, raw)` tuple.
-### `compare_versions`
+### `compare_versions`
```python
compare_versions(a: str | None, b: str | None) -> int
@@ -73,7 +81,7 @@ Compare two version strings.
- -1 if a < b, 0 if a == b, 1 if a > b.
-### `is_version_greater`
+### `is_version_greater`
```python
is_version_greater(a: str | None, b: str | None) -> bool
@@ -90,7 +98,7 @@ Check if version a is greater than version b.
- True if a > b, False otherwise.
-### `max_version`
+### `max_version`
```python
max_version(a: str | None, b: str | None) -> str | None
@@ -107,7 +115,7 @@ Return the greater of two versions.
- The greater version, or None if both are None.
-### `min_version`
+### `min_version`
```python
min_version(a: str | None, b: str | None) -> str | None
@@ -124,9 +132,29 @@ Return the lesser of two versions.
- The lesser version, or None if both are None.
+### `dedupe_with_versions`
+
+```python
+dedupe_with_versions(components: Sequence[C], key_fn: Callable[[C], str]) -> list[C]
+```
+
+
+Deduplicate components by key, keeping highest version.
+
+Groups components by key, selects the highest version from each group,
+and injects available versions into meta if any component is versioned.
+
+**Args:**
+- `components`: Sequence of components to deduplicate.
+- `key_fn`: Function to extract the grouping key from a component.
+
+**Returns:**
+- Deduplicated list with versions injected into meta.
+
+
## Classes
-### `VersionSpec`
+### `VersionSpec`
Specification for filtering components by version.
@@ -139,11 +167,18 @@ match any spec.
- `gte`: If set, only versions >= this value match.
- `lt`: If set, only versions < this value match.
- `eq`: If set, only this exact version matches (gte/lt ignored).
+Matching is PEP 440-normalized and `v`-prefix insensitive, so
+`eq="v1.0"` matches a component versioned `"1.0"`, and `eq="1.0"`
+matches `"1"` (PEP 440 treats `1` and `1.0` as the same version).
+If a server registers two PEP 440-equivalent spellings of the
+same component (e.g. both `"1"` and `"1.0"`), they are the same
+version under this spec; selection among them is deterministic
+(see `version_sort_key`), not registration-order dependent.
**Methods:**
-#### `matches`
+#### `matches`
```python
matches(self, version: str | None) -> bool
@@ -162,7 +197,7 @@ from version-specific rules.
- True if the version matches the spec.
-#### `intersect`
+#### `intersect`
```python
intersect(self, other: VersionSpec | None) -> VersionSpec
@@ -181,7 +216,7 @@ the intersection validates "1.0" is in range and returns the exact spec.
- A VersionSpec that matches only versions satisfying both specs.
-### `VersionKey`
+### `VersionKey`
A comparable version key that handles None, PEP 440 versions, and strings.
diff --git a/docs/servers/auth/authentication.mdx b/docs/servers/auth/authentication.mdx
index 9a26df138..a948a647c 100644
--- a/docs/servers/auth/authentication.mdx
+++ b/docs/servers/auth/authentication.mdx
@@ -161,7 +161,7 @@ The implementation provides all required OAuth endpoints including authorization
```python
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.oauth import MyOAuthProvider
+from fastmcp.server.auth import OAuthProvider
auth = MyOAuthProvider(
user_store=your_user_database,
@@ -189,11 +189,19 @@ from fastmcp import FastMCP
from fastmcp.server.auth import MultiAuth, OAuthProxy
from fastmcp.server.auth.providers.jwt import JWTVerifier
+upstream_verifier = JWTVerifier(
+ jwks_uri="https://login.example.com/.well-known/jwks.json",
+ issuer="https://login.example.com",
+ audience="my-app",
+)
+
auth = MultiAuth(
server=OAuthProxy(
- issuer_url="https://login.example.com/...",
- client_id="my-app",
- client_secret="secret",
+ upstream_authorization_endpoint="https://login.example.com/oauth/authorize",
+ upstream_token_endpoint="https://login.example.com/oauth/token",
+ upstream_client_id="my-app",
+ upstream_client_secret="secret",
+ token_verifier=upstream_verifier,
base_url="https://my-server.com",
),
verifiers=[
diff --git a/docs/servers/auth/multi-auth.mdx b/docs/servers/auth/multi-auth.mdx
index ba54d25ab..3675c92a6 100644
--- a/docs/servers/auth/multi-auth.mdx
+++ b/docs/servers/auth/multi-auth.mdx
@@ -22,11 +22,19 @@ from fastmcp import FastMCP
from fastmcp.server.auth import MultiAuth, OAuthProxy
from fastmcp.server.auth.providers.jwt import JWTVerifier
+upstream_verifier = JWTVerifier(
+ jwks_uri="https://login.example.com/.well-known/jwks.json",
+ issuer="https://login.example.com",
+ audience="my-app",
+)
+
auth = MultiAuth(
server=OAuthProxy(
- issuer_url="https://login.example.com/...",
- client_id="my-app",
- client_secret="secret",
+ upstream_authorization_endpoint="https://login.example.com/oauth/authorize",
+ upstream_token_endpoint="https://login.example.com/oauth/token",
+ upstream_client_id="my-app",
+ upstream_client_secret="secret",
+ token_verifier=upstream_verifier,
base_url="https://my-server.com",
),
verifiers=[
diff --git a/docs/servers/auth/oauth-proxy.mdx b/docs/servers/auth/oauth-proxy.mdx
index e8e79507c..03727ec87 100644
--- a/docs/servers/auth/oauth-proxy.mdx
+++ b/docs/servers/auth/oauth-proxy.mdx
@@ -3,7 +3,6 @@ title: OAuth Proxy
sidebarTitle: OAuth Proxy
description: Bridge traditional OAuth providers to work seamlessly with MCP's authentication flow.
icon: share
-tag: NEW
---
import { VersionBadge } from "/snippets/version-badge.mdx";
@@ -100,8 +99,11 @@ mcp = FastMCP(name="My Server", auth=auth)
Client ID from your registered OAuth application
-
- Client secret from your registered OAuth application
+
+ Client secret from your registered OAuth application. Optional for PKCE public
+ clients or when using alternative credentials (e.g., managed identity client
+ assertions via a subclass). When omitted, `jwt_signing_key` must be provided
+ explicitly since it cannot be derived from the secret.
@@ -112,7 +114,13 @@ mcp = FastMCP(name="My Server", auth=auth)
Public URL where OAuth endpoints will be accessible, **including any mount path** (e.g., `https://your-server.com/api`).
- This URL is used to construct OAuth callback URLs and operational endpoints. When mounting under a path prefix, include that prefix in `base_url`. Use `issuer_url` separately to specify where auth server metadata is located (typically at root level).
+ This URL is used to construct OAuth callback URLs and operational endpoints. When mounting under a path prefix, include that prefix in `base_url`. Use `issuer_url` separately to give the server an OAuth identity that differs from where its endpoints are mounted (typically the root level).
+
+
+
+ Optional public base URL for the protected resource metadata and token audience.
+
+ Use this when your OAuth callbacks and operational endpoints need to live under one public URL, but the protected MCP resource should be advertised under another. FastMCP will still append the MCP mount path (for example, `/mcp`) to this base URL.
@@ -127,6 +135,8 @@ mcp = FastMCP(name="My Server", auth=auth)
Issuer URL for OAuth authorization server metadata (defaults to `base_url`).
+ `issuer_url` is the server's OAuth identity: it is the `issuer` field of the authorization server metadata, the `iss` claim of the tokens the proxy mints, and the RFC 9207 `iss` parameter on authorization responses. `base_url` remains the location of the endpoints, so `authorization_endpoint`, `token_endpoint`, and the rest of the metadata still point at `base_url` where the routes are actually mounted.
+
When `issuer_url` has a path component (either explicitly or by defaulting from `base_url`), FastMCP creates path-aware discovery routes per RFC 8414. For example, if `base_url` is `http://localhost:8000/api`, the authorization server metadata will be at `/.well-known/oauth-authorization-server/api`.
**Default behavior (recommended for most cases):**
@@ -163,6 +173,14 @@ mcp = FastMCP(name="My Server", auth=auth)
provider doesn't support PKCE
+
+ Whether to forward RFC 8707 `resource` parameters from MCP clients to the
+ upstream OAuth provider. When enabled, the proxy includes the resource indicator
+ in authorization requests, allowing providers that support RFC 8707 to scope
+ tokens to specific resources. Disable for providers that reject unknown
+ parameters.
+
+
Token endpoint authentication method for the upstream OAuth server. Controls
how the proxy authenticates when exchanging authorization codes and refresh
@@ -176,17 +194,23 @@ mcp = FastMCP(name="My Server", auth=auth)
List of allowed redirect URI patterns for MCP clients. Patterns support
- wildcards (e.g., `"http://localhost:*"`, `"https://*.example.com/*"`). -
- `None` (default): All redirect URIs allowed (for MCP/DCR compatibility) -
- Empty list `[]`: No redirect URIs allowed - Custom list: Only matching
- patterns allowed These patterns apply to MCP client loopback redirects, NOT
- the upstream OAuth app redirect URI.
+ wildcards (e.g., `"http://localhost:*"`, `"https://*.example.com/*"`).
+ - `None` (default): DCR clients use registered redirect URIs, with loopback
+ ports allowed to vary for MCP compatibility. Unsafe browser schemes such as
+ `javascript:`, `data:`, `file:`, and `vbscript:` are rejected.
+ - Empty list `[]`: No redirect URIs allowed
+ - Custom list: Only matching patterns allowed
+
+ These patterns apply to MCP client loopback redirects. Configure the upstream
+ OAuth app redirect URI separately with `redirect_path`.
- List of all possible valid scopes for the OAuth provider. These are advertised
- to clients through the `/.well-known` endpoints. Defaults to `required_scopes`
- from your TokenVerifier if not specified.
+ The complete set of scopes clients are allowed to request — the full set of
+ available scopes (a superset of `required_scopes`). These are advertised to
+ clients through the `/.well-known` endpoints and enforced at Dynamic Client
+ Registration. Defaults to `required_scopes` from your TokenVerifier if not
+ specified.
@@ -259,10 +283,11 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
- Secret used to sign FastMCP JWT tokens issued to clients. Accepts any string or bytes - will be derived into a proper 32-byte cryptographic key using HKDF.
+ Secret used to sign FastMCP JWT tokens issued to clients. How the key is derived depends on what you pass:
- **Default behavior (`None`):**
- Derives a 32-byte key using PBKDF2 from the upstream client secret.
+ - **`bytes`** are used as-is, with no stretching, so supply at least 32 bytes of high-entropy key material. With the default file-backed client storage, the bytes must also decode as UTF-8; use `secrets.token_urlsafe(32).encode()` instead of raw `secrets.token_bytes()`, or configure `client_storage` explicitly.
+ - **A string** is stretched into a 32-byte key with PBKDF2 (1,000,000 iterations), since a supplied string may be low-entropy. Strings shorter than 12 characters also log a warning.
+ - **`None`** (the default) derives a 32-byte key from the upstream client secret using HKDF.
**For production:**
Provide an explicit secret (e.g., from environment variable) to use a fixed key instead of the key derived from the upstream client secret. This allows you to manage keys securely in cloud environments, allows keys to work across multiple instances, and allows you to rotate keys without losing client registrations.
@@ -281,14 +306,22 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
-
- Whether to require user consent before authorizing MCP clients. When enabled (default), users see a consent screen that displays which client is requesting access, preventing [confused deputy attacks](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#confused-deputy-problem) by ensuring users explicitly approve new clients.
+
+ Consent screen behavior for authorization requests. The consent page displays which client is requesting access, defending against [confused deputy and AS-in-the-middle attacks](#confused-deputy-attacks) by requiring explicit user approval.
- **Default behavior (True):**
- Users see a consent screen on first authorization. Consent choices are remembered via signed cookies, so users only need to approve each client once. This protects against malicious clients impersonating the user.
+ **`True` (default) — always prompt:**
+ Users see the consent screen on every authorization. Strongest protection against AS-in-the-middle attacks where a malicious MCP server redirects the victim's browser into a legitimate proxy and relies on a previously-remembered approval to silently complete the flow.
- **Disabling consent (False):**
- Authorization proceeds directly to the upstream provider without user confirmation. Only use this for local development or testing environments where the security trade-off is acceptable.
+ **`"remember"` — silent consent on return:**
+ Users see the consent screen on first authorization; subsequent flows from the same browser for the same `(client_id, redirect_uri)` are silently approved via a signed cookie. Cross-site navigations (detected via `Sec-Fetch-Site`) fall back to the prompt. `Sec-Fetch-Site` is a browser-level heuristic rather than a protocol guarantee: an attacker who finds a way to initiate a non-cross-site navigation (XSS on a sibling origin, a same-site redirect chain, etc.) can reach the silent-consent path. `True` does not depend on this signal. See [Confused Deputy Attacks](#confused-deputy-attacks) for the underlying attack class.
+
+ **`"external"` — externally managed:**
+ Follows the same authorization path as `False`: FastMCP skips its consent page and associated browser-binding protections, then redirects directly to the upstream provider. The difference is logging. `False` emits a security warning, while `"external"` suppresses that warning as an explicit acknowledgment that the operator is enforcing equivalent consent and transaction-binding protections elsewhere. FastMCP does not provide or verify those external protections.
+
+ Ordinary upstream OAuth consent is generally not equivalent. It typically authorizes FastMCP's shared upstream application without identifying the downstream MCP client or binding approval to that client's transaction. Use `"external"` only when your surrounding authorization system supplies those protections.
+
+ **`False` — disable entirely:**
+ Authorization proceeds directly to the upstream provider without any consent UI. Logs a security warning. Only for local development or testing.
```python
# Development/testing only - skip consent screen
@@ -296,10 +329,16 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
...,
require_authorization_consent=False # ⚠️ Security warning: only for local/testing
)
+
+ # Convenience mode - silent consent on return visits (less safe than True)
+ auth = OAuthProxy(
+ ...,
+ require_authorization_consent="remember",
+ )
```
- Disabling consent removes an important security layer. Only disable for local development or testing environments where you fully control all connecting clients.
+ Both `False` and `"external"` disable FastMCP's consent and browser-binding protections. `False` warns about this configuration; `"external"` suppresses the warning because it is an operator acknowledgment that equivalent protections exist elsewhere. Prefer the default `True` unless you own that external authorization flow.
@@ -384,7 +423,7 @@ auth = OAuthProxy(
)
```
-The proxy also automatically forwards RFC 8707 `resource` parameters from MCP clients to upstream providers that support them.
+The proxy also forwards RFC 8707 `resource` parameters from MCP clients to upstream providers that support them. This is enabled by default via the `forward_resource` parameter. Disable it for providers that reject unknown parameters.
## OAuth Flow
@@ -482,7 +521,31 @@ This architecture also prevents [token passthrough](#token-passthrough) — see
**Token expiry alignment:**
-FastMCP token lifetimes match the upstream token lifetimes. When the upstream token expires, the FastMCP token also expires, maintaining consistent security boundaries.
+By default, FastMCP token lifetimes match the upstream token lifetimes. When the upstream token expires, the FastMCP token also expires, maintaining consistent security boundaries.
+
+**Extending the FastMCP token lifetime:**
+
+Some upstream providers issue short-lived access tokens (5–60 minutes is common). Because the FastMCP token is a reference into the proxy's storage rather than the upstream credential itself, its client-facing lifetime can be longer than the upstream token's without weakening security: every request re-validates the upstream token and transparently refreshes it when it has expired, so a revoked or genuinely expired upstream session still fails validation and forces re-authentication.
+
+This matters for MCP clients that don't refresh gracefully. For example, [`mcp-remote`](https://github.com/geelen/mcp-remote) (used by Claude Desktop) has known issues handling access-token expiry, so a short upstream lifetime can push users through a full OAuth flow after every idle period. Set `fastmcp_access_token_expiry_seconds` to decouple the FastMCP token lifetime from the upstream `expires_in`:
+
+```python
+from fastmcp.server.auth import OAuthProxy
+
+auth = OAuthProxy(
+ upstream_authorization_endpoint="https://provider.com/oauth/authorize",
+ upstream_token_endpoint="https://provider.com/oauth/token",
+ upstream_client_id="your-client-id",
+ upstream_client_secret="your-client-secret",
+ token_verifier=token_verifier,
+ base_url="https://your-server.com",
+ fastmcp_access_token_expiry_seconds=60 * 60 * 24, # 24 hours
+)
+```
+
+The upstream token's real expiry is preserved internally to drive transparent refresh; only the FastMCP-issued token lives longer. This parameter is available on every provider built on the OAuth proxy (`GitHubProvider`, `GoogleProvider`, `AzureProvider`, and the rest).
+
+Extending the lifetime only works when the upstream provider issues a refresh token, since that's what lets the proxy renew the access token behind the scenes. When the upstream provides no refresh token, the FastMCP token lifetime is capped at the upstream `expires_in` — issuing a longer-lived token would claim a validity the proxy can't honor.
**Refresh tokens:**
@@ -504,7 +567,7 @@ auth = OAuthProxy(
### Redirect URI Validation
-While the OAuth proxy accepts all redirect URIs by default (for DCR compatibility), you can restrict which clients can connect by specifying allowed patterns:
+By default, the OAuth proxy validates DCR clients against their registered redirect URIs while allowing loopback ports to vary for MCP compatibility. Unsafe browser schemes such as `javascript:` are always rejected. You can restrict which clients can connect at the server level by specifying allowed patterns:
```python
# Allow only localhost clients (common for development)
@@ -529,6 +592,27 @@ auth = OAuthProxy(
Check your server logs for "Client registered with redirect_uri" messages to identify what URLs your clients use.
+### Application Type (Web vs. Native)
+
+
+
+During Dynamic Client Registration, a client may declare an `application_type` (per RFC 7591 and SEP-837) that governs which redirect URIs it is allowed to use. The OAuth proxy honors this field both at registration and when authorizing a redirect.
+
+`application_type` defaults to `"native"` because MCP clients typically run locally and register loopback callbacks. Clients that omit the field keep the permissive behavior described above. A client that explicitly registers as `"web"` is held to the stricter browser-app rules.
+
+Loopback covers the whole reserved range in both the address and name forms: every address in `127.0.0.0/8`, `::1`, and — per RFC 6761 — the name `localhost` along with any subdomain of it, such as `app.localhost`. The absolute (trailing-dot) spellings `localhost.` and `127.0.0.1.` are treated identically. A name that merely contains `localhost` as a label of a registrable domain, like `localhost.example.com`, is an ordinary public host and is not treated as loopback.
+
+| `application_type` | Allowed redirect URIs |
+| ------------------ | --------------------- |
+| `"native"` (default) | `https` URLs; app and private-use schemes (`vscode://callback`, `com.example.app:/callback`, `myapp://callback`, `urn:ietf:wg:oauth:2.0:oob`); and loopback `http` (`http://127.0.0.1`, any address in `127.0.0.0/8`, `http://localhost`, subdomains such as `http://app.localhost`, `http://[::1]`, any port) |
+| `"web"` | `https` on a non-loopback host only |
+
+Web clients must register a non-loopback `https` callback — that is the restriction SEP-837 asks for, and a web client that registers no redirect URI at all is refused, since it could never complete an authorization. Native clients keep the full range of schemes their platforms use; the only new limit is that cleartext `http` must target a loopback host, per RFC 8252 §7.3.
+
+Both application types always reject unsafe browser schemes (`javascript:`, `data:`, `file:`, `vbscript:`). FastMCP does not otherwise filter a native client's scheme: there is no reliable way to tell an app-dispatch scheme from a network transport, since the IANA registry lists `vscode:` alongside `coap:` and `smb:`, so any such filter would reject callbacks that real MCP clients depend on.
+
+A redirect URI that violates the declared type is refused during registration with a `RegistrationError` (`invalid_redirect_uri`). For example, a `"web"` client that registers `http://localhost:12345/callback` is rejected, since web clients must use a non-loopback `https` callback. Configure remote, browser-based clients as `application_type="web"` and give them an `https` callback URL.
+
## CIMD Support
@@ -597,6 +681,83 @@ auth = OAuthProxy(
)
```
+## Identity Assertion (SEP-990)
+
+
+
+
+Identity assertion is a beta feature. The API may change in a future release.
+
+
+Identity assertion enables an enterprise "on-behalf-of" flow. A corporate identity provider (Okta, Microsoft Entra, etc.) issues an *ID-JAG* — a signed JWT that asserts an employee's identity to a specific MCP authorization server. The client presents that ID-JAG at the token endpoint using the RFC 7523 `jwt-bearer` grant, and the proxy validates it and mints a short-lived access token for the asserted user. No refresh token is issued: the identity provider controls session lifetime, and the client re-exchanges a fresh ID-JAG when its access token expires. This lets a workforce reach your MCP server with corporate-managed identity and centralized revocation, without each user running an interactive browser login.
+
+To enable it, pass an `IdentityAssertion` configuration listing the issuers you trust:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import OAuthProxy, IdentityAssertion
+
+auth = OAuthProxy(
+ upstream_authorization_endpoint="https://accounts.example.com/authorize",
+ upstream_token_endpoint="https://accounts.example.com/token",
+ upstream_client_id="your-client-id",
+ upstream_client_secret="your-client-secret",
+ base_url="https://your-server.com",
+ identity_assertion=IdentityAssertion(
+ trusted_issuers=["https://login.acme-corp.com"],
+ ),
+)
+
+mcp = FastMCP("Internal API", auth=auth)
+
+@mcp.tool
+def whoami() -> str:
+ from fastmcp.server.dependencies import get_access_token
+
+ token = get_access_token()
+ return token.subject or "unknown"
+```
+
+When identity assertion is configured, the proxy advertises the `urn:ietf:params:oauth:grant-type:jwt-bearer` grant type and the `urn:ietf:params:oauth:grant-profile:id-jag` grant profile in its authorization server metadata, so compatible clients can discover the capability. When it is not configured, the grant is rejected as unsupported.
+
+### How Validation Works
+
+For each ID-JAG presented at the token endpoint, the proxy checks that:
+
+- the JOSE header `typ` is `oauth-id-jag+jwt`;
+- the `iss` claim is one of the configured `trusted_issuers`;
+- the signature verifies against the issuer's published keys;
+- the `aud` claim identifies this authorization server — configure your identity provider to mint assertions whose `aud` is the `issuer` value published at `/.well-known/oauth-authorization-server`, which is your `issuer_url` when you set one and your `base_url` otherwise;
+- the signed `client_id` claim matches the client presenting the assertion — an assertion the IdP minted for one client cannot be redeemed by another;
+- the signed `resource` claim names this server — an assertion minted for a different MCP server behind the same IdP is rejected;
+- `exp` (and `iat`/`nbf`, when present) place the assertion within a short lifetime and its validity window; and
+- the `jti` has not been seen before, preventing replay.
+
+The issuer's signing keys are discovered automatically via OIDC (`{issuer}/.well-known/openid-configuration`). For issuers that do not publish a discovery document, provide the JWKS URI explicitly per issuer:
+
+```python
+identity_assertion=IdentityAssertion(
+ trusted_issuers=["https://login.acme-corp.com"],
+ jwks_uris={"https://login.acme-corp.com": "https://login.acme-corp.com/keys"},
+)
+```
+
+Verification assumes `RS256` unless the issuer signs with another algorithm, in which case set `algorithm` explicitly (any asymmetric JWS algorithm — `RS*`, `PS*`, or `ES*` — since assertions are verified against a published JWKS, not a shared secret). When trusted issuers use different algorithms, override per issuer with `algorithms`, keyed the same way as `jwks_uris`:
+
+```python
+identity_assertion=IdentityAssertion(
+ trusted_issuers=["https://login.acme-corp.com", "https://sso.other-corp.com"],
+ algorithm="ES256",
+ algorithms={"https://sso.other-corp.com": "RS256"},
+)
+```
+
+The subject asserted in the ID-JAG flows into the normal FastMCP auth context. Tools read it through `get_access_token()` exactly as they would for any other token, because the proxy issues the access token through its own token factory.
+
+
+Replay protection is per-process. Each server process tracks seen `jti` values in memory, so a horizontally-scaled deployment running multiple workers or replicas could accept the same assertion once per process. The same applies to revocation of ID-JAG access tokens: they are self-contained, so revocation is tracked in-process until the token's (short, 5-minute default) natural expiry. For deployments that require strict single-use enforcement across replicas, configure sticky routing so a given client's requests reach the same process, or place a shared store in front of the token endpoint. This mirrors the posture of CIMD `private_key_jwt` replay protection, which is also per-process.
+
+
## Security
### Key and Storage Management
@@ -605,8 +766,7 @@ auth = OAuthProxy(
The OAuth proxy requires cryptographic keys for JWT signing and storage encryption, plus persistent storage to maintain valid tokens across server restarts.
**Default behavior (appropriate for development only):**
-- **Mac/Windows**: FastMCP automatically generates keys and stores them in your system keyring. Storage defaults to disk. Tokens survive server restarts. This is **only** suitable for development and local testing.
-- **Linux**: Keys are ephemeral (random salt at startup). Storage defaults to memory. Tokens become invalid on server restart.
+On every platform, FastMCP deterministically derives `jwt_signing_key` from `upstream_client_secret` using HKDF, and storage defaults to an encrypted disk store in your platform's data directory (derived from `platformdirs`). Tokens survive server restarts as long as `upstream_client_secret` doesn't change. This is **only** suitable for development and local testing.
**For production:**
Configure the following parameters together: provide a unique `jwt_signing_key` (for signing FastMCP JWTs), and a shared `client_storage` backend (for storing tokens). Both are required for production deployments. Use a network-accessible storage backend like Redis or DynamoDB rather than local disk storage. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** (see the `client_storage` parameter documentation above for examples). The keys accept any secret string and derive proper cryptographic keys using HKDF. See [OAuth Token Security](/deployment/http#oauth-token-security) and [Storage Backends](/servers/storage-backends) for complete production setup.
@@ -623,7 +783,7 @@ The OAuth proxy works by bridging DCR clients to traditional auth providers, whi
FastMCP's OAuth proxy defends against confused deputy attacks with two layers of protection:
-**Consent screen.** Before any authorization happens, you see a consent page showing the client's details, redirect URI, and requested scopes. This gives you the opportunity to review and deny suspicious requests. Once you approve a client, it's remembered so you don't see the consent page again for that client. The consent mechanism is implemented with CSRF tokens and cryptographically signed cookies to prevent tampering.
+**Consent screen.** Before any authorization happens, you see a consent page showing the client's details, redirect URI, and requested scopes. This gives you the opportunity to review and deny suspicious requests. By default (`require_authorization_consent=True`), the page is shown on every flow, which is the strongest protection. Setting `require_authorization_consent="remember"` approves previously-approved `(client_id, redirect_uri)` pairs silently on return visits, trading some protection for UX (see below). The consent mechanism is implemented with CSRF tokens and cryptographically signed cookies to prevent tampering.

@@ -631,6 +791,12 @@ The consent page automatically displays your server's name, icon, and website UR
**Browser-session binding.** When you approve consent (or when a previously-approved client auto-approves), the proxy sets a cryptographically signed cookie that binds your browser session to the authorization flow. When the identity provider redirects back to the proxy's callback, the proxy verifies that this cookie is present and matches the expected transaction. A different browser — such as a victim who was sent the authorization URL by an attacker — won't have this cookie, and the callback will be rejected with a 403 error. This prevents the attack even when the identity provider skips the consent page for previously-authorized applications.
+#### AS-in-the-middle variant
+
+A related attack works even with browser-session binding in place: a malicious MCP server advertises its own authorization server, which redirects the victim's browser into the legitimate proxy's `/authorize` endpoint. Because the victim's browser carries both the prior-approval cookie and the newly-issued session-binding cookie throughout, both layers pass. The defense is the consent prompt itself: if consent is shown (`require_authorization_consent=True`), the victim sees the benign MCP server's name on the consent page — which doesn't match the malicious server they thought they were connecting to — and can deny.
+
+`require_authorization_consent="remember"` adds a `Sec-Fetch-Site` check to keep this path safe for legitimate return flows (the attack navigation lands as `cross-site` and falls back to the prompt), but this is a browser-level heuristic. For the strongest defense, leave `require_authorization_consent=True`.
+
**Learn more:**
- [MCP Security Best Practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#confused-deputy-problem) - Official specification guidance
- [Confused Deputy Attacks Explained](https://den.dev/blog/mcp-confused-deputy-api-management/) - Detailed walkthrough by Den Delimarsky
diff --git a/docs/servers/auth/oidc-proxy.mdx b/docs/servers/auth/oidc-proxy.mdx
index 86661bc16..be4bcb22d 100644
--- a/docs/servers/auth/oidc-proxy.mdx
+++ b/docs/servers/auth/oidc-proxy.mdx
@@ -70,14 +70,21 @@ mcp = FastMCP(name="My Server", auth=auth)
Client ID from your registered OAuth application
-
- Client secret from your registered OAuth application
+
+ Client secret from your registered OAuth application. Optional for PKCE public
+ clients. When omitted, `jwt_signing_key` must be provided.
Public URL of your FastMCP server (e.g., `https://your-server.com`)
+
+ Optional public base URL for the protected resource metadata and token audience.
+
+ Use this when your OAuth callbacks and operational endpoints need to live under one public URL, but the protected MCP resource should be advertised under another. FastMCP will still append the MCP mount path (for example, `/mcp`) to this base URL.
+
+
Strict flag for configuration validation. When True, requires all OIDC
mandatory fields.
@@ -117,14 +124,23 @@ mcp = FastMCP(name="My Server", auth=auth)
List of allowed redirect URI patterns for MCP clients. Patterns support wildcards (e.g., `"http://localhost:*"`, `"https://*.example.com/*"`).
- - `None` (default): All redirect URIs allowed (for MCP/DCR compatibility)
+ - `None` (default): DCR clients use registered redirect URIs, with loopback ports allowed to vary for MCP compatibility. Unsafe browser schemes such as `javascript:`, `data:`, `file:`, and `vbscript:` are rejected.
- Empty list `[]`: No redirect URIs allowed
- Custom list: Only matching patterns allowed
-These patterns apply to MCP client loopback redirects, NOT the upstream OAuth app redirect URI.
+These patterns apply to MCP client loopback redirects. Configure the upstream OAuth app redirect URI separately with `redirect_path`.
+
+ The complete set of scopes clients are allowed to request — the full set of
+ available scopes (a superset of `required_scopes`). These are advertised to
+ clients through the `/.well-known` endpoints (as `scopes_supported`) and
+ enforced at Dynamic Client Registration: a client registering with a scope
+ outside this set is rejected. Defaults to `required_scopes` from your token
+ verifier if not specified.
+
+
Token endpoint authentication method for the upstream OAuth server. Controls how the proxy authenticates when exchanging authorization codes and refresh tokens with the upstream provider.
- `"client_secret_basic"`: Send credentials in Authorization header (most common)
@@ -139,14 +155,13 @@ Set this if your provider requires a specific authentication method and the defa
- Secret used to sign FastMCP JWT tokens issued to clients. Accepts any string or bytes - will be derived into a proper 32-byte cryptographic key using HKDF.
+ Secret used to sign FastMCP JWT tokens issued to clients. **`bytes`** are used as-is, with no stretching, so supply at least 32 bytes of high-entropy key material. With the default file-backed client storage, the bytes must also decode as UTF-8; use `secrets.token_urlsafe(32).encode()` instead of raw `secrets.token_bytes()`, or configure `client_storage` explicitly. **A string** is stretched into a 32-byte key with PBKDF2 (1,000,000 iterations), since a supplied string may be low-entropy.
**Default behavior (`None`):**
- - **Mac/Windows**: Auto-managed via system keyring. Keys are generated once and persisted, surviving server restarts with zero configuration. Keys are automatically derived from server attributes, so this approach, while convenient, is **only** suitable for development and local testing. For production, you must provide an explicit secret.
- - **Linux**: Ephemeral (random salt at startup). Tokens become invalid on server restart, triggering client re-authentication.
+ The key is deterministically derived from `client_secret` using HKDF, on every platform. Because the derivation is deterministic, the same key is produced across restarts as long as `client_secret` doesn't change, so tokens remain valid without any extra configuration. This convenience makes it **only** suitable for development and local testing.
**For production:**
- Provide an explicit secret (e.g., from environment variable) to use a fixed key instead of the auto-generated one.
+ Provide an explicit `jwt_signing_key` (e.g., from an environment variable) rather than relying on the auto-derived key.
@@ -155,10 +170,9 @@ Set this if your provider requires a specific authentication method and the defa
Storage backend for persisting OAuth client registrations and upstream tokens.
**Default behavior:**
- - **Mac/Windows**: Encrypted DiskStore in your platform's data directory (derived from `platformdirs`)
- - **Linux**: MemoryStore (ephemeral - clients lost on restart)
+ Encrypted disk store in your platform's data directory (derived from `platformdirs`), on every platform including Linux. The encryption key is itself derived from `jwt_signing_key`.
- By default on Mac/Windows, clients are automatically persisted to encrypted disk storage, allowing them to survive server restarts as long as the filesystem remains accessible. This means MCP clients only need to register once and can reconnect seamlessly. On Linux where keyring isn't available, ephemeral storage is used to match the ephemeral key strategy.
+ By default, clients are automatically persisted to encrypted disk storage, allowing them to survive server restarts as long as the filesystem remains accessible. This means MCP clients only need to register once and can reconnect seamlessly.
For production deployments with multiple servers or cloud deployments, use a network-accessible storage backend rather than local disk storage. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest.** See [Storage Backends](/servers/storage-backends) for available options.
@@ -191,8 +205,8 @@ auth = OIDCProxy(
-
- Whether to require user consent before authorizing MCP clients. When enabled (default), users see a consent screen that displays which client is requesting access. See [OAuthProxy documentation](/servers/auth/oauth-proxy#confused-deputy-attacks) for details on confused deputy attack protection.
+
+ Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (same authorization path as `False`, but the warning is suppressed because the operator asserts that equivalent protections are enforced externally), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs.
diff --git a/docs/servers/auth/remote-oauth.mdx b/docs/servers/auth/remote-oauth.mdx
index 7c94a937f..e2fd14b98 100644
--- a/docs/servers/auth/remote-oauth.mdx
+++ b/docs/servers/auth/remote-oauth.mdx
@@ -116,8 +116,6 @@ auth = RemoteAuthProvider(
token_verifier=token_verifier,
authorization_servers=[AnyHttpUrl("https://auth.yourcompany.com")],
base_url="https://api.yourcompany.com", # Your server base URL
- # Optional: restrict allowed client redirect URIs (defaults to all for DCR compatibility)
- allowed_client_redirect_uris=["http://localhost:*", "http://127.0.0.1:*"]
)
mcp = FastMCP(name="Company API", auth=auth)
@@ -147,7 +145,7 @@ When not set, `scopes_supported` defaults to the token verifier's `required_scop
You can extend `RemoteAuthProvider` to add additional endpoints beyond the standard OAuth protected resource metadata. These don't have to be OAuth-specific - you can add any endpoints your authentication integration requires.
```python
-import httpx
+import httpx2
from starlette.responses import JSONResponse
from starlette.routing import Route
@@ -173,7 +171,7 @@ class CompanyAuthProvider(RemoteAuthProvider):
# Add authorization server metadata forwarding for client convenience
async def authorization_server_metadata(request):
- async with httpx.AsyncClient() as client:
+ async with httpx2.AsyncClient() as client:
response = await client.get(
"https://auth.yourcompany.com/.well-known/oauth-authorization-server"
)
@@ -216,13 +214,7 @@ WorkOS's support for Dynamic Client Registration makes it particularly well-suit
## Client Redirect URI Security
-`RemoteAuthProvider` also supports the `allowed_client_redirect_uris` parameter for controlling which redirect URIs are accepted from MCP clients during DCR:
-
-- `None` (default): All redirect URIs allowed (for DCR compatibility)
-- Custom list: Specify allowed patterns with wildcard support
-- Empty list `[]`: No redirect URIs allowed
-
-This provides defense-in-depth even though DCR providers typically validate redirect URIs themselves.
+Redirect URIs are validated by the DCR provider itself, since it owns the registration flow. To constrain them from the FastMCP side, use [`OAuthProxy`](/servers/auth/oauth-proxy), whose `allowed_client_redirect_uris` parameter accepts a list of allowed patterns with wildcard support.
## Implementation Considerations
@@ -237,4 +229,4 @@ Remote OAuth integration requires careful attention to several technical details
**Scope Management**: Map token scopes to your application's permission model consistently. Consider how scope changes affect existing tokens and plan for smooth permission updates.
-The complexity of these considerations reinforces why external identity providers are recommended over custom OAuth implementations. Established providers handle these technical details with extensive testing and operational experience.
\ No newline at end of file
+The complexity of these considerations reinforces why external identity providers are recommended over custom OAuth implementations. Established providers handle these technical details with extensive testing and operational experience.
diff --git a/docs/servers/auth/token-verification.mdx b/docs/servers/auth/token-verification.mdx
index a9146135f..9e55640ba 100644
--- a/docs/servers/auth/token-verification.mdx
+++ b/docs/servers/auth/token-verification.mdx
@@ -41,6 +41,8 @@ Token validation must address several security requirements: signature verificat
The challenge in MCP environments is that clients need to obtain valid tokens before making requests, but the MCP protocol doesn't provide built-in discovery mechanisms for token endpoints. Clients must obtain tokens through separate channels or prior configuration.
+On the streamable-HTTP transport, each session is additionally bound to the credential that created it: a request that presents a different credential for an existing `Mcp-Session-Id` is rejected with a 404, exactly as if the session did not exist. A leaked session id is therefore useless without the original credential. Session identity is the `(client_id, issuer, subject)` triple your verifier populates.
+
## TokenVerifier Class
@@ -78,6 +80,19 @@ This configuration creates a server that validates JWTs issued by `auth.yourcomp
The `issuer` parameter ensures tokens come from your trusted authentication system, while `audience` validation prevents tokens intended for other services from being accepted by your MCP server.
+`JWTVerifier` accepts RSA (`RS*` and `PS*`), ECDSA (`ES*`), and Edwards-curve (`Ed25519` and `Ed448`) signatures from JWKS endpoints. Set `algorithm` when your issuer does not use the default `RS256`:
+
+```python
+verifier = JWTVerifier(
+ jwks_uri="https://auth.yourcompany.com/.well-known/jwks.json",
+ issuer="https://auth.yourcompany.com",
+ audience="mcp-production-api",
+ algorithm="Ed25519",
+)
+```
+
+The legacy `EdDSA` identifier is also accepted for compatibility with identity providers that have not yet adopted the fully specified identifiers from RFC 9864.
+
### Symmetric Key Verification (HMAC)
Symmetric key verification uses a shared secret for both signing and validation, making it ideal for internal microservices and trusted environments where the same secret can be securely distributed to both token issuers and validators.
@@ -119,7 +134,7 @@ The parameter is named `public_key` for backwards compatibility, but when using
### Static Public Key Verification
-Static public key verification works when you have a fixed RSA or ECDSA signing key and don't need automatic key rotation. This approach is primarily useful for development environments or controlled deployments where JWKS endpoints aren't available.
+Static public key verification works when you have a fixed RSA, ECDSA, or EdDSA signing key and don't need automatic key rotation. This approach is primarily useful for development environments or controlled deployments where JWKS endpoints aren't available.
```python
from fastmcp import FastMCP
@@ -139,7 +154,7 @@ verifier = JWTVerifier(
mcp = FastMCP(name="Protected API", auth=verifier)
```
-This configuration validates tokens using a specific RSA or ECDSA public key. The key must correspond to the private key used by your token issuer. While less flexible than JWKS endpoints, this approach can be useful in development environments or when testing with fixed keys.
+This configuration validates tokens using a specific RSA, ECDSA, or EdDSA public key. The key must correspond to the private key used by your token issuer. While less flexible than JWKS endpoints, this approach can be useful in development environments or when testing with fixed keys.
## Opaque Token Verification
Many authorization servers issue opaque tokens rather than self-contained JWTs. Opaque tokens are random strings that carry no information themselves - the authorization server maintains their state and validation requires querying the server. FastMCP supports opaque token validation through OAuth 2.0 Token Introspection (RFC 7662).
@@ -325,21 +340,21 @@ This pattern enables comprehensive testing of JWT validation logic without depen
-All token verifiers that make HTTP calls accept an optional `http_client` parameter. This lets you provide your own `httpx.AsyncClient` for connection pooling, custom TLS configuration, or proxy settings.
+All token verifiers that make HTTP calls accept an optional `http_client` parameter. This lets you provide your own `httpx2.AsyncClient` for connection pooling, custom TLS configuration, or proxy settings.
### Connection Pooling
By default, each token verification call creates a fresh HTTP client. Under high load, this means repeated TCP connections and TLS handshakes. Providing a shared client enables connection pooling across calls:
```python
-import httpx
+import httpx2
from fastmcp import FastMCP
from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
# Create a shared client with connection pooling
-http_client = httpx.AsyncClient(
+http_client = httpx2.AsyncClient(
timeout=10,
- limits=httpx.Limits(max_connections=20, max_keepalive_connections=10),
+ limits=httpx2.Limits(max_connections=20, max_keepalive_connections=10),
)
verifier = IntrospectionTokenVerifier(
@@ -376,7 +391,7 @@ from contextlib import asynccontextmanager
from fastmcp import FastMCP
from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
-http_client = httpx.AsyncClient(timeout=10)
+http_client = httpx2.AsyncClient(timeout=10)
verifier = IntrospectionTokenVerifier(
introspection_url="https://auth.example.com/introspect",
@@ -423,4 +438,3 @@ mcp = FastMCP(name="Production API", auth=verifier)
This keeps configuration out of your codebase while maintaining explicit setup.
This approach enables the same codebase to run across development, staging, and production environments with different authentication requirements. Development might use static tokens while production uses JWT verification, all controlled through environment configuration.
-
diff --git a/docs/servers/authorization.mdx b/docs/servers/authorization.mdx
index a48d2a9e8..1ba9a3b0e 100644
--- a/docs/servers/authorization.mdx
+++ b/docs/servers/authorization.mdx
@@ -58,6 +58,75 @@ def read_write_operation() -> str:
return "Read/write action completed"
```
+### require_roles
+
+
+
+Scopes are standardized, so `require_scopes` works the same everywhere. Roles and groups are not part of OIDC, so every identity provider puts them under a different claim. `require_roles` handles the comparison and takes an `extract` callable that tells it where to look.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import require_roles
+
+def keycloak_roles(claims: dict) -> list[str]:
+ return claims["realm_access"]["roles"]
+
+mcp = FastMCP("Role Server")
+
+@mcp.tool(auth=require_roles("admin", extract=keycloak_roles))
+def admin_operation() -> str:
+ """Requires the 'admin' role."""
+ return "Admin action completed"
+
+@mcp.tool(auth=require_roles("admin", "auditor", extract=keycloak_roles))
+def audited_admin_operation() -> str:
+ """Requires both the 'admin' AND 'auditor' roles."""
+ return "Audited admin action"
+```
+
+Multiple roles are required together, matching `require_scopes`. A token whose claims lack the path entirely is denied rather than raising, so the extractor can index directly.
+
+Keeping the claim path at the call site means any provider works, including ones with unusual shapes. Common locations:
+
+| Provider | Extractor |
+| --- | --- |
+| Keycloak | `lambda c: c["realm_access"]["roles"]` |
+| Microsoft Entra | `lambda c: c["roles"]` |
+| AWS Cognito | `lambda c: c["cognito:groups"]` |
+| Auth0 | `lambda c: c["permissions"]` |
+
+Verify the claim against your own tenant before relying on it. Auth0's namespaced custom claims are configured per tenant, and Entra emits `roles` or `groups` depending on the app manifest.
+
+
+`require_roles` cannot signal a scope shortfall, because OAuth has no way to request a role. A role denial surfaces as a plain `AuthorizationError` rather than one of the `insufficient_scope` challenges described in [Signaling Scope Shortfalls](#signaling-scope-shortfalls), and it suppresses any scope shortfall raised alongside it — a caller blocked by their role should not be told to go obtain a scope that would not help them. Combining `require_roles` with `require_scopes` is otherwise fine: whenever the role check passes, a scope shortfall is reported as usual.
+
+
+### Checking Other Claims
+
+`require_roles` is a convenience for the common case. `AccessToken.claims` holds every claim from the token, so gating on anything else needs no special API — just an auth check that reads it.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import AuthCheck, AuthContext
+
+mcp = FastMCP("Claim Server")
+
+def require_tenant(tenant_id: str) -> AuthCheck:
+ """Require the token to come from a specific tenant."""
+ def check(ctx: AuthContext) -> bool:
+ if ctx.token is None:
+ return False
+ return ctx.token.claims.get("tid") == tenant_id
+ return check
+
+@mcp.tool(auth=require_tenant("acme"))
+def tenant_operation() -> str:
+ """Only callable by tokens issued for the acme tenant."""
+ return "Tenant action completed"
+```
+
+The same caveat applies: a check like this is opaque, so it suppresses scope disclosure for its siblings.
+
### restrict_tag
Tag-based restrictions apply scope requirements conditionally. If a component has the specified tag, the token must have the required scopes. Components without the tag are unaffected.
@@ -107,7 +176,7 @@ Any callable that accepts `AuthContext` and returns `bool` can serve as an auth
```python
from fastmcp import FastMCP
-from fastmcp.server.auth import AuthContext
+from fastmcp.server.auth import AuthCheck, AuthContext
mcp = FastMCP("Custom Auth Server")
@@ -117,7 +186,7 @@ def require_premium_user(ctx: AuthContext) -> bool:
return False
return ctx.token.claims.get("premium", False) is True
-def require_access_level(minimum_level: int):
+def require_access_level(minimum_level: int) -> AuthCheck:
"""Factory function for level-based authorization."""
def check(ctx: AuthContext) -> bool:
if ctx.token is None:
@@ -168,6 +237,7 @@ Sync and async checks can be freely combined in a list — each check is handled
Auth checks can raise exceptions for explicit denial with custom messages:
- **`AuthorizationError`**: Propagates with its custom message, useful for explaining why access was denied
+- **`InsufficientScopeError`**: A subclass of `AuthorizationError` raised by `AuthMiddleware` when the denial is a missing scope; it [names the scopes the caller needs](#signaling-scope-shortfalls)
- **Other exceptions**: Masked for security (logged internally, treated as denial)
```python
@@ -215,7 +285,7 @@ Component-level `auth` controls both visibility (list filtering) and access (dir
## Server-Level Authorization
-For server-wide authorization enforcement, use `AuthMiddleware`. This middleware applies auth checks globally to all components—filtering list responses and blocking unauthorized execution with explicit `AuthorizationError` responses.
+For server-wide authorization enforcement, use `AuthMiddleware`. This middleware applies auth checks globally to all components—filtering list responses and blocking unauthorized execution with explicit `AuthorizationError` responses. When the denial is specifically a missing scope, the error [names the scopes the caller needs](#signaling-scope-shortfalls).
```python
from fastmcp import FastMCP
@@ -296,6 +366,48 @@ def read_record(id: str) -> str:
return f"Record {id}"
```
+### Signaling Scope Shortfalls
+
+
+
+A denial is more useful when it says what would fix it. When `AuthMiddleware` blocks a call because the token is missing scopes — rather than because some other policy rejected it — it raises `InsufficientScopeError`, which carries the specific scopes the caller needs in its `required_scopes` attribute. An agent that reads the error knows exactly which scopes to re-authorize for, instead of retrying blindly against an opaque refusal.
+
+`InsufficientScopeError` subclasses `AuthorizationError`, so existing handlers that catch `AuthorizationError` keep catching it and nothing about your error handling has to change to adopt this.
+
+Only the scopes the token *lacks* are named, so re-authorizing accumulates permissions rather than replacing them. A caller holding `read` that needs `read` and `write` is told to obtain `write` alone, and keeps `read` through the re-authorization. When several scope requirements fail at once, every unmet scope is reported together — a caller granted them all in one round succeeds on the retry, instead of discovering the next missing scope only after obtaining the first.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.exceptions import InsufficientScopeError
+from fastmcp.server.auth import require_scopes
+from fastmcp.server.middleware import AuthMiddleware
+
+mcp = FastMCP(
+ "Step-Up Server",
+ middleware=[AuthMiddleware(auth=require_scopes("read", "write"))],
+)
+
+@mcp.tool
+def update_record(id: str) -> str:
+ """Requires both 'read' and 'write'."""
+ return f"Updated {id}"
+
+# A token holding only "read" is denied with:
+# InsufficientScopeError(required_scopes=["write"])
+```
+
+This holds across several `AuthMiddleware` instances too, not just several checks within one. In the [tag-based configuration](#tag-based-global-authorization) each middleware contributes its own requirement, and the first to find a shortfall reports the requirements of the others alongside its own — so one re-authorization covers the whole chain rather than one layer at a time.
+
+A shortfall is reported only when the scope requirement is what actually caused the denial. If you [combine checks](#combining-checks) and a non-scope check rejects the request first — a tenant policy, say — the denial stays a plain `AuthorizationError` and names no scopes at all. Disclosing a scope requirement for a component the caller could not reach anyway would leak information about components they are not authorized to see.
+
+That rule also bounds what gets aggregated. Combining requirements only reaches as far down the chain as the request itself would have gone: it stops at the first layer holding a custom check, since whether that layer would admit the caller is unknown until it runs, and running it early would trigger authorization logic the request had not reached yet. Requirements at or beyond that point sit behind an unverified gate and are left out.
+
+So a custom check early in the chain makes the reported set partial, and a caller may need more than one round to satisfy everything. The reported set is complete when the layers ahead are scope-only and conservative otherwise: it may name fewer scopes than the full chain requires, but it never names scopes behind a policy that might reject the caller regardless.
+
+
+This names the missing scopes in the error rather than emitting an HTTP `403` challenge. A per-tool denial is a JSON-RPC error carried inside a `200` response, so there is no HTTP status at that layer to attach a `WWW-Authenticate` header to. Token-level scope failures — where the token does not satisfy the server's own `required_scopes` — are a separate concern handled by the transport middleware, which does return a spec-correct `403` with an `insufficient_scope` challenge.
+
+
## Accessing Tokens in Tools
Tools can access the current authentication token using `get_access_token()` from `fastmcp.server.dependencies`. This enables tools to make decisions based on user identity or permissions beyond simple authorization checks.
@@ -376,9 +488,15 @@ from fastmcp.server.auth import (
AuthContext, # Context with .token, .component
AuthCheck, # Type alias: sync or async Callable[[AuthContext], bool]
require_scopes, # Built-in: requires specific scopes
+ require_roles, # Built-in: requires roles read from token claims
restrict_tag, # Built-in: tag-based scope requirements
run_auth_checks, # Utility: run checks with AND logic
)
+from fastmcp.exceptions import (
+ AuthorizationError, # Denial with a custom message
+ InsufficientScopeError, # Subclass of AuthorizationError; has .required_scopes
+)
+
from fastmcp.server.middleware import AuthMiddleware
```
diff --git a/docs/servers/completions.mdx b/docs/servers/completions.mdx
new file mode 100644
index 000000000..d59a18488
--- /dev/null
+++ b/docs/servers/completions.mdx
@@ -0,0 +1,177 @@
+---
+title: Argument Completion
+sidebarTitle: Completions
+description: Suggest values for prompt arguments and resource template parameters as the user types.
+icon: list-check
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Argument completion lets a server suggest values while a user fills in a prompt argument or a resource template parameter. As the user types, the client sends a `completion/complete` request naming the prompt or template, the argument being completed, and the partial value so far. The server answers with candidate strings, which the client offers as autocomplete suggestions.
+
+This is the server side of the feature. A client requests completions with [`Client.complete()`](/clients/client); this page covers how a server answers.
+
+## Register a completion handler
+
+A server has a single completion handler, registered with the `@mcp.completion` decorator. The handler receives every completion request and switches on which reference and argument is being completed.
+
+```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 ref.name == "write_poem":
+ if argument.name == "theme":
+ options = ["nature", "love", "adventure"]
+ return [o for o in options if o.startswith(argument.value)]
+ return None
+```
+
+The handler is called with three values:
+
+- `ref`: which component is being completed — a `PromptReference` (carrying the prompt `name`) or a `ResourceTemplateReference` (carrying the template `uri`).
+- `argument`: a `CompletionArgument` with the argument `name` and the partial `value` typed so far.
+- `context`: an optional `CompletionContext` carrying the values of arguments the user has already supplied (see [Using already-supplied arguments](#using-already-supplied-arguments)).
+
+Filter your candidates against `argument.value` so the suggestions narrow as the user types. Returning `None` means "I have no suggestions for this reference and argument" — the client receives an empty list, which is the correct answer for a reference the server does not recognize.
+
+
+Registering a completion handler declares the server's completions capability during the handshake. A server with no handler does not advertise the capability, and a client that checks capabilities before calling will skip completion requests entirely. This works the same way on both the handshake and modern protocol eras.
+
+
+## Completing resource template parameters
+
+The same handler answers completion for resource template parameters. A `ResourceTemplateReference` identifies the template by its URI template, and `argument.name` is the parameter being completed.
+
+```python
+from fastmcp import FastMCP
+from mcp.types import ResourceTemplateReference
+
+mcp = FastMCP("Completion Server")
+
+REPOS = ["fastmcp", "prefect", "marvin"]
+
+
+@mcp.resource("github://{owner}/{repo}")
+def repo_readme(owner: str, repo: str) -> str:
+ return f"README for {owner}/{repo}"
+
+
+@mcp.completion
+def complete(ref, argument, context):
+ if isinstance(ref, ResourceTemplateReference):
+ if ref.uri == "github://{owner}/{repo}" and argument.name == "repo":
+ return [r for r in REPOS if r.startswith(argument.value)]
+ return None
+```
+
+Because a single handler answers for every prompt and template, a server that completes several components branches on `ref` first, then on `argument.name`. Grouping the branches by reference keeps the handler readable as it grows.
+
+## Using already-supplied arguments
+
+Completions often depend on values the user has already entered. A repository suggestion, for example, depends on which owner was chosen. The client sends those resolved values in the completion context, and the handler reads them from `context.arguments`.
+
+```python
+from fastmcp import FastMCP
+from mcp.types import ResourceTemplateReference
+
+mcp = FastMCP("Completion Server")
+
+REPOS_BY_OWNER = {
+ "prefecthq": ["fastmcp", "prefect", "marvin"],
+ "python": ["cpython", "mypy"],
+}
+
+
+@mcp.resource("github://{owner}/{repo}")
+def repo_readme(owner: str, repo: str) -> str:
+ return f"README for {owner}/{repo}"
+
+
+@mcp.completion
+def complete(ref, argument, context):
+ if isinstance(ref, ResourceTemplateReference) and argument.name == "repo":
+ owner = context.arguments.get("owner") if context and context.arguments else None
+ repos = REPOS_BY_OWNER.get(owner or "", [])
+ return [r for r in repos if r.startswith(argument.value)]
+ return None
+```
+
+Here the suggestions for `repo` are scoped to the `owner` the user already selected. The context is only present once at least one argument has been resolved, so guard against `context` being `None`.
+
+## Returning results
+
+A handler may return any of three things:
+
+- A list of strings — the simplest form, wrapped into a completion response automatically.
+- `None` — treated as an empty completion, for references and arguments the handler does not recognize.
+- A `Completion` object — when you want to include pagination hints alongside the values.
+
+The MCP protocol caps a single response at 100 values. When more candidates exist, return a `Completion` and set `total` (how many candidates match in all) and `has_more` (whether values were truncated) so the client can indicate that the list is partial.
+
+```python
+from fastmcp import FastMCP
+from mcp.types import Completion, PromptReference
+
+mcp = FastMCP("Completion Server")
+
+ALL_CITIES = ["Paris", "Prague", "Portland", "Phoenix", "Perth"]
+
+
+@mcp.prompt
+def pick_city(city: str) -> str:
+ return f"Tell me about {city}"
+
+
+def search_cities(prefix: str) -> list[str]:
+ # A real lookup might return thousands of matches; ALL_CITIES stands in.
+ return [c for c in ALL_CITIES if c.startswith(prefix)]
+
+
+@mcp.completion
+def complete(ref, argument, context):
+ if isinstance(ref, PromptReference) and argument.name == "city":
+ matches = search_cities(argument.value)
+ return Completion(
+ values=matches[:100],
+ total=len(matches),
+ has_more=len(matches) > 100,
+ )
+ return None
+```
+
+## Accessing the request context
+
+A completion handler may be sync or async, and it can reach the active request through FastMCP's dependency functions the same way any handler does. Use [`get_context()`](/servers/context) to access session information, authentication, or server state while computing suggestions.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.dependencies import get_context
+from mcp.types import PromptReference
+
+mcp = FastMCP("Completion Server")
+
+
+@mcp.completion
+async def complete(ref, argument, context):
+ ctx = get_context()
+ await ctx.debug(f"Completing {argument.name!r} for {ref}")
+ ...
+```
+
+## Authorization
+
+Completion runs behind the server's connection-level authentication: an unauthenticated client never reaches the handler. It is independent of per-component `auth=`, though. FastMCP does not resolve the referenced prompt or resource template, so a completion request is not filtered by that component's visibility the way `prompts/get` or a resource read is — the single handler answers for whatever reference the client names.
+
+A completion response carries only candidate strings for one argument, never component content or schema, so this exposes nothing about a hidden component on its own. If a handler computes candidates that should themselves be restricted — matching a prompt hidden from unauthorized callers, say — check the auth context inside the handler (via [`get_context()`](/servers/context)) and return `None` when the caller is not permitted.
diff --git a/docs/servers/composition.mdx b/docs/servers/composition.mdx
index 42523a5cd..9175c2438 100644
--- a/docs/servers/composition.mdx
+++ b/docs/servers/composition.mdx
@@ -199,23 +199,28 @@ If low latency is critical, consider implementing caching strategies or limiting
Custom HTTP routes defined with `@server.custom_route()` are also forwarded when mounting:
```python
+from fastmcp import FastMCP
+from starlette.requests import Request
+from starlette.responses import JSONResponse, Response
+
subserver = FastMCP("Sub")
@subserver.custom_route("/health", methods=["GET"])
-async def health_check():
- return {"status": "ok"}
+async def health_check(request: Request) -> Response:
+ return JSONResponse({"status": "ok"})
main = FastMCP("Main")
main.mount(subserver, namespace="sub")
-# /health is now accessible through main's HTTP app
+# /health is now accessible through main's HTTP app.
+# Custom route paths are not namespaced by mount(namespace=...).
```
## Conflict Resolution
-When mounting multiple servers with the same namespace (or no namespace), the **most recently mounted** server takes precedence for conflicting component names:
+When mounting multiple servers with the same namespace (or no namespace), FastMCP queries all mounted providers for a requested component and returns the highest matching version. If two unversioned components (or two equal versions) use the same identifier, the provider registered first wins.
```python
server_a = FastMCP("A")
@@ -233,5 +238,5 @@ main = FastMCP("Main")
main.mount(server_a)
main.mount(server_b)
-# shared_tool returns "From B" (most recently mounted)
+# shared_tool returns "From A" (first mounted, same unversioned key)
```
diff --git a/docs/servers/context.mdx b/docs/servers/context.mdx
index a10161baf..667ce9764 100644
--- a/docs/servers/context.mdx
+++ b/docs/servers/context.mdx
@@ -21,9 +21,8 @@ The `Context` object provides a clean interface to access MCP features within yo
- **Progress Reporting**: Update the client on the progress of long-running operations
- **Resource Access**: List and read data from resources registered with the server
- **Prompt Access**: List and retrieve prompts registered with the server
-- **LLM Sampling**: Request the client's LLM to generate text based on provided messages
- **User Elicitation**: Request structured input from users during tool execution
-- **Session State**: Store data that persists across requests within an MCP session
+- **Request State**: Pass values and non-serializable resources between middleware and handlers within a request (for state that persists across requests, see [Session State](/servers/sessions))
- **Session Visibility**: [Control which components are visible](/servers/visibility#per-session-visibility) to the current session
- **Request Information**: Access metadata about the current request
- **Server Access**: When needed, access the underlying FastMCP server instance
@@ -71,7 +70,7 @@ async def data_analysis_request(dataset: str, ctx: Context = CurrentContext()) -
- Dependency parameters are automatically excluded from the MCP schema—clients never see them.
- Context methods are async, so your function usually needs to be async as well.
-- **Each MCP request receives a new context object.** Context is scoped to a single request; state or data set in one request will not be available in subsequent requests.
+- **Each MCP request receives a new context object.** State set with `ctx.set_state()` is scoped to that request and is not available in subsequent ones. To persist state across requests, use [Session State](/servers/sessions).
- Context is only available during a request; attempting to use context methods outside a request will raise errors.
### Legacy Type-Hint Injection
@@ -152,18 +151,9 @@ if result.action == "accept":
See [User Elicitation](/servers/elicitation) for detailed examples and supported response types.
-### LLM Sampling
-
-
-
-Request the client's LLM to generate text based on provided messages, useful for leveraging AI capabilities within your tools.
-
-```python
-response = await ctx.sample("Analyze this data", temperature=0.7)
-```
-
-See [LLM Sampling](/servers/sampling) for comprehensive usage and advanced techniques.
+### Sampling and Roots
+Neither capability has a `Context` method. Both used to *push* a request into a live client connection, which the modern MCP protocol has no channel to carry, so a tool now asks for them by returning the request and reading the answer on the next round — the same [guard pattern](/servers/elicitation#sampling-and-roots) elicitation uses on modern connections. That route is the natural one for roots; for generation, [call an LLM directly from your server](/servers/sampling).
### Progress Reporting
@@ -184,13 +174,13 @@ List and read data from resources registered with your FastMCP server, allowing
resources = await ctx.list_resources()
# Read a specific resource
-content_list = await ctx.read_resource("resource://config")
-content = content_list[0].content
+resource_result = await ctx.read_resource("resource://config")
+content = resource_result.contents[0].content
```
**Method signatures:**
-- **`ctx.list_resources() -> list[MCPResource]`**: Returns list of all available resources
-- **`ctx.read_resource(uri: str | AnyUrl) -> list[ReadResourceContents]`**: Returns a list of resource content parts
+- **`ctx.list_resources() -> list[mcp.types.Resource]`**: Returns list of all available resources
+- **`ctx.read_resource(uri: str | AnyUrl) -> ResourceResult`**: Returns a `ResourceResult` whose `.contents` list contains the resource content parts
### Prompt Access
@@ -211,81 +201,68 @@ messages = result.messages
- **`ctx.list_prompts() -> list[MCPPrompt]`**: Returns list of all available prompts
- **`ctx.get_prompt(name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult`**: Get a specific prompt with optional arguments
-### Session State
+### Request State
-Store data that persists across multiple requests within the same MCP session. Session state is automatically keyed by the client's session, ensuring isolation between different clients.
+Request state carries values *within a single request*, across the middleware → handler pipeline. A request runs through any middleware you've added and then the handler — separate functions that don't share a stack frame, so a plain local variable can't pass anything between them. `ctx.set_state` / `ctx.get_state` is that channel.
+
+The common case is a middleware that resolves something once and every tool reads it, rather than each tool recomputing it:
```python
from fastmcp import FastMCP, Context
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+
+mcp = FastMCP("app")
+
+
+class Enrich(Middleware):
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
+ await context.fastmcp_context.set_state("caller", "alice")
+ return await call_next(context)
+
+
+mcp.add_middleware(Enrich())
-mcp = FastMCP("stateful-app")
@mcp.tool
-async def increment_counter(ctx: Context) -> int:
- """Increment a counter that persists across tool calls."""
- count = await ctx.get_state("counter") or 0
- await ctx.set_state("counter", count + 1)
- return count + 1
-
-@mcp.tool
-async def get_counter(ctx: Context) -> int:
- """Get the current counter value."""
- return await ctx.get_state("counter") or 0
+async def whoami(ctx: Context) -> str:
+ return await ctx.get_state("caller") or "unknown"
```
-Each client session has its own isolated state—two different clients calling `increment_counter` will each have their own counter.
+The state is scoped to the one request and discarded when it returns. State is also inherited by mounted children, so a value a parent middleware sets is visible to a mounted server's tools within the same request.
**Method signatures:**
-- **`await ctx.set_state(key, value, *, serializable=True)`**: Store a value in session state
-- **`await ctx.get_state(key)`**: Retrieve a value (returns None if not found)
-- **`await ctx.delete_state(key)`**: Remove a value from session state
-
-State methods are async and require `await`. State expires after 1 day to prevent unbounded memory growth.
-
+- **`await ctx.set_state(key, value, *, serializable=True)`** — store a value
+- **`await ctx.get_state(key)`** — retrieve a value (returns `None` if not set)
+- **`await ctx.delete_state(key)`** — remove a value
-#### Non-Serializable Values
+#### Non-serializable resources
-By default, state values must be JSON-serializable (dicts, lists, strings, numbers, etc.) so they can be persisted across requests. For non-serializable values like HTTP clients or database connections, pass `serializable=False`:
+The most useful thing request state holds is objects you *can't* persist — a database connection or an HTTP client that a middleware or the [lifespan](/servers/lifespan) opens and a handler uses. Pass `serializable=False`:
```python
@mcp.tool
async def my_tool(ctx: Context) -> str:
- # This object can't be JSON-serialized
client = SomeHTTPClient(base_url="https://api.example.com")
await ctx.set_state("client", client, serializable=False)
- # Retrieve it later in the same request
client = await ctx.get_state("client")
return await client.fetch("/data")
```
-Values stored with `serializable=False` only live for the current MCP request (a single tool call, resource read, or prompt render). They will not be available in subsequent requests within the session.
+A `serializable=False` value lives on the request context for the current call only. It is inherently request-scoped — a live connection can't be serialized and stored — which is exactly why it belongs here rather than in a persistent store.
-#### Custom Storage Backends
+#### Persisting across requests
-By default, session state uses an in-memory store suitable for single-server deployments. For distributed or serverless deployments, provide a custom storage backend:
-
-```python
-from key_value.aio.stores.redis import RedisStore
-
-# Use Redis for distributed state
-mcp = FastMCP("distributed-app", session_state_store=RedisStore(...))
-```
-
-Any backend compatible with the [py-key-value-aio](https://github.com/strawgate/py-key-value) `AsyncKeyValue` protocol works. See [Storage Backends](/servers/storage-backends) for more options including Redis, DynamoDB, and MongoDB.
-
-#### State During Initialization
-
-State set during `on_initialize` middleware persists to subsequent tool calls when using the same session object (STDIO, SSE, single-server HTTP). For distributed/serverless HTTP deployments where different machines handle init and tool calls, state is isolated by the `mcp-session-id` header.
+Request state does not survive from one call to the next. When you need a cart, a conversation, or any state that outlives a single request, use [Session State](/servers/sessions) — it stores server-side, keyed by the authenticated user, and works on every protocol era. (On session-based, handshake-era connections, serializable request state also persists across the session, but Session State is the deliberate, cross-era way to do it.)
### Session Visibility
-Tools can customize which components are visible to their current session using `ctx.enable_components()`, `ctx.disable_components()`, and `ctx.reset_visibility()`. These methods apply visibility rules that affect only the calling session, leaving other sessions unchanged. See [Per-Session Visibility](/servers/visibility#per-session-visibility) for complete documentation, filter criteria, and patterns like namespace activation.
+Tools can customize which components are visible to their current session using `ctx.enable_components()`, `ctx.disable_components()`, and `ctx.reset_visibility()`. They accept the same filters as the server-level methods, so `names={"search"}` targets a component by name and `tags` targets a group. These methods apply visibility rules that affect only the calling session, leaving other sessions unchanged. See [Per-Session Visibility](/servers/visibility#per-session-visibility) for complete documentation, filter criteria, and patterns like namespace activation.
### Change Notifications
@@ -294,14 +271,18 @@ Tools can customize which components are visible to their current session using
FastMCP automatically sends list change notifications when components (such as tools, resources, or prompts) are added, removed, enabled, or disabled. In rare cases where you need to manually trigger these notifications, you can use the context's notification methods:
```python
-import mcp.types
+from mcp.types import (
+ PromptListChangedNotification,
+ ResourceListChangedNotification,
+ ToolListChangedNotification,
+)
@mcp.tool
async def custom_tool_management(ctx: Context) -> str:
"""Example of manual notification after custom tool changes."""
- await ctx.send_notification(mcp.types.ToolListChangedNotification())
- await ctx.send_notification(mcp.types.ResourceListChangedNotification())
- await ctx.send_notification(mcp.types.PromptListChangedNotification())
+ await ctx.send_notification(ToolListChangedNotification())
+ await ctx.send_notification(ResourceListChangedNotification())
+ await ctx.send_notification(PromptListChangedNotification())
return "Notifications sent"
```
diff --git a/docs/servers/dependency-injection.mdx b/docs/servers/dependency-injection.mdx
index d13f43952..c9ada083b 100644
--- a/docs/servers/dependency-injection.mdx
+++ b/docs/servers/dependency-injection.mdx
@@ -10,7 +10,7 @@ import { VersionBadge } from "/snippets/version-badge.mdx";
FastMCP uses dependency injection to provide runtime values to your tools, resources, and prompts. Instead of passing context through every layer of your code, you declare what you need as parameter defaults—FastMCP resolves them automatically when your function runs.
-The dependency injection system is powered by [Docket](https://github.com/chrisguidry/docket) and its dependency system [uncalled-for](https://github.com/chrisguidry/uncalled-for). Core DI features like `Depends()` and `CurrentContext()` work without installing Docket. For background tasks and advanced task-related dependencies, install `fastmcp[tasks]`. For comprehensive coverage of dependency patterns, see the [Docket dependency documentation](https://docket.lol/en/latest/dependency-injection/).
+The dependency injection system is powered by [uncalled-for](https://github.com/chrisguidry/uncalled-for), the same dependency engine used by Docket. Core DI features like `Depends()` and `CurrentContext()` work without installing Docket. Background task execution and task-specific dependencies such as `CurrentDocket()` and `CurrentWorker()` require `fastmcp[tasks]`. For comprehensive coverage of dependency patterns, see the [Docket dependency documentation](https://docket.lol/en/latest/dependency-injection/).
Dependency parameters are automatically excluded from the MCP schema—clients never see them as callable parameters. This separation keeps your function signatures clean while giving you access to the runtime context you need.
@@ -160,14 +160,19 @@ def get_client_ip() -> str:
```
-Both raise `RuntimeError` when called outside an HTTP context (e.g., STDIO transport). Use HTTP Headers if you need graceful fallback.
+Both raise `RuntimeError` when called outside an HTTP context (e.g., STDIO transport,
+or inside a background task — there is no live request object to reconstruct there).
+Use HTTP Headers below if you need graceful fallback, including inside background tasks.
### HTTP Headers
-Access HTTP headers with graceful fallback—returns an empty dictionary when no HTTP request is available, making it safe for code that might run over any transport.
+Access HTTP headers with graceful fallback. When a background task originates from an
+HTTP request, FastMCP restores the originating headers inside the worker. When no HTTP
+request is available, this returns an empty dictionary, making it safe for code that
+might run over any transport.
**Dependency injection:** Use `CurrentHeaders()`:
@@ -272,11 +277,12 @@ Common claims vary by identity provider:
-For background task execution, FastMCP provides dependencies that integrate with [Docket](https://github.com/chrisguidry/docket). These require installing `fastmcp[tasks]`.
+For background task execution, FastMCP provides dependencies that integrate with [Docket](https://github.com/chrisguidry/docket). `CurrentDocket()` and `CurrentWorker()` require installing `fastmcp[tasks]`; `Progress()` also works during immediate foreground execution with an in-memory tracker, and delegates to Docket progress when a Docket worker context is active.
```python
from fastmcp import FastMCP
-from fastmcp.dependencies import CurrentDocket, CurrentWorker, Progress
+from fastmcp.dependencies import Progress
+from fastmcp_tasks.dependencies import CurrentDocket, CurrentWorker
mcp = FastMCP("Task Demo")
@@ -303,7 +309,7 @@ async def long_running_task(
- **`Progress()`**: Track task progress with atomic updates
-Task dependencies require `pip install 'fastmcp[tasks]'`. They're only available within task-enabled components (`task=True`). For comprehensive task patterns, see the [Docket documentation](https://chrisguidry.github.io/docket/dependencies/).
+`CurrentDocket()` and `CurrentWorker()` require `pip install 'fastmcp[tasks]'`. They resolve once the server lifespan has initialized Docket, which happens as soon as any component on the server is task-enabled — so regular foreground tools, resources, and prompts can inject them too, not only task-enabled components. `Progress()` can be injected anywhere regardless, though cross-process task progress requires Docket. For comprehensive task patterns, see the [Docket documentation](https://chrisguidry.github.io/docket/dependencies/).
## Custom Dependencies
diff --git a/docs/servers/elicitation.mdx b/docs/servers/elicitation.mdx
index 600a3d2fe..eacde0b60 100644
--- a/docs/servers/elicitation.mdx
+++ b/docs/servers/elicitation.mdx
@@ -1,7 +1,7 @@
---
title: User Elicitation
sidebarTitle: Elicitation
-description: Request structured input from users during tool execution through the MCP context.
+description: Ask users for input while a tool is running, on both the handshake and modern protocols.
icon: message-question
---
@@ -9,9 +9,9 @@ import { VersionBadge } from '/snippets/version-badge.mdx'
-User elicitation allows MCP servers to request structured input from users during tool execution. Instead of requiring all inputs upfront, tools can interactively ask for missing parameters, clarification, or additional context as needed.
+User elicitation allows MCP servers to request input from users during tool execution. Instead of requiring all inputs upfront, tools can interactively ask for missing parameters, clarification, or additional context as needed.
-Elicitation enables tools to pause execution and request specific information from users:
+Elicitation enables tools to request specific information from users mid-task:
- **Missing parameters**: Ask for required information not provided initially
- **Clarification requests**: Get user confirmation or choices for ambiguous scenarios
@@ -20,9 +20,18 @@ Elicitation enables tools to pause execution and request specific information fr
For example, a file management tool might ask "Which directory should I create?" or a data analysis tool might request "What date range should I analyze?"
-## Overview
+## Which approach to use
-Use the `ctx.elicit()` method within any tool function to request user input. Specify the message to display and the type of response you expect.
+Elicitation reaches the user two different ways, depending on the protocol era the connection negotiated:
+
+- **On handshake-era connections (≤ 2025-11-25)**, a running tool calls [`ctx.elicit()`](#requesting-input-on-handshake-connections). The tool pauses mid-execution, the server sends a request over the session back-channel, and the tool resumes with the answer. This is the original elicitation API and the rest of this page's first half covers it in full.
+- **On the modern protocol (2026-07-28)**, that back-channel is gone — server-initiated requests were removed from the wire (SEP-2577), so a tool cannot issue a request mid-execution and block on the answer. Instead a tool asks for input by *returning* a description of what it needs; each round completes normally and the client issues a new call with the answer attached. This is the [guard pattern](#elicitation-on-the-modern-protocol), covered in the second half.
+
+The era gate is strict: `ctx.elicit()` only works on handshake connections, and the guard pattern only works on modern ones. A tool that returns a guard result on a handshake connection — or calls `ctx.elicit()` on a modern one — raises a clear era error rather than failing obscurely. A server that serves both eras may need both paths; branch on `ctx.request_context.protocol_version` to pick the right one. `fastmcp.Client` drives whichever the connection negotiated automatically.
+
+## Requesting input on handshake connections
+
+Use the `ctx.elicit()` method within any tool function to request user input on a handshake-era connection. Specify the message to display and the type of response you expect.
```python
from fastmcp import FastMCP, Context
@@ -156,16 +165,38 @@ async def pick_a_boolean(ctx: Context) -> str:
```
-### No Response
+#### Customizing the Field Label
-Sometimes, the goal of an elicitation is to simply get a user to approve or reject an action. Pass `None` as the response type to indicate that no data is expected. The `data` field will be `None` when the user accepts.
+
+
+When FastMCP wraps a scalar, `Literal`, `Enum`, or one of the constrained-option shorthands, the wrapper's `value` property is labelled `"Value"` by default — and some clients (including VS Code) render that label directly in the UI. Pass `response_title` and `response_description` to override it:
+
+```python
+@mcp.tool
+async def confirm_purchase(ctx: Context) -> str:
+ result = await ctx.elicit(
+ "Buy 1x Baguette?",
+ response_type=bool,
+ response_title="Confirm purchase",
+ response_description="Approve this transaction?",
+ )
+ if result.action == "accept":
+ return "Purchased" if result.data else "Declined"
+ return "No response"
+```
+
+These arguments only apply when FastMCP is adding the wrapper. For structured responses (`BaseModel`, dataclass, `TypedDict`), set the metadata on the individual fields via `Field(title=..., description=...)` — passing `response_title` or `response_description` alongside a model type raises `TypeError`.
+
+### Confirmations
+
+`response_type` is required. When all you want is a yes/no answer, ask for a `bool` rather than an empty schema — an empty schema gives the client nothing to render, and some clients show an empty, non-functional form.
```python
@mcp.tool
async def approve_action(ctx: Context) -> str:
- result = await ctx.elicit("Approve this action?", response_type=None)
+ result = await ctx.elicit("Approve this action?", response_type=bool)
- if result.action == "accept":
+ if result.action == "accept" and result.data:
return do_action()
else:
raise ValueError("Action rejected")
@@ -355,3 +386,248 @@ async def create_task(ctx: Context) -> str:
```
Default values are supported for strings, integers, numbers, booleans, and enums.
+
+## Elicitation on the modern protocol
+
+
+
+The modern protocol (2026-07-28) removes the server-initiated back-channel that `ctx.elicit()` depends on (SEP-2577), so a running tool has no way to reach the user mid-execution. Elicitation reaches the user a different way: a tool asks for input by *returning* a description of what it needs. That return value completes the call normally — the result just happens to be an `InputRequiredResult` describing a request rather than a final answer. The client fulfils the request and issues a **new** tool call with the answer attached, and the tool runs again from the top, sees the answer, and either asks for the next thing or returns its final result.
+
+Every round is a complete, independent request→response cycle: the tool holds no state between rounds, and nothing on the server stays alive waiting between them. That makes elicitation work on stateless, serverless, and load-balanced deployments where no two rounds are guaranteed to land on the same worker. A booking tool can ask for a destination, then a date, then confirm, across as many rounds as the work requires, without keeping a connection or a server-side session alive in between.
+
+
+This pattern requires an MCP **2026-07-28** connection. The `InputRequiredResult` result type does not exist on earlier protocol versions; a tool that returns one on a handshake-era connection raises a clear error (see [Protocol requirements](#protocol-requirements)). On those connections, use [`ctx.elicit()`](#requesting-input-on-handshake-connections) instead.
+
+
+### How it works
+
+A tool that asks for input this way is a **guard**: each round it re-runs from the top, checks whether the answers it needs are present, and either asks for more or proceeds. Each of those rounds is an ordinary tool call that runs the full request path — middleware chain included — and returns a result like any other; the framework does not hold the call open between rounds. It inspects two request-scoped properties on the `Context` to decide what to do:
+
+- `ctx.input_responses` — the client's answers to what you asked on a previous round. It is `None` on the very first round (nothing has been asked yet) and a mapping of answers on later rounds.
+- `ctx.request_state` — a small opaque string you can carry from one round to the next. It is `None` on the first round and echoes back whatever you last put in `InputRequiredResult.request_state`.
+
+To ask for input, return an `InputRequiredResult` whose `input_requests` map describes the requests to run — most commonly an elicitation. Each request has a key; the client's answer comes back under the same key in `ctx.input_responses`.
+
+The following tool books a flight across three rounds: it asks for a destination, then asks for a date (carrying the destination forward), then confirms the booking.
+
+```python
+from fastmcp import FastMCP, Context
+from mcp.types import InputRequiredResult, ElicitRequest, ElicitRequestFormParams
+
+mcp = FastMCP("Booking Server")
+
+
+def ask(key: str, message: str, field: str, request_state: str | None = None) -> InputRequiredResult:
+ """Build an InputRequiredResult that elicits a single text field."""
+ params = ElicitRequestFormParams(
+ message=message,
+ requested_schema={
+ "type": "object",
+ "properties": {field: {"type": "string"}},
+ "required": [field],
+ },
+ )
+ elicitation = ElicitRequest(method="elicitation/create", params=params)
+ return InputRequiredResult(
+ result_type="input_required",
+ input_requests={key: elicitation},
+ request_state=request_state,
+ )
+
+
+@mcp.tool
+async def book_flight(ctx: Context) -> str | InputRequiredResult:
+ responses = ctx.input_responses
+
+ if responses is None:
+ return ask("destination", "Where would you like to fly?", "destination")
+
+ if "destination" in responses:
+ destination = responses["destination"].content["destination"]
+ return ask(
+ "date",
+ f"When would you like to fly to {destination}?",
+ "date",
+ request_state=f"dest={destination}",
+ )
+
+ destination = ctx.request_state.split("=", 1)[1]
+ date = responses["date"].content["date"]
+ return f"Booked a flight to {destination} on {date}"
+```
+
+The tool runs three times for one logical call. On the first run `ctx.input_responses` is `None`, so it asks for a destination. On the second run the destination is present, so it asks for a date and stashes the destination in `request_state`. On the third run the date is present, so it reads the destination back out of `ctx.request_state` and returns the booking. Each round is a fresh execution — the tool holds no state of its own between rounds; everything it needs travels on the request.
+
+### Reading answers
+
+Each value in `ctx.input_responses` is the client's result for one request, keyed by the key you gave it. For an elicitation, that is an `ElicitResult` with an `action` and (when accepted) `content`:
+
+```python
+@mcp.tool
+async def confirm(ctx: Context) -> str:
+ responses = ctx.input_responses
+ if responses is None:
+ return ask("ok", "Proceed?", "ok")
+
+ answer = responses["ok"]
+ if answer.action != "accept":
+ return "Cancelled."
+ return f"Proceeding with {answer.content['ok']}"
+```
+
+Always check `answer.action` before reading `answer.content`: a client may **decline** or **cancel**, in which case `content` is absent. A decline is a normal answer, not an error — it is delivered to your tool like any other round so you can handle it deliberately.
+
+### Driving the loop from a client
+
+`fastmcp.Client` drives the whole loop automatically. Point it at a 2026-era connection (`mode="auto"` negotiates one) and give it an elicitation handler; it fulfils each round's requests and retries until the tool returns its final result.
+
+```python
+from fastmcp import Client
+from fastmcp.client.elicitation import ElicitResult
+
+
+async def handler(message, response_type, params, ctx):
+ if "Where" in message:
+ return ElicitResult(action="accept", content=response_type(destination="Paris"))
+ return ElicitResult(action="accept", content=response_type(date="2026-08-01"))
+
+
+async with Client(mcp, mode="auto", elicitation_handler=handler) as client:
+ result = await client.call_tool("book_flight", {})
+ print(result.data) # "Booked a flight to Paris on 2026-08-01"
+```
+
+The client caps the number of rounds it will drive (`input_required_max_rounds`, default 10) so a misbehaving guard cannot loop forever; exceeding it raises an error rather than hanging.
+
+### Carrying state across rounds
+
+The `request_state` you return is **sealed by the framework** before it reaches the wire and **unsealed and verified** before your tool runs again. Your tool only ever mints and reads plaintext — the client receives an opaque token it cannot read, and a token that has been tampered with, has expired, or was minted by a different server is rejected before your tool sees it. You never call any crypto yourself.
+
+Because sealing is automatic, `request_state` is a safe place to carry a computed value forward instead of re-deriving it each round. Keep it small — it round-trips through the client on every leg.
+
+#### Multi-replica deployments
+
+By default each server process seals under a per-process **ephemeral key**. That is correct for single-process deployments (stdio, one HTTP worker), but it means state minted by one process is rejected by another — so a horizontally scaled deployment, where consecutive rounds may land on different replicas, needs a **shared key**.
+
+Give every replica the same key (or key ring) via `request_state_security`:
+
+```python
+import os
+from fastmcp import FastMCP
+from mcp.server.request_state import RequestStateSecurity
+
+mcp = FastMCP(
+ "Booking Server",
+ request_state_security=RequestStateSecurity(keys=[os.environ["REQUEST_STATE_KEY"].encode()]),
+)
+```
+
+Keys must be at least 32 bytes of secret randomness. `keys` is a rotation ring: `keys[0]` seals, and every key in the ring can unseal, so you can rotate without downtime by rolling `keys=[old, new]` → `keys=[new, old]` → `keys=[new]` across deployments. Generate a key with:
+
+```bash
+python -c "import secrets; print(secrets.token_hex(32))"
+```
+
+### Protocol requirements
+
+The `InputRequiredResult` result type is part of MCP **2026-07-28** and does not exist on earlier protocol versions. If a tool returns one on a handshake-era (≤ 2025-11-25) connection, FastMCP rejects the call with a clear error naming the era mismatch rather than letting it fail as a generic invalid result:
+
+```
+Tool 'book_flight' returned an InputRequiredResult to request client input, but
+the multi-round-trip result type (SEP-2322) only exists at MCP 2026-07-28; this
+connection negotiated '2025-11-25'. Use ctx.elicit() for server-initiated input
+on handshake-era connections.
+```
+
+If you need to support both eras, branch on `ctx.request_context.protocol_version`: return an `InputRequiredResult` on modern connections and fall back to [`ctx.elicit()`](#requesting-input-on-handshake-connections) on handshake-era ones.
+
+### Prompts and resources
+
+`InputRequiredResult` is a **result type**, not a tools feature: any request can resolve to one. Prompts, resources, and resource templates ask for input exactly the way tools do — return an `InputRequiredResult`, read `ctx.input_responses` on the next round, and the client re-issues the same `prompts/get` or `resources/read` with the answer attached.
+
+This prompt gathers the context it needs before rendering:
+
+```python
+from fastmcp import FastMCP, Context
+from mcp.types import InputRequiredResult, ElicitRequest, ElicitRequestFormParams
+
+mcp = FastMCP("Reporting Server")
+
+ask_for_quarter = InputRequiredResult(
+ result_type="input_required",
+ input_requests={
+ "quarter": ElicitRequest(
+ method="elicitation/create",
+ params=ElicitRequestFormParams(
+ message="Which quarter should the summary cover?",
+ requested_schema={
+ "type": "object",
+ "properties": {"quarter": {"type": "string"}},
+ "required": ["quarter"],
+ },
+ ),
+ )
+ },
+)
+
+
+@mcp.prompt
+async def summarize(ctx: Context) -> str | InputRequiredResult:
+ responses = ctx.input_responses
+ if responses is None:
+ return ask_for_quarter
+ quarter = responses["quarter"].content["quarter"]
+ return f"Summarize the {quarter} results."
+```
+
+Resources and resource templates work the same way, with the URI standing in for the tool name:
+
+```python
+@mcp.resource("report://summary")
+async def report(ctx: Context) -> str | InputRequiredResult:
+ responses = ctx.input_responses
+ if responses is None:
+ return ask_for_quarter
+ quarter = responses["quarter"].content["quarter"]
+ return f"Revenue report for {quarter}"
+```
+
+The same protocol requirement applies: returning an `InputRequiredResult` from a prompt or resource needs a 2026-07-28 connection, and FastMCP names the era mismatch if one arrives on an older one. Client-side, `read_resource` and `get_prompt` drive the loop the way `call_tool` does, so a configured elicitation handler answers all three without extra wiring.
+
+### Sampling and roots
+
+Elicitation is the most common request to carry this way, and the map carries the others just as well. A `ListRootsRequest` or a `CreateMessageRequest` sits in `input_requests` exactly as an `ElicitRequest` does, and its answer arrives in `ctx.input_responses` under the same key as a `ListRootsResult` or a `CreateMessageResult`. One map can mix all three, and `fastmcp.Client` answers each from the handlers it already has — `elicitation_handler=`, `roots=`, and `sampling_handler=` — so a tool that asks for a mixture needs no extra client wiring. [Client Roots](/clients/roots) covers what a roots request contains.
+
+Roots and sampling differ in how well they suit the round trip. A server asks for roots once and then has what it needs, so the extra round buys the whole answer. Generation rarely works out that way, because every round is a full request-response cycle and a tool that generates in a loop pays that cost each time — [call an LLM directly from your server](/servers/sampling) unless the point is specifically to use the caller's model.
+
+### Middleware
+
+Because each round is a complete request→response cycle, a multi-round tool call runs the **full middleware chain on every round**. `on_call_tool` fires once per round and `call_next(context)` returns that round's result like any other call — there is no held-open call and no special control flow to account for. Default middleware behaves sensibly with no changes: logging logs each round, timing times each round, and error-handling middleware does not fire on an asking round — an ask is a legitimate result, not an error.
+
+An asking round returns an `InputRequiredToolResult` (a `ToolResult` subclass); the final round returns an ordinary `ToolResult`. Middleware that needs to treat the two differently identifies an ask with an `isinstance` check, and tells an initial round from a continuation round by inspecting `ctx.input_responses` (`None` on the first round, present once the client has answered):
+
+```python
+from fastmcp.server.middleware import Middleware
+from fastmcp.tools import InputRequiredToolResult
+
+
+class GuardAwareMiddleware(Middleware):
+ async def on_call_tool(self, context, call_next):
+ ctx = context.fastmcp_context
+ # Either signal marks a continuation: a state-only round carries
+ # request_state with no answers, so it retries with input_responses=None.
+ is_continuation = (
+ ctx.input_responses is not None or ctx.request_state is not None
+ )
+
+ result = await call_next(context)
+
+ if isinstance(result, InputRequiredToolResult):
+ ... # this round asked the client for input
+ else:
+ ... # this round returned a final result
+
+ return result
+```
+
+One built-in makes a deliberate exception: the [response caching middleware](/servers/middleware) never stores an `InputRequiredToolResult`, because caching an ask would replay a stale question to a later caller.
diff --git a/docs/servers/extensions.mdx b/docs/servers/extensions.mdx
new file mode 100644
index 000000000..de81d2251
--- /dev/null
+++ b/docs/servers/extensions.mdx
@@ -0,0 +1,166 @@
+---
+title: Server Extensions
+sidebarTitle: Extensions
+description: Add negotiated protocol features to a server without forking the framework.
+icon: plug
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+An MCP extension is a protocol feature that lives outside the core spec, named by a reverse-DNS identifier and negotiated as a capability. A server advertises the extensions it implements, and a client advertises the ones it understands. That negotiation is per request: a client repeats its extension capabilities in every request's `_meta`, so a handler can always tell whether the caller opted in to this particular call.
+
+Honoring that opt-in is the extension's job, not the framework's. FastMCP advertises your capability and routes your methods, but it does not filter callers for you, so an extension that changes behavior must check before it acts. The [tool-call interceptor](#intercepting-tool-calls) below shows the check.
+
+FastMCP 4 makes extensions a first-class surface. `FastMCP.add_extension()` takes an object that can advertise a capability, serve new request methods, wrap every `tools/call`, and own resources for the life of the server. [Background tasks](/servers/tasks) are built this way, on the same public interface available to you, so a cross-cutting protocol feature becomes a plugin rather than a change to FastMCP itself.
+
+## Writing an extension
+
+Subclass `ServerExtension` and set an `identifier`. The identifier must carry a reverse-DNS prefix in `vendor-prefix/name` form, which FastMCP validates when the class is defined, so a malformed one fails immediately rather than at connection time. Everything else is optional: each contribution method has a working default, and a useful extension often overrides just one.
+
+Registering the extension binds it to the server and advertises its capability. The capability is advertised only while the extension is registered, and registering two extensions with the same identifier is an error.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.extensions import ServerExtension
+
+
+class CallCounterExtension(ServerExtension):
+ identifier = "com.example/call-counter"
+
+ def __init__(self) -> None:
+ self.count = 0
+
+
+mcp = FastMCP("Demo")
+mcp.add_extension(CallCounterExtension())
+```
+
+Register extensions before the server starts. Adding one after the lifespan is running raises, because the extension's own lifespan could no longer run and it would end up silently half-active.
+
+An extension reaches the rest of the server through `self.server`, which is the `FastMCP` instance it was registered on. That is how handlers and interceptors get at the component registry, the request [`Context`](/servers/context), and the authenticated caller.
+
+## Advertising settings
+
+Some extensions need to tell the client how they are configured: a size limit, a supported mode, a flag. Override `settings()` to return a JSON-serializable dict, and it appears on the wire under `capabilities.extensions[identifier]`. The default is an empty dict, which advertises the extension with no settings attached.
+
+```python
+from typing import Any
+
+from fastmcp import FastMCP
+from fastmcp.server.extensions import ServerExtension
+
+
+class UploadExtension(ServerExtension):
+ identifier = "com.example/uploads"
+
+ def settings(self) -> dict[str, Any]:
+ return {"maxBytes": 10_000_000, "resumable": True}
+
+
+mcp = FastMCP("Demo")
+mcp.add_extension(UploadExtension())
+```
+
+A client reads these alongside the capability itself, so it can adapt before making a single call.
+
+## Adding request methods
+
+An extension can serve request methods the core spec does not define. Return a `MethodBinding` from `methods()` naming the wire method, the Pydantic model its params validate against, and the handler to run.
+
+Extension methods are strictly additive. Binding a spec-defined method like `tools/call` raises at construction, because doing so would silently shadow the server's own handler. To change how a core method behaves, use [middleware](/servers/middleware) or the tool-call interceptor below.
+
+The params model should subclass `RequestParams` so `_meta` parses uniformly, and the handler receives the request context and the validated params.
+
+```python
+from typing import Any
+
+from mcp.types import RequestParams
+from fastmcp.server.extensions import MethodBinding, ServerExtension
+
+
+class GetCallCountParams(RequestParams):
+ pass
+
+
+class CallCounterExtension(ServerExtension):
+ identifier = "com.example/call-counter"
+
+ def __init__(self) -> None:
+ self.count = 0
+
+ def methods(self) -> list[MethodBinding]:
+ return [
+ MethodBinding(
+ method="callCounter/get",
+ params_type=GetCallCountParams,
+ handler=self.get_count,
+ )
+ ]
+
+ async def get_count(self, ctx, params: GetCallCountParams) -> dict[str, Any]:
+ return {"count": self.count}
+```
+
+Setting `protocol_versions` on a binding restricts the method to specific wire versions, and a request at any other version is rejected as `METHOD_NOT_FOUND`. Leaving it unset, the default, serves the method on every version.
+
+## Intercepting tool calls
+
+Override `intercept_tool_call()` to wrap every `tools/call` the server handles. The interceptor runs after the FastMCP middleware chain and immediately before the tool body, making it the last gate before execution. Await `call_next()` to let the call proceed, or return a result without awaiting it to short-circuit.
+
+Every registered interceptor runs on every tool call, including calls from clients that never advertised your extension. FastMCP does not gate this for you, so an interceptor that changes what the caller gets back must first confirm the caller opted in. `context.client_extension_settings(identifier)` returns the settings the client declared for this request, or `None` when it declared nothing.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.extensions import ServerExtension
+
+
+class CallCounterExtension(ServerExtension):
+ identifier = "com.example/call-counter"
+
+ def __init__(self) -> None:
+ self.count = 0
+
+ async def intercept_tool_call(self, params, context, call_next):
+ if context.client_extension_settings(self.identifier) is None:
+ return await call_next()
+ self.count += 1
+ return await call_next()
+
+
+mcp = FastMCP("Demo")
+mcp.add_extension(CallCounterExtension())
+```
+
+Counting is harmless either way, so this example passes unaware callers straight through. The check becomes essential the moment an interceptor short-circuits: returning an extension-specific result to a client that never negotiated the extension hands it a shape it has no way to understand. Request methods have the same requirement, and `self.client_settings(ctx)` is the equivalent inside a handler.
+
+`params` holds the validated `tools/call` params, and `context` is the FastMCP `Context`, so the tool being invoked is reachable as `context.fastmcp.get_tool(params.name)` along with auth scope and the server itself. When several extensions intercept, they nest with the first-registered outermost.
+
+Reach for middleware when you want to observe or modify requests generally; reach for an interceptor when the behavior belongs to a negotiated capability and should exist only while that extension is registered.
+
+## Owning resources
+
+An extension that owns something with a lifecycle, such as a connection pool or a background worker, overrides `lifespan()` to return an async context manager. FastMCP enters it with the server's own [lifespan](/servers/lifespan) and exits it on shutdown, so setup and teardown stay with the extension that needs them rather than leaking into the application's startup code.
+
+The lifespan is entered once per runtime tree, at the root. This matters when you compose servers: extensions are served by the server they are registered on, and a mounted child's extensions do not propagate upward. The root server owns the wire, so only root-registered extensions advertise capabilities and answer methods. Register extensions on the server you actually run.
+
+## Client extensions
+
+The client half of an extension is what makes negotiation two-sided. Pass `ClientExtension` instances to `Client(extensions=...)` and each contributes its capability advertisement, its result claims, and its notification bindings to the underlying session. A claimed `call_tool` result is then resolved transparently through the extension that owns it.
+
+When a client needs only to say it understands an extension, without implementing behavior for it, `advertise()` produces an advertise-only entry.
+
+```python
+from fastmcp import Client
+from mcp.client import advertise
+
+client = Client(
+ "https://example.com/mcp",
+ extensions=[advertise("com.example/uploads", {"maxBytes": 10_000_000})],
+)
+```
+
+Advertise only what you genuinely support: the advertisement asserts wire compatibility, and claiming an extension you have not implemented invites the server to use a feature you cannot answer. For anything behavioral, construct the real extension instead.
+
+Claimed result shapes are a modern-protocol feature and stay inert on a legacy connection, so an extension-aware client is still safe to point at an older server.
diff --git a/docs/servers/icons.mdx b/docs/servers/icons.mdx
index c9b558094..065c28471 100644
--- a/docs/servers/icons.mdx
+++ b/docs/servers/icons.mdx
@@ -12,14 +12,14 @@ Icons provide visual representations for your MCP servers and components, helpin
## Icon Format
-Icons use the standard MCP Icon type from the MCP protocol specification. Each icon specifies a source URL or data URI, and optionally includes MIME type and size information.
+Icons use the standard MCP Icon type from the MCP protocol specification. Each icon specifies a source URL or data URI, and optionally includes MIME type, size, and theme information.
```python
from mcp.types import Icon
icon = Icon(
src="https://example.com/icon.png",
- mimeType="image/png",
+ mime_type="image/png",
sizes=["48x48"]
)
```
@@ -27,8 +27,9 @@ icon = Icon(
The fields serve different purposes:
- **src**: URL or data URI pointing to the icon image
-- **mimeType** (optional): MIME type of the image (e.g., "image/png", "image/svg+xml")
+- **mime_type** (optional): MIME type of the image (e.g., "image/png", "image/svg+xml")
- **sizes** (optional): Array of size descriptors (e.g., ["48x48"], ["any"])
+- **theme** (optional): The UI theme the icon is designed for, `"light"` or `"dark"`
## Server Icons
@@ -44,12 +45,12 @@ mcp = FastMCP(
icons=[
Icon(
src="https://weather.example.com/icon-48.png",
- mimeType="image/png",
+ mime_type="image/png",
sizes=["48x48"]
),
Icon(
src="https://weather.example.com/icon-96.png",
- mimeType="image/png",
+ mime_type="image/png",
sizes=["96x96"]
),
]
@@ -110,6 +111,45 @@ def analyze_code(code: str):
return f"Please analyze this code:\n\n{code}"
```
+## Theme Variants
+
+
+
+MCP clients like VS Code and GitHub Desktop render their own interface in either a light or dark theme, and an icon designed for one can be hard to see against the other, such as a dark logo that disappears into a dark sidebar. The `theme` field on `Icon` tells a client which UI theme an icon is designed for, so the client can display the version that stays visible.
+
+Supply two icons with complementary `theme` values and the client picks the one that matches its current appearance:
+
+```python
+from fastmcp import FastMCP
+from mcp.types import Icon
+
+mcp = FastMCP(
+ name="WeatherService",
+ icons=[
+ Icon(src="https://weather.example.com/icon-light.png", theme="light"),
+ Icon(src="https://weather.example.com/icon-dark.png", theme="dark"),
+ ],
+)
+```
+
+The same field works on tools, resources, resource templates, and prompts:
+
+```python
+from mcp.types import Icon
+
+@mcp.tool(
+ icons=[
+ Icon(src="https://example.com/calculator-light.png", theme="light"),
+ Icon(src="https://example.com/calculator-dark.png", theme="dark"),
+ ]
+)
+def calculate_sum(a: int, b: int) -> int:
+ """Add two numbers together."""
+ return a + b
+```
+
+Omitting `theme` means the icon is assumed suitable for any theme. That's the right choice for a single icon with enough contrast to read clearly against both light and dark backgrounds.
+
## Using Data URIs
For small icons or when you want to embed the icon directly without external dependencies, use data URIs. This approach eliminates the need for hosting and ensures the icon is always available.
@@ -121,7 +161,7 @@ from fastmcp.utilities.types import Image
# SVG icon as data URI
svg_icon = Icon(
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6Ii8+PC9zdmc+",
- mimeType="image/svg+xml"
+ mime_type="image/svg+xml"
)
@mcp.tool(icons=[svg_icon])
diff --git a/docs/servers/middleware.mdx b/docs/servers/middleware.mdx
index 40449ae10..1a0aa96bb 100644
--- a/docs/servers/middleware.mdx
+++ b/docs/servers/middleware.mdx
@@ -84,6 +84,8 @@ parent.mount(child, namespace="child")
Requests to `child_tool` flow through the parent's `AuthMiddleware` first, then through the child's `LoggingMiddleware`.
+Middleware-stored state does not automatically cross mount boundaries. If `AuthMiddleware` on the parent calls `ctx.set_state("user_id", ...)`, a tool on the child server calling `ctx.get_state("user_id")` will get `None` — each `FastMCP` instance owns its own session state store. To share state across the mount, either pass the same `session_state_store` to both servers or use `serializable=False` for request-scoped values. See [Session State](/servers/sessions) for details.
+
## Hooks
Rather than processing every message identically, FastMCP provides specialized hooks at different levels of specificity. Multiple hooks fire for a single request, going from general to specific:
@@ -96,6 +98,22 @@ Rather than processing every message identically, FastMCP provides specialized h
When a client calls a tool, the middleware chain processes `on_message` first, then `on_request`, then `on_call_tool`. This hierarchy lets you target exactly the right scope—use `on_message` for logging everything, `on_request` for authentication, and `on_call_tool` for tool-specific behavior.
+### What middleware sees
+
+
+
+Dispatch begins in the SDK's middleware layer — the single point every inbound message passes through. As a result, `on_message`, `on_request`, and `on_notification` observe **every** message a client sends, including the ones that never reach a tool, resource, or prompt handler:
+
+- **Notifications** such as `notifications/cancelled`, `notifications/initialized`, and `notifications/progress` reach `on_message` and `on_notification`.
+- **Cancellations** are observed as a `notifications/cancelled` message. The connection applies the cancellation itself and then hands the notification to your middleware.
+- **Malformed or unroutable requests**—an unknown method, or a `tools/call` whose params fail validation before the tool runs—reach `on_message` and `on_request` as a raised error propagating through `call_next`, so logging and error-handling middleware record them.
+
+The operation hooks (`on_call_tool`, `on_list_tools`, and the rest) fire exactly once per request, and their `call_next` still returns the typed component result—a `ToolResult`, a `list[Tool]`, and so on—so a tool exception propagates through `on_call_tool`, `on_request`, and `on_message` exactly where error, logging, and timing middleware expect it.
+
+#### Multi-round tool calls
+
+A guard tool asks the client for input by returning an `InputRequiredResult` (see [Elicitation on the modern protocol](/servers/elicitation#elicitation-on-the-modern-protocol)). Each round of a multi-round call is a complete request→response cycle that runs the **full middleware chain**: `on_call_tool` fires once per round, and on an asking round `call_next` returns the ask as that round's ordinary result value—an `InputRequiredToolResult`, a `ToolResult` subclass. Nothing is raised and nothing is held open, so default middleware completes normally on every round (logging logs the ask, timing times it, error handling does not fire—an ask is a legitimate result, not an error). Middleware that needs to treat an ask differently identifies it with an `isinstance(result, InputRequiredToolResult)` check; see [Middleware and multi-round calls](/servers/elicitation#middleware) for a worked example.
+
### Hook Signature
Every hook follows the same pattern:
@@ -260,30 +278,54 @@ async def on_list_prompts(self, context: MiddlewareContext, call_next):
-Called when a client connects and initializes the session. This hook cannot modify the initialization response.
+Called when a client connects and initializes the session. Middleware can reject the client before `call_next()` raises an error response, or inspect and modify the `InitializeResult` after `call_next()` returns.
+
+The request params carry the identity the client declared for itself on `client_info`, which makes this the natural place to gate access by client. Note that these fields are snake_case: the MCP wire format spells it `clientInfo`, but the Python model exposes `client_info` and treats the camelCase form as a serialization alias only.
```python
-from mcp import McpError
-from mcp.types import ErrorData
+from fastmcp.exceptions import McpError
async def on_initialize(self, context: MiddlewareContext, call_next):
- client_info = context.message.params.get("clientInfo", {})
- client_name = client_info.get("name", "unknown")
+ client_name = context.message.params.client_info.name
# Reject before call_next to send error to client
if client_name == "blocked-client":
- raise McpError(ErrorData(code=-32000, message="Client not supported"))
+ raise McpError(code=-32000, message="Client not supported")
- await call_next(context)
+ result = await call_next(context)
print(f"Client {client_name} initialized")
+ return result
```
-**Returns:** `None` — The initialization response is handled internally by the MCP protocol.
+**Returns:** `InitializeResult | None` — The value you return is what gets serialized to the client, so modifying the result from `call_next()` changes what the client receives, including fields like `instructions` and `server_info`.
+
+```python
+async def on_initialize(self, context: MiddlewareContext, call_next):
+ result = await call_next(context)
+ result.instructions = "Custom instructions for this client"
+ return result
+```
-Raising `McpError` after `call_next()` will only log the error, not send it to the client. The response has already been sent. Always reject **before** `call_next()`.
+Rejection works only **before** `call_next()`. Raising `McpError` afterward logs the error without sending it — the client still receives a successful initialize response.
+#### on_discover
+
+Called when a modern client negotiates through `server/discover`. Core discovery responses are returned as `DiscoverResult`; extension-owned result types are returned as dictionaries and should be passed through unless the middleware handles that extension.
+
+```python
+from mcp_types import DiscoverResult
+
+async def on_discover(self, context, call_next):
+ result = await call_next(context)
+ if not isinstance(result, DiscoverResult):
+ return result
+ return result.model_copy(update={"instructions": "Custom instructions"})
+```
+
+Fields such as `supported_versions`, `capabilities`, and cache policy should only be changed when the server's public behavior also changes.
+
### Raw Handler
For complete control over all messages, override `__call__` instead of individual hooks:
@@ -324,7 +366,7 @@ async def on_request(self, context: MiddlewareContext, call_next):
return await call_next(context)
```
-For HTTP-specific data (headers, client IP) when using HTTP transports, see [HTTP Requests](/servers/context#http-requests).
+For HTTP-specific data (headers, client IP) when using HTTP transports, see [HTTP Request](/servers/dependency-injection#http-request).
## Built-in Middleware
@@ -352,7 +394,7 @@ mcp.add_middleware(LoggingMiddleware(
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `include_payloads` | `bool` | `False` | Log request/response content |
-| `max_payload_length` | `int` | `500` | Truncate payloads beyond this length |
+| `max_payload_length` | `int` | `1000` | Truncate payloads beyond this length |
| `logger` | `Logger` | module logger | Custom logger instance |
### Timing
@@ -421,11 +463,21 @@ Each settings class accepts:
For persistence or distributed deployments, configure a different storage backend:
```python
+from pathlib import Path
from fastmcp.server.middleware.caching import ResponseCachingMiddleware
-from key_value.aio.stores.disk import DiskStore
+from key_value.aio.stores.filetree import (
+ FileTreeStore,
+ FileTreeV1KeySanitizationStrategy,
+ FileTreeV1CollectionSanitizationStrategy,
+)
+cache_dir = Path("cache")
mcp.add_middleware(ResponseCachingMiddleware(
- cache_storage=DiskStore(directory="cache")
+ cache_storage=FileTreeStore(
+ data_directory=cache_dir,
+ key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(cache_dir),
+ collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(cache_dir),
+ )
))
```
@@ -461,7 +513,7 @@ mcp.add_middleware(RateLimitingMiddleware(
|-----------|------|---------|-------------|
| `max_requests_per_second` | `float` | `10.0` | Sustained request rate |
| `burst_capacity` | `int` | `20` | Maximum burst size |
-| `client_id_func` | `Callable` | `None` | Custom client identification |
+| `get_client_id` | `Callable` | `None` | Custom client identification |
For sliding window rate limiting:
@@ -497,7 +549,7 @@ mcp.add_middleware(ErrorHandlingMiddleware(
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `include_traceback` | `bool` | `False` | Include stack traces in logs |
-| `transform_errors` | `bool` | `False` | Convert exceptions to MCP errors |
+| `transform_errors` | `bool` | `True` | Convert exceptions to MCP errors |
| `error_callback` | `Callable` | `None` | Custom callback on errors |
For automatic retries:
@@ -535,30 +587,6 @@ mcp.add_middleware(PingMiddleware(interval_ms=5000))
The ping task starts on the first message and stops automatically when the session ends. Most useful for stateful HTTP connections; has no effect on stateless connections.
-### Tool Injection
-
-```python
-from fastmcp.server.middleware.tool_injection import (
- ToolInjectionMiddleware,
- PromptToolMiddleware,
- ResourceToolMiddleware
-)
-```
-
-`ToolInjectionMiddleware` dynamically injects tools during request processing. `PromptToolMiddleware` and `ResourceToolMiddleware` provide compatibility layers for clients that cannot list or access prompts and resources directly—they expose those capabilities as tools.
-
-```python
-from fastmcp import FastMCP
-from fastmcp.tools import Tool
-from fastmcp.server.middleware.tool_injection import ToolInjectionMiddleware
-
-def my_tool_fn(a: int, b: int) -> int:
- return a + b
-
-my_tool = Tool.from_function(fn=my_tool_fn, name="my_tool")
-mcp.add_middleware(ToolInjectionMiddleware(tools=[my_tool]))
-```
-
### Response Limiting
@@ -807,7 +835,7 @@ def get_user_data(ctx: Context) -> str:
return f"Data for user: {user_id}"
```
-See [Context State Management](/servers/context#state-management) for details.
+See [Request State](/servers/context#request-state) for details.
### Constructor Parameters
@@ -850,6 +878,82 @@ class ErrorLogger(Middleware):
Catching and not re-raising suppresses the error entirely. Usually you want to log and re-raise.
+### Audit and Event Records
+
+A common need is to emit one structured record per tool call — for audit logs, policy decisions, or offline analysis — without wrapping individual tools or storing raw payloads. `on_call_tool` is the right place: it sees the call start, the resolved `ToolResult` (so it can detect empty or error results), the duration, and can deny the call before it runs.
+
+Use [OpenTelemetry](/servers/telemetry) when the goal is to *export* spans to an observability backend. Reach for a record like this when you want a self-contained, redacted audit trail — or to drive runtime decisions from the result.
+
+```python
+import hashlib
+import json
+from datetime import datetime
+
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+from fastmcp.exceptions import ToolError
+
+
+def _schema_hash(arguments: dict | None) -> str:
+ """Stable hash of the argument shape — detects schema drift without storing values."""
+ shape = sorted(arguments or {})
+ return hashlib.sha256(json.dumps(shape).encode()).hexdigest()[:12]
+
+
+def _redact(arguments: dict | None) -> dict:
+ """Keep keys, drop values — raw inputs stay out of the default path."""
+ return {key: "" for key in (arguments or {})}
+
+
+def _call_id(context: MiddlewareContext) -> str | None:
+ """Request id when an MCP session is active (see Session Availability above)."""
+ ctx = context.fastmcp_context
+ if ctx is not None and ctx.request_context:
+ return ctx.request_id
+ return None
+
+
+class AuditMiddleware(Middleware):
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
+ record = {
+ "tool": context.message.name,
+ "call_id": _call_id(context),
+ "schema_hash": _schema_hash(context.message.arguments),
+ "arguments": _redact(context.message.arguments),
+ "received_at": context.timestamp.isoformat(),
+ }
+
+ try:
+ result = await call_next(context)
+ except Exception as exc:
+ record["status"] = "failed"
+ record["error"] = type(exc).__name__
+ self.emit(record)
+ raise
+
+ empty = not result.content and result.structured_content is None
+ record["status"] = "error" if result.is_error else "empty" if empty else "completed"
+ now = datetime.now(context.timestamp.tzinfo)
+ record["duration_ms"] = round((now - context.timestamp).total_seconds() * 1000, 2)
+ self.emit(record)
+ return result
+
+ def emit(self, record: dict) -> None:
+ # Swap in your sink: structured logger, queue, audit store, etc.
+ print(json.dumps(record))
+```
+
+Each record carries the fields downstream tools tend to need — tool name, call id, input schema hash, redacted arguments, result class (`completed` / `empty` / `error` / `failed`), and duration — while raw inputs and outputs stay out by default.
+
+To make this a policy layer, deny inside the same hook before calling `call_next`:
+
+```python
+async def on_call_tool(self, context: MiddlewareContext, call_next):
+ if not self.is_allowed(context.message.name, context.message.arguments):
+ self.emit({"tool": context.message.name, "status": "denied", "reason": "policy"})
+ raise ToolError("Call blocked by policy")
+ return await call_next(context)
+```
+
### Complete Example
Authentication middleware checking API keys for specific tools:
diff --git a/docs/servers/pagination.mdx b/docs/servers/pagination.mdx
index 97ad2c7a2..b78f09bb1 100644
--- a/docs/servers/pagination.mdx
+++ b/docs/servers/pagination.mdx
@@ -14,7 +14,7 @@ When a server exposes many tools, resources, or prompts, returning them all in a
## Server Configuration
-By default, FastMCP servers return all components in a single response for backward compatibility. To enable pagination, set the `list_page_size` parameter when creating your server. This value determines the maximum number of items returned per page across all list operations.
+By default, FastMCP servers return all components in a single response for backward compatibility. To enable pagination, set the `list_page_size` parameter when creating your server. This value must be a positive integer and determines the maximum number of items returned per page across all list operations.
```python
from fastmcp import FastMCP
@@ -34,7 +34,7 @@ def analyze(data: str) -> dict:
# ... many more tools, resources, prompts
```
-When `list_page_size` is configured, the `tools/list`, `resources/list`, `resources/templates/list`, and `prompts/list` endpoints all paginate their responses. Each response includes a `nextCursor` field when more results exist, which clients use to fetch subsequent pages.
+When `list_page_size` is configured, the `tools/list`, `resources/list`, `resources/templates/list`, and `prompts/list` endpoints all paginate their responses. Each response includes a `next_cursor` field when more results exist, which clients use to fetch subsequent pages.
### Cursor Format
@@ -66,12 +66,12 @@ async with Client(server) as client:
print(f"Page 1: {len(result.tools)} tools")
# Continue fetching while more pages exist
- while result.nextCursor:
- result = await client.list_tools_mcp(cursor=result.nextCursor)
+ while result.next_cursor:
+ result = await client.list_tools_mcp(cursor=result.next_cursor)
print(f"Next page: {len(result.tools)} tools")
```
-The `_mcp` methods return the raw MCP protocol objects, which include both the items and the `nextCursor` for the next page. When `nextCursor` is `None`, you've reached the end of the result set.
+The `_mcp` methods return the raw MCP protocol objects, which include both the items and the `next_cursor` for the next page. When `next_cursor` is `None`, you've reached the end of the result set.
All four list operations support manual pagination:
diff --git a/docs/servers/progress.mdx b/docs/servers/progress.mdx
index 9600a05ce..7dadb73a4 100644
--- a/docs/servers/progress.mdx
+++ b/docs/servers/progress.mdx
@@ -11,7 +11,7 @@ Progress reporting allows MCP tools to notify clients about the progress of long
## Basic Usage
-Use `ctx.report_progress()` to send progress updates to the client. The method accepts a `progress` value representing how much work is complete, and an optional `total` representing the full scope of work.
+Use `ctx.report_progress()` to send progress updates to the client. The method accepts a `progress` value representing how much work is complete, an optional `total` representing the full scope of work, and an optional `message` with human-readable status text.
```python
from fastmcp import FastMCP, Context
@@ -26,7 +26,7 @@ async def process_items(items: list[str], ctx: Context) -> dict:
results = []
for i, item in enumerate(items):
- await ctx.report_progress(progress=i, total=total)
+ await ctx.report_progress(progress=i, total=total, message=f"Processing {item}")
await asyncio.sleep(0.1)
results.append(item.upper())
diff --git a/docs/servers/prompts.mdx b/docs/servers/prompts.mdx
index bdfefc24e..1986a50bf 100644
--- a/docs/servers/prompts.mdx
+++ b/docs/servers/prompts.mdx
@@ -54,7 +54,7 @@ def generate_code_request(language: str, task_description: str) -> list[Message]
* **Parameters:** The function parameters define the inputs needed to generate the prompt.
* **Inferred Metadata:** By default:
* Prompt Name: Taken from the function name (`ask_about_topic`).
- * Prompt Description: Taken from the function's docstring.
+ * Prompt Description: Taken from the summary of the function's docstring. If the docstring includes parameter descriptions (Google, NumPy, or Sphinx style), they populate each prompt argument's description in the MCP protocol (see [Argument Descriptions](#argument-descriptions)).
Functions with `*args` or `**kwargs` are not supported as prompts. This restriction exists because FastMCP needs to generate a complete parameter schema for the MCP protocol, which isn't possible with variable argument lists.
@@ -88,17 +88,13 @@ def data_analysis_prompt(
- Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose
+ Provides the description exposed via MCP. If set, the function's docstring is ignored for the prompt description, though docstring-derived argument descriptions still apply (see [Argument Descriptions](#argument-descriptions)).
A set of strings used to categorize the prompt. These can be used by the server and, in some cases, by clients to filter or group available prompts.
-
- Deprecated in v3.0.0. Use `mcp.enable()` / `mcp.disable()` at the server level instead.
- A boolean to enable or disable the prompt. See [Component Visibility](#component-visibility) for the recommended approach.
-
@@ -201,6 +197,28 @@ Good choices: `list[int]`, `dict[str, str]`, `float`, `bool`
Avoid: Complex Pydantic models, deeply nested structures, custom classes
+### Argument Descriptions
+
+
+
+FastMCP parses your function's docstring to extract the prompt description and per-argument descriptions. Google, NumPy, and Sphinx styles are all supported:
+
+```python
+@mcp.prompt
+def analyze_data(dataset: str, method: str = "summary") -> str:
+ """Generate an analysis prompt for a dataset.
+
+ Args:
+ dataset: URI or identifier of the dataset to analyze.
+ method: Type of analysis to perform (summary, detailed, etc).
+ """
+ return f"Please perform a '{method}' analysis on {dataset}."
+```
+
+The free-form text above the `Args` section — whether a single line or multiple paragraphs — becomes the prompt description, and each argument's docstring entry becomes the description on the corresponding `PromptArgument` in the MCP protocol. Sections like `Returns`, `Raises`, and `Example` are excluded from the description but otherwise ignored.
+
+If an argument already has an explicit description — via `Annotated[x, "..."]` or `Field(description=...)` — that description takes precedence over the docstring. This makes it safe to adopt docstring-based descriptions incrementally: existing annotations keep working, and docstrings fill in the gaps.
+
### Return Values
Prompt functions must return one of these types:
@@ -262,7 +280,7 @@ Message(["item1", "item2"])
`PromptResult` gives you explicit control over prompt responses: multiple messages, roles, and metadata at both the message and result level.
-```python
+```python test="skip"
from fastmcp import FastMCP
from fastmcp.prompts import PromptResult, Message
@@ -292,7 +310,7 @@ return PromptResult("Please help me with this task") # auto-converts to single
Messages to return. Strings are wrapped as a single user Message.
- Optional description of the prompt result. If not provided, defaults to the prompt's docstring.
+ Optional description of this rendered prompt result. Plain `str` and `list[Message | str]` returns inherit the prompt definition description automatically, but an explicit `PromptResult` uses the description you pass here and otherwise leaves it unset.
Result-level metadata, included in the MCP response's `_meta` field. Use this for runtime metadata like categorization, priority, or other client-specific data.
@@ -345,7 +363,7 @@ def internal_prompt() -> str:
return "Internal system prompt"
# Disable specific prompts by key
-mcp.disable(keys={"prompt:internal_prompt"})
+mcp.disable(names={"internal_prompt"})
# Disable prompts by tag
mcp.disable(tags={"internal"})
@@ -412,28 +430,32 @@ def example_prompt() -> str:
# These operations trigger notifications:
mcp.add_prompt(example_prompt) # Sends prompts/list_changed notification
-mcp.disable(keys={"prompt:example_prompt"}) # Sends prompts/list_changed notification
-mcp.enable(keys={"prompt:example_prompt"}) # Sends prompts/list_changed notification
+mcp.disable(names={"example_prompt"}) # Sends prompts/list_changed notification
+mcp.enable(names={"example_prompt"}) # Sends prompts/list_changed notification
```
Notifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications.
Clients can handle these notifications using a [message handler](/clients/notifications) to automatically refresh their prompt lists or update their interfaces.
+## Requesting Input
+
+A prompt can ask the client for information before it renders. On an MCP 2026-07-28 connection, return an `InputRequiredResult` describing what you need; the client answers and re-issues the `prompts/get`, and your function runs again with the answer on `ctx.input_responses`. See [Elicitation](/servers/elicitation#prompts-and-resources) for the full pattern.
+
## Server Behavior
### Duplicate Prompts
-You can configure how the FastMCP server handles attempts to register multiple prompts with the same name. Use the `on_duplicate_prompts` setting during `FastMCP` initialization.
+You can configure how the FastMCP server handles attempts to register the same prompt twice. Identity is the component's type, name, and version together, so a prompt may share a name with a tool, and two versions of one prompt coexist. The `on_duplicate` setting covers every component type, so it applies to prompts alongside tools and resources.
```python
from fastmcp import FastMCP
mcp = FastMCP(
name="PromptServer",
- on_duplicate_prompts="error" # Raise an error if a prompt name is duplicated
+ on_duplicate="error" # Raise an error on an exact duplicate
)
@mcp.prompt
diff --git a/docs/servers/providers/custom.mdx b/docs/servers/providers/custom.mdx
index f5673c683..73d244ff7 100644
--- a/docs/servers/providers/custom.mdx
+++ b/docs/servers/providers/custom.mdx
@@ -186,7 +186,7 @@ from contextlib import asynccontextmanager
from collections.abc import AsyncIterator, Sequence
from fastmcp.server.providers import Provider
from fastmcp.resources import Resource
-import httpx
+import httpx2
class ApiResourceProvider(Provider):
"""Provides resources backed by an external API."""
@@ -199,7 +199,7 @@ class ApiResourceProvider(Provider):
@asynccontextmanager
async def lifespan(self) -> AsyncIterator[None]:
- self.client = httpx.AsyncClient(
+ self.client = httpx2.AsyncClient(
base_url=self.base_url,
headers={"Authorization": f"Bearer {self.api_key}"}
)
diff --git a/docs/servers/providers/filesystem.mdx b/docs/servers/providers/filesystem.mdx
index a5b798faa..353a671d5 100644
--- a/docs/servers/providers/filesystem.mdx
+++ b/docs/servers/providers/filesystem.mdx
@@ -34,13 +34,13 @@ from pathlib import Path
from fastmcp import FastMCP
from fastmcp.server.providers import FileSystemProvider
-mcp = FastMCP("MyServer", providers=[FileSystemProvider(Path(__file__).parent / "mcp")])
+mcp = FastMCP("MyServer", providers=[FileSystemProvider(Path(__file__).parent / "components")])
```
-In your `mcp/` directory, create Python files with decorated functions.
+In your `components/` directory, create Python files with decorated functions.
```python
-# mcp/tools/greet.py
+# components/tools/greet.py
from fastmcp.tools import tool
@tool
@@ -114,7 +114,7 @@ The decorator supports: `uri` (required), `name`, `title`, `description`, `icons
Mark a function as a prompt template.
-```python
+```python test="skip"
from fastmcp.prompts import prompt
@prompt
@@ -139,7 +139,7 @@ The decorator supports: `name`, `title`, `description`, `icons`, `tags`, and `me
The directory structure is purely organizational. The provider recursively scans all `.py` files regardless of which subdirectory they're in. Subdirectories like `tools/`, `resources/`, and `prompts/` are optional conventions that help you organize code.
```
-mcp/
+components/
├── tools/
│ ├── greeting.py # @tool functions
│ └── calculator.py # @tool functions
@@ -152,7 +152,7 @@ mcp/
You can also put all components in a single file or organize by feature rather than type.
```
-mcp/
+components/
├── user_management.py # @tool, @resource, @prompt for users
├── billing.py # @tool, @resource for billing
└── analytics.py # @tool for analytics
@@ -176,9 +176,9 @@ The provider follows these rules when scanning:
If your directory contains an `__init__.py` file, the provider imports files as proper Python package members. This means relative imports work correctly within your components directory.
```python
-# mcp/__init__.py exists
+# components/__init__.py exists
-# mcp/tools/greeting.py
+# components/tools/greeting.py
from ..helpers import format_name # Relative imports work
@tool
@@ -197,7 +197,7 @@ from pathlib import Path
from fastmcp.server.providers import FileSystemProvider
-provider = FileSystemProvider(Path(__file__).parent / "mcp", reload=True)
+provider = FileSystemProvider(Path(__file__).parent / "components", reload=True)
```
With `reload=True`, the provider:
@@ -227,7 +227,7 @@ A complete example is available in the repository at `examples/filesystem-provid
```
examples/filesystem-provider/
├── server.py # Server entry point
-└── mcp/
+└── components/
├── tools/
│ ├── greeting.py # greet, farewell tools
│ └── calculator.py # add, multiply tools
@@ -246,7 +246,7 @@ from fastmcp import FastMCP
from fastmcp.server.providers import FileSystemProvider
provider = FileSystemProvider(
- root=Path(__file__).parent / "mcp",
+ root=Path(__file__).parent / "components",
reload=True,
)
diff --git a/docs/servers/providers/local.mdx b/docs/servers/providers/local.mdx
index 86726655a..147ef7e59 100644
--- a/docs/servers/providers/local.mdx
+++ b/docs/servers/providers/local.mdx
@@ -124,10 +124,10 @@ def get_status() -> str:
mcp.disable(tags={"admin"})
# Or only enable specific tools
-mcp.enable(keys={"tool:get_status"}, only=True)
+mcp.enable(names={"get_status"}, only=True)
```
-See [Visibility](/servers/visibility) for the full documentation on keys, tags, allowlist mode, and provider-level control.
+See [Visibility](/servers/visibility) for the full documentation on names, tags, keys, allowlist mode, and provider-level control.
## Standalone LocalProvider
diff --git a/docs/servers/providers/overview.mdx b/docs/servers/providers/overview.mdx
index d3e3e4e5f..2f23f76a5 100644
--- a/docs/servers/providers/overview.mdx
+++ b/docs/servers/providers/overview.mdx
@@ -57,9 +57,9 @@ Transforms can be added to individual providers (affecting just that source) or
## Provider Order
-When a client requests a tool, FastMCP queries providers in registration order. The first provider that has the tool handles the request.
+When a client requests a component by name or URI, FastMCP queries providers and returns the highest matching version across the providers that have it. For unversioned components, or for components with equal versions, provider registration order is the tie-breaker.
-`LocalProvider` is always first, so your decorator-defined tools take precedence. Additional providers are queried in the order you added them. This means if two providers have a tool with the same name, the first one wins.
+`LocalProvider` is always registered first, so your decorator-defined components take precedence over equal-version components from mounted or proxied providers. Additional providers are registered in the order you add them.
## When to Care About Providers
@@ -70,12 +70,6 @@ When a client requests a tool, FastMCP queries providers in registration order.
- [Proxy a remote server](/servers/providers/proxy) through yours
- [Control visibility state](/servers/visibility) of components
- [Build dynamic sources](/servers/providers/custom) like database-backed tools
+- [Transform components](/servers/transforms/transforms) to namespace, rename, or modify them
-## Next Steps
-
-- [Local](/servers/providers/local) - How decorators work
-- [Mounting](/servers/composition) - Compose servers together
-- [Proxying](/servers/providers/proxy) - Connect to remote servers
-- [Transforms](/servers/transforms/transforms) - Namespace, rename, and modify components
-- [Visibility](/servers/visibility) - Control which components clients can access
-- [Custom](/servers/providers/custom) - Build your own providers
+The decorators you already use are themselves a provider: [`LocalProvider`](/servers/providers/local) is what backs `@mcp.tool` and its siblings.
diff --git a/docs/servers/providers/proxy.mdx b/docs/servers/providers/proxy.mdx
index a2def6892..1ff116bbd 100644
--- a/docs/servers/providers/proxy.mdx
+++ b/docs/servers/providers/proxy.mdx
@@ -49,6 +49,7 @@ if __name__ == "__main__":
```
This gives you:
+
- Safe concurrent request handling
- Automatic forwarding of MCP features (sampling, elicitation, etc.)
- Session isolation to prevent context mixing
@@ -57,6 +58,12 @@ This gives you:
To mount a proxy inside another FastMCP server, see [Mounting External Servers](/servers/composition#mounting-external-servers).
+## Connection Semantics
+
+FastMCP proxies are lazy bridges. Creating the proxy object and starting the local server do not contact the upstream server. During client negotiation, the proxy makes a best-effort request for optional server metadata using the backend client's existing lifecycle and negotiation mode; an unavailable backend does not prevent the client from connecting to the proxy.
+
+Subsequent MCP requests such as `ping`, `tools/list`, `resources/list`, `prompts/list`, tool calls, resource reads, sampling, elicitation, logging, and progress connect to the backend as needed. Component provider failures follow `provider_error_strategy`: the default `"warn"` logs and skips a failed provider, while `"raise"` reports the failure to the client.
+
## Transport Bridging
A common use case is bridging transports between servers:
@@ -161,6 +168,80 @@ backend = ProxyClient(
)
```
+### Tool Results Are Relayed, Not Inspected
+
+
+
+A proxy passes a backend's tool results through untouched, including results that don't match the output schema the backend advertised. Deciding whether a server honored its own contract belongs to the client consuming the result, and that client validates for itself.
+
+This matters when a backend's declared schema is subtly wrong — an enum missing a variant it actually returns, say. A proxy that enforced the schema would replace the backend's working response with an error of its own, and the client would never see what the backend actually said.
+
+```python
+from fastmcp import Client
+from fastmcp.server import create_proxy
+
+proxy = create_proxy("backend_server.py")
+
+async with Client(proxy) as client:
+ # The backend's response arrives as the backend sent it. If it violates
+ # the backend's own output schema, this client raises — its decision.
+ result = await client.call_tool("get_status")
+```
+
+Skipping the check also avoids a `tools/list` round trip to the backend on every proxied call, since validation would need the backend's schemas and a proxy builds a fresh connection per request.
+
+### Protocol Era Mirroring
+
+
+
+A proxy is a server on its front and a client on its back, and the two MCP protocol eras have mutually exclusive interaction models on a single session. On the handshake era (≤2025-11-25) the backend can push server-initiated requests — sampling, elicitation, roots — which the proxy forwards to your client. On the modern era (2026-07-28) those pushes are gone; a backend guard tool instead returns an input request that the proxy relays back as a result. A single proxy session speaks one era, so the whole chain has to agree end-to-end.
+
+By default the proxy relays the era: whatever era your client negotiates on the front, the proxy negotiates the same era on its backend connection, per request. A handshake client reaches a handshake backend, so server-initiated forwarding works; a modern client reaches a modern backend, so a guard tool's input request round-trips. Different clients hitting the same proxy each get a backend session in their own era — the eras never cross.
+
+```python
+from fastmcp import Client
+from fastmcp.server import create_proxy
+
+# No mode: the backend mirrors each client's negotiated era.
+proxy = create_proxy("backend_server.py")
+
+# A handshake client gets a handshake backend (push-forwarding works).
+async with Client(proxy, mode="legacy") as client:
+ ...
+
+# A modern client gets a modern backend (guard tools round-trip).
+async with Client(proxy, mode="auto") as client:
+ ...
+```
+
+Passing an explicit `mode` pins the backend to one era regardless of the client:
+
+```python
+# Always negotiate the modern era upstream, whatever the client speaks.
+proxy = create_proxy("backend_server.py", mode="auto")
+```
+
+Pinning breaks the end-to-end era agreement, so reserve it for a backend that only speaks one era. When the client's era and the pinned backend era disagree on a feature — a modern client asking for a guard round-trip against a handshake-pinned backend, say — the mismatch surfaces through the normal era gates rather than silently degrading. Mirroring applies to proxies created from a target the proxy connects itself (a URL, path, config, or `FastMCP` instance); when you hand `create_proxy` an already-configured `Client`, that client carries its own mode and mirroring does not override it.
+
+A multi-server configuration adds a hop: FastMCP mounts one proxy per configured server onto a router, and your client talks to that router rather than to any backend directly. The era carries through the whole depth, so each real backend negotiates the era your client did — not just the router in front of them.
+
+```python
+proxy = create_proxy(
+ {
+ "mcpServers": {
+ "weather": {"url": "https://weather.example.com/mcp"},
+ "calendar": {"url": "https://calendar.example.com/mcp"},
+ }
+ }
+)
+```
+
+A modern client here reaches both `weather` and `calendar` on modern sessions, so a guard tool on either one round-trips end to end. An explicit `mode` pins every backend in the configuration, the same way it pins a single one.
+
+### Request Metadata
+
+Request `_meta` follows the same connection boundary. Progress tokens, tracing, task state, and application or vendor metadata pass through the proxy to the backend. The connection-owned keys — protocol version, client identity, and client capabilities — never copy from the frontend connection: a modern backend session stamps its own negotiated values, and a handshake-era backend receives none. This holds even when the two connections negotiate different eras, such as a modern client reaching a handshake-only backend through an explicit `mode`.
+
## Configuration-Based Proxies
@@ -258,10 +339,75 @@ Proxying introduces network latency:
When mounting proxy servers, this latency affects all operations on the parent server.
-For low-latency requirements, consider caching strategies or limiting mounting depth.
+### Component List Caching
+
+
+
+`ProxyProvider` caches the backend's component lists (tools, resources, templates, prompts) so that individual lookups — like resolving a tool by name during `call_tool` — don't require a separate backend connection. The cache stores raw component metadata and is shared across all proxy sessions; per-session visibility, auth, and transforms are still applied after cache lookup by the server layer. The cache refreshes whenever an explicit `list_*` call is made, and entries expire after a configurable TTL (default 300 seconds).
+
+For backends whose component lists change dynamically, disable caching by setting `cache_ttl=0`.
+
+```python
+from fastmcp.server.providers.proxy import ProxyProvider, ProxyClient
+
+# Default 300s TTL
+provider = ProxyProvider(lambda: ProxyClient("http://backend/mcp"))
+
+# Custom TTL
+provider = ProxyProvider(lambda: ProxyClient("http://backend/mcp"), cache_ttl=60)
+
+# Disable caching
+provider = ProxyProvider(lambda: ProxyClient("http://backend/mcp"), cache_ttl=0)
+```
+
+### Session Reuse for Stateless Backends
+
+By default, each tool call opens a fresh MCP session to the backend. This is the safe default because it prevents state from leaking between requests. However, for stateless HTTP backends where there's no session state to protect, this overhead is unnecessary.
+
+You can reuse a single backend session by providing a client factory that returns the same client instance:
+
+```python
+from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient
+
+base_client = ProxyClient("http://backend:8000/mcp")
+shared_client = base_client.new()
+
+proxy = FastMCPProxy(
+ client_factory=lambda: shared_client,
+ name="ReusedSessionProxy",
+)
+```
+
+This eliminates the MCP initialization handshake on every call, which can dramatically reduce latency under load. The `Client` uses reference counting for its session lifecycle, so concurrent callers sharing the same instance is safe.
+
+
+Only reuse sessions when you know the backend is stateless (e.g. stateless HTTP). For stateful backends (stdio processes, servers that track session state), use the default fresh-session behavior to avoid context mixing.
+
## Advanced Usage
+### Forwarding Server Metadata
+
+Add `ProxyMetadataMiddleware` when a gateway built with `ProxyProvider` should also expose backend instructions and namespaced `_meta`:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.providers.proxy import (
+ ProxyClient,
+ ProxyMetadataMiddleware,
+ ProxyProvider,
+)
+
+backend = ProxyProvider(lambda: ProxyClient("http://backend:8000/mcp", mode="auto"))
+gateway = FastMCP(
+ "Controlled Gateway",
+ providers=[backend],
+ middleware=[ProxyMetadataMiddleware(backend)],
+)
+```
+
+By default the gateway keeps its own `serverInfo`; pass `identity="upstream"` to use the backend identity when available. Frontend instructions and `_meta` values win on collisions. The middleware never copies upstream protocol versions, connection metadata, capabilities, cache policy, `resultType`, or unknown top-level fields. If the backend is unavailable, the client can still connect without its optional metadata.
+
### FastMCPProxy Class
For explicit session control, use `FastMCPProxy` directly:
diff --git a/docs/servers/providers/skills.mdx b/docs/servers/providers/skills.mdx
index 3c810b3f2..01214d3d4 100644
--- a/docs/servers/providers/skills.mdx
+++ b/docs/servers/providers/skills.mdx
@@ -38,10 +38,10 @@ Each subdirectory containing a `SKILL.md` file becomes a discoverable skill. Cli
from fastmcp import Client
async with Client(mcp) as client:
- # List all skill resources
+ # List each skill's main file and manifest
resources = await client.list_resources()
for r in resources:
- print(r.uri) # skill://my-skill/SKILL.md, skill://my-skill/_manifest, ...
+ print(r.uri) # skill://my-skill/SKILL.md, skill://my-skill/_manifest
# Read a skill's main instruction file
result = await client.read_resource("skill://my-skill/SKILL.md")
@@ -111,6 +111,10 @@ skill://pdf-processing/reference.md
skill://pdf-processing/examples/sample.pdf
```
+
+Supporting-file access is confined to the skill directory. Requested paths are validated before any filesystem access: attempts to traverse out with `..`, inject an absolute path, or smuggle a null byte are rejected with a clear error, and symlinks that resolve outside the skill directory are refused.
+
+
## Provider Architecture
The Skills Provider uses a two-layer architecture to handle both single skills and skill directories.
diff --git a/docs/servers/resources.mdx b/docs/servers/resources.mdx
index 0a8a772aa..13a5d986a 100644
--- a/docs/servers/resources.mdx
+++ b/docs/servers/resources.mdx
@@ -78,7 +78,7 @@ mcp = FastMCP(name="DataServer")
)
def get_application_status() -> str:
"""Internal function description (ignored if description is provided above)."""
- return json.dumps({"status": "ok", "uptime": 12345, "version": mcp.settings.version})
+ return json.dumps({"status": "ok", "uptime": 12345, "version": "2.1"})
```
@@ -90,6 +90,10 @@ def get_application_status() -> str:
A human-readable name. If not provided, defaults to function name
+
+ A human-readable display title for the resource or template
+
+
Explanation of the resource. If not provided, defaults to docstring
@@ -102,11 +106,6 @@ def get_application_status() -> str:
A set of strings used to categorize the resource. These can be used by the server and, in some cases, by clients to filter or group available resources.
-
- Deprecated in v3.0.0. Use `mcp.enable()` / `mcp.disable()` at the server level instead.
- A boolean to enable or disable the resource. See [Component Visibility](#component-visibility) for the recommended approach.
-
-
@@ -144,14 +143,16 @@ For decorating instance or class methods, use the standalone `@resource` decorat
### Return Values
-Resource functions must return one of three types:
+Resource functions can return these supported shapes:
- **`str`**: Sent as `TextResourceContents` (with `mime_type="text/plain"` by default).
- **`bytes`**: Base64 encoded and sent as `BlobResourceContents`. You should specify an appropriate `mime_type` (e.g., `"image/png"`, `"application/octet-stream"`).
-- **`ResourceResult`**: Full control over contents, MIME types, and metadata. See [ResourceResult](#resourceresult) below.
+- **JSON-native values** (`dict`, `list`, `tuple`, numbers, booleans, or `None`): Serialized to JSON text, keeping the resource's declared MIME type — which is `text/plain` unless you set one. Pass `mime_type="application/json"` on the decorator whenever clients rely on the content type to parse the payload.
+- **`list[ResourceContent]`**: Multiple content items with per-item MIME types and metadata.
+- **`ResourceResult`**: Full control over contents, MIME types, and result-level metadata. See [ResourceResult](#resourceresult) below.
-To return structured data like dicts or lists, serialize them to JSON strings using `json.dumps()`. This explicit approach ensures your type checker catches errors during development rather than at runtime when a client reads the resource.
+For custom classes that are not JSON-native, return a `ResourceResult` or wrap values in a `ResourceContent` list so serialization and MIME types are explicit.
#### ResourceResult
@@ -231,7 +232,7 @@ def get_public(): return "public"
def get_secret(): return "secret"
# Disable specific resources by key
-mcp.disable(keys={"resource:data://secret"})
+mcp.disable(names={"data://secret"})
# Disable resources by tag
mcp.disable(tags={"internal"})
@@ -349,8 +350,8 @@ if data_dir_path.is_dir():
- `TextResource`: For simple string content.
- `BinaryResource`: For raw `bytes` content.
-- `FileResource`: Reads content from a local file path. Handles text/binary modes and lazy reading.
-- `HttpResource`: Fetches content from an HTTP(S) URL (requires `httpx`).
+- `FileResource`: Reads content from a local file path. Handles text/binary modes, encoding, and lazy reading.
+- `HttpResource`: Fetches content from an HTTP(S) URL (requires `httpx2`).
- `DirectoryResource`: Lists files in a local directory (returns JSON).
- (`FunctionResource`: Internal class used by `@mcp.resource`).
@@ -369,8 +370,8 @@ def example_resource() -> str:
# These operations trigger notifications:
mcp.add_resource(example_resource) # Sends resources/list_changed notification
-mcp.disable(keys={"resource:data://example"}) # Sends resources/list_changed notification
-mcp.enable(keys={"resource:data://example"}) # Sends resources/list_changed notification
+mcp.disable(names={"data://example"}) # Sends resources/list_changed notification
+mcp.enable(names={"data://example"}) # Sends resources/list_changed notification
```
Notifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications.
@@ -477,7 +478,7 @@ FastMCP implements [RFC 6570 URI Templates](https://datatracker.ietf.org/doc/htm
-Resource templates support wildcard parameters that can match multiple path segments. While standard parameters (`{param}`) only match a single path segment and don't cross "/" boundaries, wildcard parameters (`{param*}`) can capture multiple segments including slashes. Wildcards capture all subsequent path segments *up until* the defined part of the URI template (whether literal or another parameter). This allows you to have multiple wildcard parameters in a single URI template.
+Resource templates support wildcard parameters that can match multiple path segments. Standard parameters (`{param}`) match a single URI segment before decoding and do not cross literal "/" boundaries in the request URI. Wildcard parameters (`{param*}`) can capture multiple segments including slashes. Wildcards capture all subsequent path segments *up until* the defined part of the URI template (whether literal or another parameter). This allows you to have multiple wildcard parameters in a single URI template.
```python {15, 23}
from fastmcp import FastMCP
@@ -522,6 +523,108 @@ Wildcard parameters are useful when:
Note that like regular parameters, each wildcard parameter must still be a named parameter in your function signature, and all required function parameters must appear in the URI template.
+#### Path Security
+
+
+
+Template parameters are extracted from the request URI and decoded before your function receives them, so a path-traversal payload like `../` or an absolute path can reach a handler that builds filesystem paths or upstream URLs. FastMCP screens every templated resource's parameter values **before the handler runs**, and this screening is **on by default**.
+
+By default, a parameter value is rejected if its `..` path segments would escape the value's own starting depth, if it looks like an absolute path, or if it contains a null byte. A rejected read surfaces a clean "resource not found" error to the client and logs the reason at debug level, so the failing parameter and policy are never revealed on the wire.
+
+The traversal check is component-based and tracks net depth: `..` only counts against you when it climbs above where the value starts. `../secret`, a bare `..`, and `a/../../b` are rejected; `foo/../bar` is allowed because it never leaves the starting directory, and values that merely *contain* dots — `HEAD~3..HEAD`, `v1..v2`, `file.tar.gz`, dotfiles like `.env` — all pass. Screening runs on the decoded value, so `..%2F` is caught the same as a literal `../`. This bounds relative escapes; anchoring the *final* path inside a root directory is still your handler's job (for example with `safe_join`), since only the handler knows what the value is joined to.
+
+```python
+from pathlib import Path
+
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="DocsServer")
+
+DOCS_ROOT = Path("/srv/docs")
+
+
+@mcp.resource("docs://{path*}")
+def read_doc(path: str) -> str:
+ # A request for docs://../secret is rejected before this runs.
+ return (DOCS_ROOT / path).read_text(encoding="utf-8")
+```
+
+##### Exempting parameters
+
+Some parameters legitimately carry values that look like traversal — a git ref, a version range, an opaque token. Exempt them by name with `ResourceSecurity`:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.resources import ResourceSecurity
+
+mcp = FastMCP(name="DocsServer")
+
+
+@mcp.resource(
+ "git://diff/{ref}",
+ security=ResourceSecurity(exempt_params={"ref"}),
+)
+def git_diff(ref: str) -> str:
+ # ref="HEAD~3..HEAD" is allowed
+ ...
+```
+
+##### Disabling screening
+
+Pass `security=None` to turn screening off for a single component:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="DocsServer")
+
+
+@mcp.resource("raw://{value}", security=None)
+def raw(value: str) -> str: ...
+```
+
+Or set a server-wide default with `resource_security`, which applies to every templated resource that does not set its own `security`:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.resources import ResourceSecurity
+
+# Relax one check across the whole server:
+relaxed = FastMCP(
+ name="DocsServer",
+ resource_security=ResourceSecurity(reject_absolute_paths=False),
+)
+
+# Or disable screening entirely across the server:
+unscreened = FastMCP(name="DocsServer", resource_security=None)
+```
+
+A per-component `security` always overrides the server default.
+
+
+Screening rejects the obvious injection shapes, but it does not know your filesystem root. When a parameter determines a real path, still resolve it against an allowed root and confirm containment before reading — screening and containment are complementary layers.
+
+
+```python
+from pathlib import Path
+
+from fastmcp import FastMCP
+from fastmcp.exceptions import ResourceError
+
+mcp = FastMCP(name="DocsServer")
+DOCS_ROOT = Path("docs").resolve()
+
+
+@mcp.resource("docs://{filename}")
+def read_doc(filename: str) -> str:
+ requested_path = (DOCS_ROOT / filename).resolve()
+
+ if not requested_path.is_relative_to(DOCS_ROOT) or not requested_path.is_file():
+ raise ResourceError("Document not found")
+
+ return requested_path.read_text(encoding="utf-8")
+```
+
#### Query Parameters
@@ -680,20 +783,24 @@ def get_data_by_id(id: str) -> dict:
When `mask_error_details=True`, only error messages from `ResourceError` will include details, other exceptions will be converted to a generic message.
+## Requesting Input
+
+A resource or resource template can ask the client for information before it produces content. On an MCP 2026-07-28 connection, return an `InputRequiredResult` describing what you need; the client answers and re-issues the `resources/read`, and your function runs again with the answer on `ctx.input_responses`. See [Elicitation](/servers/elicitation#prompts-and-resources) for the full pattern.
+
## Server Behavior
### Duplicate Resources
-You can configure how the FastMCP server handles attempts to register multiple resources or templates with the same URI. Use the `on_duplicate_resources` setting during `FastMCP` initialization.
+You can configure how the FastMCP server handles attempts to register the same resource or template twice. Identity is the component's type, URI, and version together, so two versions of one resource coexist and only an exact repeat collides. The `on_duplicate` setting covers every component type, so it applies to resources and templates alongside tools and prompts.
```python
from fastmcp import FastMCP
mcp = FastMCP(
name="ResourceServer",
- on_duplicate_resources="error" # Raise error on duplicates
+ on_duplicate="error" # Raise an error on an exact duplicate
)
@mcp.resource("data://config")
@@ -716,4 +823,4 @@ The duplicate behavior options are:
-Resources and resource templates support versioning, allowing you to maintain multiple implementations under the same URI while clients automatically receive the highest version. See [Versioning](/servers/versioning) for complete documentation on version comparison, retrieval, and migration patterns.
\ No newline at end of file
+Resources and resource templates support versioning, allowing you to maintain multiple implementations under the same URI while clients automatically receive the highest version. See [Versioning](/servers/versioning) for complete documentation on version comparison, retrieval, and migration patterns.
diff --git a/docs/servers/sampling.mdx b/docs/servers/sampling.mdx
index 8ea479eb0..e3448bcbe 100644
--- a/docs/servers/sampling.mdx
+++ b/docs/servers/sampling.mdx
@@ -1,7 +1,7 @@
---
title: Sampling
sidebarTitle: Sampling
-description: Request LLM text generation from the client or a configured provider through the MCP context.
+description: Generate text from a FastMCP server — by calling an LLM directly, or by asking the client to sample.
icon: robot
---
@@ -9,565 +9,103 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
-LLM sampling allows your MCP tools to request text generation from an LLM during execution. This enables tools to leverage AI capabilities for analysis, generation, reasoning, and more—without the client needing to orchestrate multiple calls.
+
+**`ctx.sample()` and `ctx.sample_step()` were removed in FastMCP 4.** The modern MCP protocol gives a server no channel to push a request to its client, so there is nothing left for those methods to do.
-By default, sampling requests are routed to the client's LLM. You can also configure a fallback handler to use a specific provider (like OpenAI) when the client doesn't support sampling, or to always use your own LLM regardless of client capabilities.
+To build a server that uses sampling, stay on [FastMCP 3.x](/v3/servers/sampling). On FastMCP 4, generate by [calling an LLM directly](#calling-an-llm-directly), or [ask the caller's model](#asking-the-callers-model) when borrowing their model is the point.
+
-## Overview
+A tool that needs text generated calls a model to get it, and in FastMCP 4 that call is ordinary Python: your server holds an API key, creates a provider client, and awaits a completion inside the tool. No protocol is involved, so the tool behaves the same for every client — including the many that never implemented sampling at all.
-The simplest use of sampling is passing a prompt string to `ctx.sample()`. The method sends the prompt to the LLM, waits for the complete response, and returns a `SamplingResult`. You can access the generated text through the `.text` attribute.
-
-```python
-from fastmcp import FastMCP, Context
-
-mcp = FastMCP()
-
-@mcp.tool
-async def summarize(content: str, ctx: Context) -> str:
- """Generate a summary of the provided content."""
- result = await ctx.sample(f"Please summarize this:\n\n{content}")
- return result.text or ""
-```
-
-The `SamplingResult` also provides `.result` (identical to `.text` for plain text responses) and `.history` containing the full message exchange—useful if you need to continue the conversation or debug the interaction.
-
-### System Prompts
-
-System prompts let you establish the LLM's role and behavioral guidelines before it processes your request. This is useful for controlling tone, enforcing constraints, or providing context that shouldn't clutter the user-facing prompt.
-
-````python
-from fastmcp import FastMCP, Context
-
-mcp = FastMCP()
-
-@mcp.tool
-async def generate_code(concept: str, ctx: Context) -> str:
- """Generate a Python code example for a concept."""
- result = await ctx.sample(
- messages=f"Write a Python example demonstrating '{concept}'.",
- system_prompt=(
- "You are an expert Python programmer. "
- "Provide concise, working code without explanations."
- ),
- temperature=0.7,
- max_tokens=300
- )
- return f"```python\n{result.text}\n```"
-````
-
-The `temperature` parameter controls randomness—higher values (up to 1.0) produce more varied outputs, while lower values make responses more deterministic. The `max_tokens` parameter limits response length.
-
-### Model Preferences
-
-Model preferences let you hint at which LLM the client should use for a request. You can pass a single model name or a list of preferences in priority order. These are hints rather than requirements—the actual model used depends on what the client has available.
-
-```python
-from fastmcp import FastMCP, Context
-
-mcp = FastMCP()
-
-@mcp.tool
-async def technical_analysis(data: str, ctx: Context) -> str:
- """Analyze data using a reasoning-focused model."""
- result = await ctx.sample(
- messages=f"Analyze this data:\n\n{data}",
- model_preferences=["claude-opus-4-5", "gpt-5-2"],
- temperature=0.2,
- )
- return result.text or ""
-```
-
-Use model preferences when different tasks benefit from different model characteristics. Creative writing might prefer faster models with higher temperature, while complex analysis might benefit from larger reasoning-focused models.
-
-### Multi-Turn Conversations
-
-For requests that need conversational context, construct a list of `SamplingMessage` objects representing the conversation history. Each message has a `role` ("user" or "assistant") and `content` (a `TextContent` object).
-
-```python
-from mcp.types import SamplingMessage, TextContent
-from fastmcp import FastMCP, Context
-
-mcp = FastMCP()
-
-@mcp.tool
-async def contextual_analysis(query: str, data: str, ctx: Context) -> str:
- """Analyze data with conversational context."""
- messages = [
- SamplingMessage(
- role="user",
- content=TextContent(type="text", text=f"Here's my data: {data}"),
- ),
- SamplingMessage(
- role="assistant",
- content=TextContent(type="text", text="I see the data. What would you like to know?"),
- ),
- SamplingMessage(
- role="user",
- content=TextContent(type="text", text=query),
- ),
- ]
- result = await ctx.sample(messages=messages)
- return result.text or ""
-```
-
-The LLM receives the full conversation thread and responds with awareness of the preceding context.
-
-### Fallback Handlers
-
-Client support for sampling is optional—some clients may not implement it. To ensure your tools work regardless of client capabilities, configure a `sampling_handler` that sends requests directly to an LLM provider.
-
-FastMCP provides built-in handlers for [OpenAI and Anthropic APIs](/clients/sampling#built-in-handlers). These handlers support the full sampling API including tools, automatically converting your Python functions to each provider's format.
-
-
-Install handlers with `pip install fastmcp[openai]` or `pip install fastmcp[anthropic]`.
-
+The alternative is to ask the caller. Sampling borrows *the caller's* model — their provider, their credentials, their bill — by returning a request for a completion that the client fulfils and hands back. Every ask costs a full round trip, so it earns its keep when using the caller's model is the point, and rarely otherwise.
+
+## Calling an LLM directly
+
+Hold a provider API key in your server's environment, create the client once at module scope so connections are reused across calls, and generate inside the tool. You choose the model, control the prompt, see the token usage, and can test the tool with no client attached.
```python
+import anthropic
from fastmcp import FastMCP
-from fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler
-server = FastMCP(
- name="My Server",
- sampling_handler=OpenAISamplingHandler(default_model="gpt-4o-mini"),
- sampling_handler_behavior="fallback",
-)
-```
+mcp = FastMCP("Summarizer")
+llm = anthropic.AsyncAnthropic()
-The `sampling_handler_behavior` parameter controls when the handler is used:
-
-- **`"fallback"`** (default): Use the handler only when the client doesn't support sampling. This lets capable clients use their own LLM while ensuring your tools still work with clients that lack sampling support.
-- **`"always"`**: Always use the handler, bypassing the client entirely. Use this when you need guaranteed control over which LLM processes requests—for cost control, compliance requirements, or when specific model characteristics are essential.
-
-## Structured Output
-
-
-
-When you need validated, typed data instead of free-form text, use the `result_type` parameter. FastMCP ensures the LLM returns data matching your type, handling validation and retries automatically.
-
-The `result_type` parameter accepts Pydantic models, dataclasses, and basic types like `int`, `list[str]`, or `dict[str, int]`. When you specify a result type, FastMCP automatically creates a `final_response` tool that the LLM calls to provide its response. If validation fails, the error is sent back to the LLM for retry.
-
-```python
-from pydantic import BaseModel
-from fastmcp import FastMCP, Context
-
-mcp = FastMCP()
-
-class SentimentResult(BaseModel):
- sentiment: str
- confidence: float
- reasoning: str
@mcp.tool
-async def analyze_sentiment(text: str, ctx: Context) -> SentimentResult:
- """Analyze text sentiment with structured output."""
- result = await ctx.sample(
- messages=f"Analyze the sentiment of: {text}",
- result_type=SentimentResult,
+async def summarize(text: str) -> str:
+ """Summarize a document in two sentences."""
+ response = await llm.messages.create(
+ model="claude-sonnet-4-5",
+ max_tokens=512,
+ system="Summarize the user's text in exactly two sentences.",
+ messages=[{"role": "user", "content": text}],
)
- return result.result # A validated SentimentResult object
+ return response.content[0].text
```
-When you call this tool, the LLM returns a structured response that FastMCP validates against your Pydantic model. You access the validated object through `result.result`, while `result.text` contains the JSON representation.
+Any provider SDK works the same way — swap the client and the call, and the tool signature is unchanged. Because generation is ordinary application code, the concerns around it are ordinary too: retries, timeouts, caching, and cost accounting go wherever you want them rather than being negotiated across a protocol boundary. A tool that chains several generations pays nothing extra for the second and third, where asking the caller would pay a full round trip for each.
-### Structured Output with Tools
+## Asking the caller's model
-Combine structured output with tools for agentic workflows that return validated data. The LLM uses your tools to gather information, then returns a response matching your type.
+A tool asks for a completion by returning an `InputRequiredResult` whose `input_requests` map holds a `CreateMessageRequest` under a key you choose. That result completes the round normally. The client runs the completion, then re-issues the same `call_tool` with the answer attached, and your tool reads it from `ctx.input_responses` under the same key — a `CreateMessageResult`. Because the tool runs from the top on every round, the presence of `ctx.input_responses` is what tells the two rounds apart: `None` on the first call, populated on the continuation.
+
+`fastmcp.Client` drives that loop for you and answers from the [`sampling_handler`](/clients/sampling) it already has, so a client written for a handshake-era server needs no extra wiring to satisfy a modern tool that asks this way.
```python
-from pydantic import BaseModel
-from fastmcp import FastMCP, Context
+from fastmcp import Context, FastMCP
+from mcp.types import (
+ CreateMessageRequest,
+ CreateMessageRequestParams,
+ CreateMessageResult,
+ InputRequiredResult,
+ SamplingMessage,
+ TextContent,
+)
-mcp = FastMCP()
+mcp = FastMCP("Research")
-def search(query: str) -> str:
- """Search the web for information."""
- return f"Results for: {query}"
-
-def fetch_url(url: str) -> str:
- """Fetch content from a URL."""
- return f"Content from: {url}"
-
-class ResearchResult(BaseModel):
- summary: str
- sources: list[str]
- confidence: float
@mcp.tool
-async def research(topic: str, ctx: Context) -> ResearchResult:
- """Research a topic and return structured findings."""
- result = await ctx.sample(
- messages=f"Research: {topic}",
- tools=[search, fetch_url],
- result_type=ResearchResult,
- )
- return result.result
-```
-
-
-Structured output with automatic validation only applies to `sample()`. With `sample_step()`, you must manage structured output yourself.
-
-
-## Tool Use
-
-
-
-Sampling with tools enables agentic workflows where the LLM can call functions to gather information before responding. This implements [SEP-1577](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1577), allowing the LLM to autonomously orchestrate multi-step operations.
-
-Pass Python functions to the `tools` parameter, and FastMCP handles the execution loop automatically—calling tools, returning results to the LLM, and continuing until the LLM provides a final response.
-
-### Defining Tools
-
-Define regular Python functions with type hints and docstrings. FastMCP extracts the function's name, docstring, and parameter types to create tool schemas that the LLM can understand.
-
-```python
-from fastmcp import FastMCP, Context
-
-def search(query: str) -> str:
- """Search the web for information."""
- return f"Results for: {query}"
-
-def get_time() -> str:
- """Get the current time."""
- from datetime import datetime
- return datetime.now().strftime("%H:%M:%S")
-
-mcp = FastMCP()
-
-@mcp.tool
-async def research(question: str, ctx: Context) -> str:
- """Answer questions using available tools."""
- result = await ctx.sample(
- messages=question,
- tools=[search, get_time],
- )
- return result.text or ""
-```
-
-The LLM sees each function's signature and docstring, using this information to decide when and how to call them. Tool errors are caught and sent back to the LLM, allowing it to recover gracefully. An internal safety limit prevents infinite loops.
-
-### Custom Tool Definitions
-
-For custom names or descriptions, use `SamplingTool.from_function()`:
-
-```python
-from fastmcp.server.sampling import SamplingTool
-
-tool = SamplingTool.from_function(
- my_func,
- name="custom_name",
- description="Custom description"
-)
-
-result = await ctx.sample(messages="...", tools=[tool])
-```
-
-### Error Handling
-
-By default, when a sampling tool raises an exception, the error message (including details) is sent back to the LLM so it can attempt recovery. To prevent sensitive information from leaking to the LLM, use the `mask_error_details` parameter:
-
-```python
-result = await ctx.sample(
- messages=question,
- tools=[search],
- mask_error_details=True, # Generic error messages only
-)
-```
-
-When `mask_error_details=True`, tool errors become generic messages like `"Error executing tool 'search'"` instead of exposing stack traces or internal details.
-
-To intentionally provide specific error messages to the LLM regardless of masking, raise `ToolError`:
-
-```python
-from fastmcp.exceptions import ToolError
-
-def search(query: str) -> str:
- """Search for information."""
- if not query.strip():
- raise ToolError("Search query cannot be empty")
- return f"Results for: {query}"
-```
-
-`ToolError` messages always pass through to the LLM, making it the escape hatch for errors you want the LLM to see and handle.
-
-### Concurrent Tool Execution
-
-By default, tools execute sequentially — one at a time, in order. When your tools are independent (no shared state between them), you can execute them in parallel with `tool_concurrency`:
-
-```python
-result = await ctx.sample(
- messages="Research these three topics",
- tools=[search, fetch_url],
- tool_concurrency=0, # Unlimited parallel execution
-)
-```
-
-The `tool_concurrency` parameter controls how many tools run at once:
-
-- **`None`** (default): Sequential execution
-- **`0`**: Unlimited parallel execution
-- **`N > 0`**: Execute at most N tools concurrently
-
-For tools that must not run concurrently (file writes, shared state mutations, etc.), mark them as `sequential` when creating the `SamplingTool`:
-
-```python
-from fastmcp.server.sampling import SamplingTool
-
-db_writer = SamplingTool.from_function(
- write_to_db,
- sequential=True, # Forces all tools in the batch to run sequentially
-)
-
-result = await ctx.sample(
- messages="Process this data",
- tools=[search, db_writer],
- tool_concurrency=0, # Would be parallel, but db_writer forces sequential
-)
-```
-
-
-When any tool in a batch has `sequential=True`, the entire batch executes sequentially regardless of `tool_concurrency`. This is a conservative guarantee — if one tool needs ordering, all tools in that batch respect it.
-
-
-### Client Requirements
-
-
-Sampling with tools requires the client to advertise the `sampling.tools` capability. FastMCP clients do this automatically. For external clients that don't support tool-enabled sampling, configure a fallback handler with `sampling_handler_behavior="always"`.
-
-
-## Advanced Control
-
-
-
-While `sample()` handles the tool execution loop automatically, some scenarios require fine-grained control over each step. The `sample_step()` method makes a single LLM call and returns a `SampleStep` containing the response and updated history.
-
-Unlike `sample()`, `sample_step()` is stateless—it doesn't remember previous calls. You control the conversation by passing the full message history each time. The returned `step.history` includes all messages up through the current response, making it easy to continue the loop.
-
-Use `sample_step()` when you need to:
-
-- Inspect tool calls before they execute
-- Implement custom termination conditions
-- Add logging, metrics, or checkpointing between steps
-- Build custom agentic loops with domain-specific logic
-
-### Basic Loop
-
-By default, `sample_step()` executes any tool calls and includes the results in the history. Call it in a loop, passing the updated history each time, until a stop condition is met.
-
-```python
-from mcp.types import SamplingMessage
-from fastmcp import FastMCP, Context
-
-mcp = FastMCP()
-
-def search(query: str) -> str:
- return f"Results for: {query}"
-
-def get_time() -> str:
- return "12:00 PM"
-
-@mcp.tool
-async def controlled_agent(question: str, ctx: Context) -> str:
- """Agent with manual loop control."""
- messages: list[str | SamplingMessage] = [question]
-
- while True:
- step = await ctx.sample_step(
- messages=messages,
- tools=[search, get_time],
- )
-
- if step.is_tool_use:
- # Tools already executed (execute_tools=True by default)
- for call in step.tool_calls:
- print(f"Called tool: {call.name}")
-
- if not step.is_tool_use:
- return step.text or ""
-
- messages = step.history
-```
-
-### SampleStep Properties
-
-Each `SampleStep` provides information about what the LLM returned:
-
-| Property | Description |
-|----------|-------------|
-| `step.is_tool_use` | True if the LLM requested tool calls |
-| `step.tool_calls` | List of tool calls requested (if any) |
-| `step.text` | The text content (if any) |
-| `step.history` | All messages exchanged so far |
-
-The contents of `step.history` depend on `execute_tools`:
-- **`execute_tools=True`** (default): Includes tool results, ready for the next iteration
-- **`execute_tools=False`**: Includes the assistant's tool request, but you add results yourself
-
-### Manual Tool Execution
-
-Set `execute_tools=False` to handle tool execution yourself. When disabled, `step.history` contains the user message and the assistant's response with tool calls—but no tool results. You execute the tools and append the results as a user message.
-
-```python
-from mcp.types import SamplingMessage, ToolResultContent, TextContent
-from fastmcp import FastMCP, Context
-
-mcp = FastMCP()
-
-@mcp.tool
-async def research(question: str, ctx: Context) -> str:
- """Research with manual tool handling."""
-
- def search(query: str) -> str:
- return f"Results for: {query}"
-
- def get_time() -> str:
- return "12:00 PM"
-
- tools = {"search": search, "get_time": get_time}
- messages: list[SamplingMessage] = [question]
-
- while True:
- step = await ctx.sample_step(
- messages=messages,
- tools=list(tools.values()),
- execute_tools=False,
- )
-
- if not step.is_tool_use:
- return step.text or ""
-
- # Execute tools and collect results
- tool_results = []
- for call in step.tool_calls:
- fn = tools[call.name]
- result = fn(**call.input)
- tool_results.append(
- ToolResultContent(
- type="tool_result",
- toolUseId=call.id,
- content=[TextContent(type="text", text=result)],
+async def ask_the_caller(question: str, ctx: Context) -> str | InputRequiredResult:
+ """Put a question to the caller's model and report what it answered."""
+ responses = ctx.input_responses
+ if responses is None:
+ return InputRequiredResult(
+ result_type="input_required",
+ input_requests={
+ "answer": CreateMessageRequest(
+ method="sampling/createMessage",
+ params=CreateMessageRequestParams(
+ messages=[
+ SamplingMessage(
+ role="user",
+ content=TextContent(type="text", text=question),
+ )
+ ],
+ max_tokens=100,
+ ),
)
- )
+ },
+ )
- messages = list(step.history)
- messages.append(SamplingMessage(role="user", content=tool_results))
+ answer = responses["answer"]
+ if isinstance(answer, CreateMessageResult) and isinstance(
+ answer.content, TextContent
+ ):
+ return answer.content.text
+ return "The client returned no completion."
```
-To report an error to the LLM, set `isError=True` on the tool result:
+Returning an `InputRequiredResult` needs a `2026-07-28` connection, and FastMCP names the era mismatch if an older client reaches the tool; the conformance suite exercises this route on that version. The map can carry several requests at once and mix kinds — a sampling request beside an elicitation or a roots request — with each answer coming back under its own key. [Elicitation](/servers/elicitation#sampling-and-roots) covers the mechanics of the pattern in full, including how to carry state across rounds.
-```python
-tool_result = ToolResultContent(
- type="tool_result",
- toolUseId=call.id,
- content=[TextContent(type="text", text="Permission denied")],
- isError=True,
-)
-```
+## The removed methods
-## Method Reference
+`Context` has no `sample()` and no `sample_step()`; touching either raises `AttributeError` on every protocol era, rather than failing at runtime only against modern clients. `FastMCP()` accepts neither `sampling_handler=` nor `sampling_handler_behavior=`, and naming one raises a `TypeError` that points at the migration.
-
-
- Request text generation from the LLM, running to completion automatically.
+The reason is the distinction MCP draws between telling and asking. A notification is fire-and-forget: the server emits it and moves on, and it travels down the response stream the caller already opened, so nothing has to be held open on the server's behalf. That is why [logging](/servers/logging) is untouched by any of this — `ctx.info()` and its siblings reach the client mid-call on every era. Sampling is the other kind. `sampling/createMessage` goes out and the caller must answer before the tool can continue, which needs a live, addressable connection the server can reach into, and the `2026-07-28` revision removed server-initiated requests ([SEP-2577](https://modelcontextprotocol.io/community/sep-guidelines)) precisely because a stateless protocol has no such thing.
-
-
- The prompt to send. Can be a simple string or a list of messages for multi-turn conversations.
-
+What the protocol removed is the pushing, not the asking, so the capability survives in the shape described above. Keeping `ctx.sample()` alongside it would mean shipping a method whose outcome against a default client — one that negotiates the modern era — is a runtime failure.
-
- Instructions that establish the LLM's role and behavior.
-
-
-
- Controls randomness (0.0 = deterministic, 1.0 = creative).
-
-
-
- Maximum tokens to generate.
-
-
-
- Hints for which model the client should use.
-
-
-
- Functions the LLM can call during sampling.
-
-
-
- A type for validated structured output. Supports Pydantic models, dataclasses, and basic types like `int`, `list[str]`, or `dict[str, int]`.
-
-
-
- If True, mask detailed error messages from tool execution. When None (default), uses the global `settings.mask_error_details` value. Tools can raise `ToolError` to bypass masking and provide specific error messages to the LLM.
-
-
-
- Controls parallel execution of tools. `None` (default) for sequential, `0` for unlimited parallel, or a positive integer for bounded concurrency. If any tool has `sequential=True`, all tools execute sequentially regardless.
-
-
-
-
-
-
- - `.text`: The raw text response (or JSON for structured output)
- - `.result`: The typed result—same as `.text` for plain text, or a validated Pydantic object for structured output
- - `.history`: All messages exchanged during sampling
-
-
-
-
-
-
-
- Make a single LLM sampling call. Use this for fine-grained control over the sampling loop.
-
-
-
- The prompt or conversation history.
-
-
-
- Instructions that establish the LLM's role and behavior.
-
-
-
- Controls randomness (0.0 = deterministic, 1.0 = creative).
-
-
-
- Maximum tokens to generate.
-
-
-
- Functions the LLM can call during sampling.
-
-
-
- Controls tool usage: `"auto"`, `"required"`, or `"none"`.
-
-
-
- If True, execute tool calls and append results to history. If False, return immediately with tool calls available for manual execution.
-
-
-
- If True, mask detailed error messages from tool execution.
-
-
-
- Controls parallel execution of tools. `None` (default) for sequential, `0` for unlimited parallel, or a positive integer for bounded concurrency.
-
-
-
-
-
- - `.response`: The raw LLM response
- - `.history`: Messages including input, assistant response, and tool results
- - `.is_tool_use`: True if the LLM requested tool execution
- - `.tool_calls`: List of tool calls (if any)
- - `.text`: The text content (if any)
-
-
-
-
+
+Servers on FastMCP 3 still have `ctx.sample()` and `ctx.sample_step()`, documented in the [FastMCP 3 sampling guide](/v3/servers/sampling). Nothing changes for them until they upgrade.
+
diff --git a/docs/servers/server.mdx b/docs/servers/server.mdx
index ed84b5e48..ee778f887 100644
--- a/docs/servers/server.mdx
+++ b/docs/servers/server.mdx
@@ -11,36 +11,105 @@ The `FastMCP` class is the central piece of every FastMCP application. It acts a
## Creating a Server
-Instantiate a server by providing a name that identifies it in client applications and logs. You can also provide instructions that help clients understand the server's purpose.
+At its simplest, a FastMCP server just needs a name. Everything else has sensible defaults.
```python
from fastmcp import FastMCP
-mcp = FastMCP(name="MyAssistantServer")
+mcp = FastMCP("MyServer")
+```
-# Instructions help clients understand how to interact with the server
-mcp_with_instructions = FastMCP(
- name="HelpfulAssistant",
- instructions="""
- This server provides data analysis tools.
- Call get_average() to analyze numerical data.
- """,
+Instructions help clients (and the LLMs behind them) understand what your server does and how to use it effectively.
+
+```python
+mcp = FastMCP(
+ "DataAnalysis",
+ instructions="Provides tools for analyzing numerical datasets. Start with get_summary() for an overview.",
)
```
-The `FastMCP` constructor accepts several configuration options. The most commonly used parameters control server identity, authentication, and component behavior.
+## Components
-
-
- A human-readable name for your server
+FastMCP servers expose three types of components to clients, each serving a distinct role in the MCP protocol.
+
+**Tools** are functions that clients invoke to perform actions or access external systems.
+
+```python
+@mcp.tool
+def multiply(a: float, b: float) -> float:
+ """Multiplies two numbers together."""
+ return a * b
+```
+
+**Resources** expose data that clients can read — passive data sources rather than invocable functions.
+
+```python
+@mcp.resource("data://config")
+def get_config() -> dict:
+ return {"theme": "dark", "version": "1.0"}
+```
+
+**Prompts** are reusable message templates that guide LLM interactions.
+
+```python
+@mcp.prompt
+def analyze_data(data_points: list[float]) -> str:
+ formatted_data = ", ".join(str(point) for point in data_points)
+ return f"Please analyze these data points: {formatted_data}"
+```
+
+Each component type has detailed documentation: [Tools](/servers/tools), [Resources](/servers/resources) (including [Resource Templates](/servers/resources#resource-templates)), and [Prompts](/servers/prompts).
+
+## Running the Server
+
+Start your server by calling `mcp.run()`. The `if __name__` guard ensures compatibility with MCP clients that launch your server as a subprocess.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("MyServer")
+
+@mcp.tool
+def greet(name: str) -> str:
+ """Greet a user by name."""
+ return f"Hello, {name}!"
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+FastMCP supports several transports:
+- **STDIO** (default): For local integrations and CLI tools
+- **HTTP**: For web services using the Streamable HTTP protocol
+- **SSE**: Legacy web transport (deprecated)
+
+```python
+# Run with HTTP transport
+mcp.run(transport="http", host="127.0.0.1", port=9000)
+```
+
+The server can also be run using the FastMCP CLI. For detailed information on transports and deployment, see [Running Your Server](/deployment/running-server).
+
+
+## Configuration Reference
+
+The `FastMCP` constructor accepts parameters organized into four categories: identity, composition, behavior, and handlers.
+
+### Identity
+
+These parameters control how your server presents itself to clients.
+
+
+
+ A human-readable name for your server, shown in client applications and logs. If omitted, FastMCP generates a random name
- Description of how to interact with this server. These instructions help clients understand the server's purpose and available functionality
+ Description of how to interact with this server. Clients surface these instructions to help LLMs understand the server's purpose and available functionality
-
- Version string for your server. If not provided, defaults to the FastMCP library version
+
+ Version string for your server. Defaults to the FastMCP library version if not provided
@@ -52,108 +121,131 @@ The `FastMCP` constructor accepts several configuration options. The most common
- List of icon representations for your server. Icons help users visually identify your server in client applications. See [Icons](/servers/icons) for detailed examples
+ List of icon representations for your server. See [Icons](/servers/icons) for details
-
- Authentication provider for securing HTTP-based transports. See [Authentication](/servers/auth/authentication) for configuration options
+
+
+
+ Arbitrary experimental capabilities to advertise in the MCP `initialize` response. Use this to declare cross-server interop conventions or draft extensions that follow the MCP spec's `experimental` field. Keys are capability names; values are free-form dicts. FastMCP's built-in derived capabilities (`tools`, `resources`, etc.) are unaffected — this only populates `capabilities.experimental`
+
+
+
+### Composition
+
+These parameters control what your server is built from — its components, middleware, providers, and lifecycle.
+
+
+
+ Tools to register on the server. An alternative to the `@mcp.tool` decorator when you need to add tools programmatically
-
- Server-level setup and teardown logic. See [Lifespans](/servers/lifespan) for composable lifespans
+
+ Authentication provider for securing HTTP-based transports. See [Authentication](/servers/auth/authentication) for configuration
-
- A list of tools (or functions to convert to tools) to add to the server. In some cases, providing tools programmatically may be more convenient than using the `@mcp.tool` decorator
+
+ [Middleware](/servers/middleware) that intercepts and transforms every MCP message flowing through the server — requests, responses, and notifications in both directions. Use for cross-cutting concerns like logging, error handling, and rate limiting
-
+
+ [Providers](/servers/providers/overview) that supply tools, resources, and prompts dynamically. Providers are queried at request time, so they can serve components from databases, APIs, or other external sources
+
+
+
- Server-level [transforms](/servers/transforms/transforms) to apply to all components. Transforms modify how tools, resources, and prompts are presented to clients — for example, [search transforms](/servers/transforms/tool-search) replace large catalogs with on-demand discovery, and [CodeMode](/servers/transforms/code-mode) lets LLMs write scripts that chain tool calls in a sandbox
+ Server-level [transforms](/servers/transforms/transforms) to apply to all components. Transforms modify how tools, resources, and prompts are presented to clients — for example, [search transforms](/servers/transforms/tool-search) replace large catalogs with on-demand discovery
+
+ Server-level setup and teardown logic that runs when the server starts and stops. See [Lifespans](/servers/lifespan) for composable lifespans
+
+
+
+### Behavior
+
+These parameters tune how the server processes requests and communicates with clients.
+
+
How to handle duplicate component registrations
-
- Controls how tool input parameters are validated. When `False` (default), FastMCP uses Pydantic's flexible validation that coerces compatible inputs (e.g., `"10"` to `10` for int parameters). When `True`, uses the MCP SDK's JSON Schema validation to validate inputs against the exact schema before passing them to your function, rejecting any type mismatches. The default mode improves compatibility with LLM clients while maintaining type safety. See [Input Validation Modes](/servers/tools#input-validation-modes) for details
+
+ When `False` (default), FastMCP uses Pydantic's flexible validation that coerces compatible inputs (e.g., `"10"` → `10` for int parameters). When `True`, validates inputs against the exact JSON Schema before calling your function, rejecting type mismatches. See [Validation Modes](/servers/tools#validation-modes) for details
+
+
+
+ When `True`, replaces internal error details in tool/resource responses with a generic message to avoid leaking implementation details to clients. Defaults to the `FASTMCP_MASK_ERROR_DETAILS` environment variable
- Maximum number of items per page for list operations (`tools/list`, `resources/list`, etc.). When `None` (default), all results are returned in a single response. When set, responses are paginated and include a `nextCursor` for fetching additional pages. See [Pagination](/servers/pagination) for details
+
+ Maximum items per page for list operations (`tools/list`, `resources/list`, etc.). Must be a positive integer when set. When `None`, all results are returned in a single response. See [Pagination](/servers/pagination) for details
+
+ Enable background task support. When `True`, tools and resources can return `CreateTaskResult` to run work asynchronously while the client polls for results
+
+
+
+
+
+ Default minimum log level for messages sent to MCP clients via `context.log()`. When set, messages below this level are suppressed. Handshake-era clients can override this per-session using the MCP `logging/setLevel` request; the modern protocol has no session to hold that level, so clients on it filter by level in their own log handler instead. One of `"debug"`, `"info"`, `"notice"`, `"warning"`, `"error"`, `"critical"`, `"alert"`, or `"emergency"`
+
+
+
+ Automatically dereference `$ref` pointers in JSON schemas generated from complex Pydantic models. Most clients require flat schemas without `$ref`, so this should usually stay enabled
+
+
+
+ How long, in seconds, a client may treat this server's cacheable responses as fresh (SEP-2549). When set, the hint applies uniformly to `tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, and `resources/read`. Clients must opt into caching to honor it — see [Response caching](/clients/client#response-caching). Must be a positive integer
+
+
+
+ Whether a cached response may be shared across authorization contexts (`"public"`) or reused only within the one that produced it (`"private"`, the default when a `cache_ttl` is set). Requires `cache_ttl`
+
-## Components
+### Storage
-FastMCP servers expose three types of components to clients. Each type serves a distinct purpose in the MCP protocol.
+
+
+ Persistent key-value store for session state that survives across requests. Defaults to an in-memory store. Provide a custom implementation for persistence across server restarts
+
+
-### Tools
-Tools are functions that clients can invoke to perform actions or access external systems. They're the primary way clients interact with your server's capabilities.
+## Response Caching
+
+
+
+A server whose listings and resource reads change slowly can tell clients how long they may reuse a response before fetching it again (SEP-2549). Set `cache_ttl` (seconds) on the server, and the hint is attached uniformly to every cacheable response — `tools/list`, `prompts/list`, `resources/list`, `resources/templates/list`, and `resources/read`.
```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("Weather", cache_ttl=300, cache_scope="public")
+
@mcp.tool
-def multiply(a: float, b: float) -> float:
- """Multiplies two numbers together."""
- return a * b
+def forecast(city: str) -> str:
+ return f"Sunny in {city}"
```
-See [Tools](/servers/tools) for detailed documentation.
+`cache_scope` controls whether a cached response may be shared across authorization contexts (`"public"`) or reused only within the one that produced it (`"private"`, the default when a TTL is set). A `cache_scope` without a `cache_ttl` does not enable caching and raises at construction.
-### Resources
+The hint is inert on its own: a client only reuses a response if it opts into caching and negotiates the modern protocol. See [Response caching](/clients/client#response-caching) for the client side.
-Resources expose data that clients can read. Unlike tools, resources are passive data sources that clients pull from rather than invoke.
-
-```python
-@mcp.resource("data://config")
-def get_config() -> dict:
- """Provides the application configuration."""
- return {"theme": "dark", "version": "1.0"}
-```
-
-See [Resources](/servers/resources) for detailed documentation.
-
-### Resource Templates
-
-Resource templates are parameterized resources. The client provides values for template parameters in the URI, and the server returns data specific to those parameters.
-
-```python
-@mcp.resource("users://{user_id}/profile")
-def get_user_profile(user_id: int) -> dict:
- """Retrieves a user's profile by ID."""
- return {"id": user_id, "name": f"User {user_id}", "status": "active"}
-```
-
-See [Resource Templates](/servers/resources#resource-templates) for detailed documentation.
-
-### Prompts
-
-Prompts are reusable message templates that guide LLM interactions. They help establish consistent patterns for how clients should frame requests.
-
-```python
-@mcp.prompt
-def analyze_data(data_points: list[float]) -> str:
- """Creates a prompt asking for analysis of numerical data."""
- formatted_data = ", ".join(str(point) for point in data_points)
- return f"Please analyze these data points: {formatted_data}"
-```
-
-See [Prompts](/servers/prompts) for detailed documentation.
## Tag-Based Filtering
-Tags let you categorize components and selectively expose them based on configurable include/exclude sets. This is useful for creating different views of your server for different environments or user types.
-
-Components can be tagged when defined using the `tags` parameter. A component can have multiple tags, and filtering operates on tag membership.
+Tags let you categorize components and selectively expose them. This is useful for creating different views of your server for different environments or user types.
```python
@mcp.tool(tags={"public", "utility"})
@@ -171,11 +263,9 @@ The filtering logic works as follows:
- **Precedence**: Later calls override earlier ones, so call `disable` after `enable` to exclude from an allowlist
-To ensure a component is never exposed, you can set `enabled=False` on the component itself. See the component-specific documentation for details.
+To hide a component by default, disable it at the server level with `mcp.disable(names={"admin_tool"})`. This is a default rather than a guarantee — a later `enable()` call or a per-session visibility rule can bring the component back. When something must never be reachable, leave it unregistered or guard it with [authentication](/servers/auth/authentication) instead of relying on visibility.
-Configure tag-based filtering after creating your server.
-
```python
# Only expose components tagged with "public"
mcp = FastMCP()
@@ -192,38 +282,9 @@ mcp.enable(tags={"admin"}, only=True).disable(tags={"deprecated"})
This filtering applies to all component types (tools, resources, resource templates, and prompts) and affects both listing and access.
-## Running the Server
-
-FastMCP servers communicate with clients through transport mechanisms. Start your server by calling `mcp.run()`, typically within an `if __name__ == "__main__":` block. This pattern ensures compatibility with various MCP clients.
-
-```python
-from fastmcp import FastMCP
-
-mcp = FastMCP(name="MyServer")
-
-@mcp.tool
-def greet(name: str) -> str:
- """Greet a user by name."""
- return f"Hello, {name}!"
-
-if __name__ == "__main__":
- # Defaults to STDIO transport
- mcp.run()
-
- # Or use HTTP transport
- # mcp.run(transport="http", host="127.0.0.1", port=9000)
-```
-
-FastMCP supports several transports:
-- **STDIO** (default): For local integrations and CLI tools
-- **HTTP**: For web services using the Streamable HTTP protocol
-- **SSE**: Legacy web transport (deprecated)
-
-The server can also be run using the FastMCP CLI. For detailed information on transports and configuration, see the [Running Your Server](/deployment/running-server) guide.
-
## Custom Routes
-When running with HTTP transport, you can add custom web routes alongside your MCP endpoint using the `@custom_route` decorator. This is useful for auxiliary endpoints like health checks.
+When running with HTTP transport, you can add custom web routes alongside your MCP endpoint using the `@custom_route` decorator.
```python
from fastmcp import FastMCP
@@ -240,9 +301,4 @@ if __name__ == "__main__":
mcp.run(transport="http") # Health check at http://localhost:8000/health
```
-Custom routes are served alongside your MCP endpoint and are useful for:
-- Health check endpoints for monitoring
-- Simple status or info endpoints
-- Basic webhooks or callbacks
-
-For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/deployment/http#integration-with-web-frameworks).
+Custom routes are useful for health checks, status endpoints, and simple webhooks. For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/deployment/http#integration-with-web-frameworks).
diff --git a/docs/servers/sessions.mdx b/docs/servers/sessions.mdx
new file mode 100644
index 000000000..25f4e8e70
--- /dev/null
+++ b/docs/servers/sessions.mdx
@@ -0,0 +1,107 @@
+---
+title: Session State
+sidebarTitle: Sessions
+description: Persist state across requests on stateless connections.
+icon: id-badge
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+The modern MCP protocol (`2026-07-28`) is stateless. Every request stands alone: the server builds a fresh connection to handle it and discards everything when it returns. There is no session to hang state on, so a tool that wants to remember something between calls — the items in a cart, the thread of a conversation, a running total — has nowhere to keep it. Store it on the connection and it vanishes the moment the request finishes.
+
+This is a deliberate choice in the protocol. Weighing protocol-level sessions against statelessness, the MCP working group [chose statelessness](https://github.com/modelcontextprotocol/transports-wg/blob/main/docs/sessions-vs-sessionless-decision.md) and moved session semantics up to the application: the server hands the client an identifier, and the client passes it back as an argument on later calls. Their own example is a shopping cart — the server returns a `basket_id`, and the client includes it in each subsequent `add_item` and `checkout` call.
+
+FastMCP implements that pattern as **session state**, and adds the one thing the bare handle lacks: isolation. State is stored server-side and keyed to the authenticated user, so a handle is inert in anyone else's hands. You pick one of two shapes per tool, depending on whether a user has a single bucket of state or many.
+
+## Per-user state
+
+Most tools that remember things want one bucket per user — their preferences, their history, their accumulated context. Declare a `UserSession` parameter and FastMCP injects it, keyed to the authenticated user. It behaves like the request [context](/servers/context): it never appears in the tool's input schema and the caller passes nothing, because the user's identity comes from their validated credentials and selects the right bucket automatically.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.sessions import UserSession
+
+mcp = FastMCP("assistant")
+
+
+@mcp.tool
+async def remember(fact: str, session: UserSession) -> str:
+ facts = await session.get("facts", default=[])
+ facts.append(fact)
+ await session.set("facts", facts)
+ return f"Remembered {len(facts)} facts."
+```
+
+Because the bucket is chosen from the caller's identity, `UserSession` requires [authentication](/servers/auth/authentication). On an unauthenticated request there is no user to key on, so the tool raises a clear error rather than guessing at a bucket.
+
+## Distinct sessions
+
+Sometimes one user needs more than one bucket — separate carts, parallel conversations, independent workflows. Now the caller has to say *which* session it means, so the identifier becomes a tool argument.
+
+Declare a `SessionId` parameter. Unlike `UserSession`, it appears in the input schema as a string, because the agent is the one that supplies it. FastMCP fills in that argument's description for you — instructing the agent to obtain an id and pass it back — so the tool teaches the protocol on its own, with no prompting on your side.
+
+An agent obtains an id by calling `create_session`, which comes from a `SessionProvider` — [providers](/servers/providers/overview) are how FastMCP contributes functionality like this. Register one whenever your tools take a `session_id`. Without it there is no way to mint an id, so every id is rejected and the tools cannot resolve a session — a mistake you catch the first time you run them.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.sessions import SessionId, SessionProvider
+from fastmcp.server.dependencies import get_session
+
+mcp = FastMCP("shop")
+mcp.add_provider(SessionProvider())
+
+
+@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 in cart."
+```
+
+`get_session` resolves and validates the id, returning a [`Session`](#the-session-object). It is a standalone function, not a context method, so it needs no foreground context and works from a [background task](/servers/tasks)'s worker as well as a normal request.
+
+A session id is real and owned: `create_session` records it under the current user, and only an id created that way resolves. Passing an id that was never created — or one created by a different user — raises rather than quietly opening a fresh bucket, so a typo or a stolen id fails loudly instead of misrouting state.
+
+When your application already mints its own identifiers — conversation ids, workflow ids — take them as ordinary string arguments rather than `SessionId`, and skip the provider entirely; `SessionId` is specifically the create-then-pass contract backed by `create_session`.
+
+## The session object
+
+Both patterns give a tool a `Session`: an async view over one bucket of stored state. Read a value with `await session.get(key, default=None)`, write one with `await session.set(key, value)`, and remove one with `await session.delete(key)`. Values are stored as JSON, so anything JSON-serializable round-trips.
+
+The session's own identifier is available as `session.id` — the id for a session resolved from a `session_id` argument, and `None` for an injected `UserSession`, which has no distinct id because its bucket is the authenticated user.
+
+`await session.clear()` empties the session's state while keeping the session itself valid — the id still resolves, the bucket is just empty. To retire a session entirely, an agent calls `end_session`, which deletes it so the id no longer resolves at all.
+
+## Isolation
+
+Every session is keyed by two things, in this order: the authenticated user, then the session id. The order is the whole security model. The user is the wall; the id only organizes sessions *within* that wall.
+
+On an authenticated request the user comes from the validated token, which the caller cannot forge. Two different users can pass the very same session id and never reach each other's data, because each id is namespaced under its user's identity. This makes a session id safe to expose — it travels through the agent's context and your logs, and on its own it grants nothing. Guessing another user's id leads nowhere: it was created under *their* namespace, so in the guesser's namespace it simply does not exist and the call is rejected.
+
+Without authentication there is no user to key on, and the guarantee changes.
+
+**An unauthenticated session is a bearer handle: whoever holds the id can read and write it.** Ids from `create_session` are unguessable, which keeps a caller from stumbling onto another session, but that is guess-resistance, not isolation — a leaked id is a leaked session. Treat unauthenticated sessions as single-tenant: sound for a personal server with one trusted client, never a boundary between tenants. Multi-tenant isolation requires authentication.
+
+## Storage and lifetime
+
+Session state lives in the server's [storage backend](/servers/storage-backends) — in-memory by default, or Redis or another shared store when a fleet of servers must see the same sessions. Because the store owns retention, it owns expiry: FastMCP writes session state without a TTL of its own, so the store you configure is the single place session data lives and expires. To give every session a default lifetime, wrap the store so writes without an explicit TTL get one — for example, the `key-value` library's TTL-clamp wrapper takes a `missing_ttl`:
+
+```python
+from key_value.aio.stores.redis import RedisStore
+from key_value.aio.wrappers.ttl_clamp import TTLClampWrapper
+
+store = RedisStore(url="redis://localhost:6379")
+store = TTLClampWrapper(store, min_ttl=0, max_ttl=86400, missing_ttl=3600)
+
+mcp = FastMCP("shop", session_state_store=store)
+```
+
+Now a session expires an hour after its last write, and `end_session` still removes one immediately.
+
+## Relationship to request state
+
+The request [context](/servers/context) also carries state, through `ctx.set_state` and `ctx.get_state`, and the two solve different problems. Context state is scoped to a single request — the right place for a value that a middleware sets and a handler reads within the same call. Session state is what persists *across* requests. When you need a value to survive from one tool call to the next, reach for `UserSession` or `SessionId`; when it only needs to live for the current request, keep it on the context.
diff --git a/docs/servers/storage-backends.mdx b/docs/servers/storage-backends.mdx
index 1bdb19b20..32e6530eb 100644
--- a/docs/servers/storage-backends.mdx
+++ b/docs/servers/storage-backends.mdx
@@ -64,7 +64,9 @@ store = FileTreeStore(
middleware = ResponseCachingMiddleware(cache_storage=store)
```
-The sanitization strategies ensure keys and collection names are safe for the filesystem — alphanumeric names pass through as-is for readability, while special characters are hashed to prevent path traversal.
+
+**Sanitization strategies are required** when using `FileTreeStore`. Without them, keys containing special characters (such as URL-based OAuth client IDs like `https://claude.ai/oauth/claude-code-client-metadata`) will be used as-is in filesystem paths, causing `FileNotFoundError` crashes. The V1 strategies shown above are safe defaults — alphanumeric names pass through as-is for readability, while special characters are hashed to prevent path errors and traversal attacks. Changing sanitization strategies after data has been written is a breaking change, so choose your strategy upfront.
+
**Characteristics:**
- ✅ Data persists across restarts
@@ -154,9 +156,7 @@ The [OAuth Proxy](/servers/auth/oauth-proxy) and OAuth auth providers use storag
**Development (default behavior):**
-By default, FastMCP automatically manages keys and storage based on your platform:
-- **Mac/Windows**: Keys are auto-managed via system keyring, storage defaults to disk. Suitable **only** for development and local testing.
-- **Linux**: Keys are ephemeral, storage defaults to memory.
+By default, FastMCP automatically manages keys and storage the same way on every platform: the signing key is deterministically derived from your client secret, and storage defaults to an encrypted disk store in your platform's data directory (derived from `platformdirs`). Suitable **only** for development and local testing.
No configuration needed:
@@ -199,7 +199,7 @@ Both parameters are required for production. **Wrap your storage in `FernetEncry
### Response Caching Middleware
-The [Response Caching Middleware](/servers/middleware#caching-middleware) caches tool calls, resource reads, and prompt requests. Storage configuration is passed via the `cache_storage` parameter:
+The [Response Caching Middleware](/servers/middleware#caching) caches tool calls, resource reads, and prompt requests. Storage configuration is passed via the `cache_storage` parameter:
```python
from pathlib import Path
@@ -246,7 +246,7 @@ The [FastMCP Client](/clients/client) uses storage for persisting OAuth tokens l
```python
from pathlib import Path
-from fastmcp.client.auth import OAuthClientProvider
+from fastmcp.client.auth import OAuth
from key_value.aio.stores.filetree import (
FileTreeStore,
FileTreeV1KeySanitizationStrategy,
@@ -261,7 +261,7 @@ token_storage = FileTreeStore(
collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(token_dir),
)
-oauth_provider = OAuthClientProvider(
+oauth_provider = OAuth(
mcp_url="https://your-mcp-server.com/mcp/sse",
token_storage=token_storage
)
@@ -289,6 +289,6 @@ This allows clients to reconnect without re-authenticating after restarts.
## More Resources
- [py-key-value-aio GitHub](https://github.com/strawgate/py-key-value) - Full library documentation
-- [Response Caching Middleware](/servers/middleware#caching-middleware) - Using storage for caching
+- [Response Caching Middleware](/servers/middleware#caching) - Using storage for caching
- [OAuth Token Security](/deployment/http#oauth-token-security) - Production OAuth configuration
- [HTTP Deployment](/deployment/http) - Complete deployment guide
diff --git a/docs/servers/tasks.mdx b/docs/servers/tasks.mdx
index d4aae9f54..7b374473d 100644
--- a/docs/servers/tasks.mdx
+++ b/docs/servers/tasks.mdx
@@ -1,58 +1,59 @@
---
title: Background Tasks
sidebarTitle: Background Tasks
-description: Run long-running operations asynchronously with progress tracking
+description: Run long-running tools asynchronously with progress tracking
icon: clock
tag: "NEW"
---
import { VersionBadge } from "/snippets/version-badge.mdx"
-
+
-Background tasks require the `tasks` optional extra. See [installation instructions](#enabling-background-tasks) below.
+Background tasks require the `fastmcp-tasks` package. See [enabling background tasks](#enabling-background-tasks) below.
-FastMCP implements the MCP background task protocol ([SEP-1686](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks)), giving your servers a production-ready distributed task scheduler with a single decorator change.
+FastMCP implements the MCP background tasks extension ([`io.modelcontextprotocol/tasks`](https://modelcontextprotocol.io/extensions/tasks/overview), SEP-2663), giving your servers a production-ready distributed task scheduler with one extension registration and a decorator change.
**What is Docket?** FastMCP's task system is powered by [Docket](https://github.com/chrisguidry/docket), originally built by [Prefect](https://prefect.io) to power [Prefect Cloud](https://www.prefect.io/prefect/cloud)'s managed task scheduling and execution service, where it processes millions of concurrent tasks every day. Docket is now open-sourced for the community.
-
## What Are MCP Background Tasks?
-In MCP, all component interactions are blocking by default. When a client calls a tool, reads a resource, or fetches a prompt, it sends a request and waits for the response. For operations that take seconds or minutes, this creates a poor user experience.
+In MCP, a tool call is blocking by default. When a client calls a tool, it sends a request and waits for the response. For operations that take seconds or minutes, this creates a poor user experience.
-The MCP background task protocol solves this by letting clients:
-1. **Start** an operation and receive a task ID immediately
-2. **Track** progress as the operation runs
-3. **Retrieve** the result when ready
+Background tasks solve this by letting a server tell a supporting client:
+1. **Start** the tool and return a task ID immediately
+2. **Poll** for status as the tool runs
+3. **Retrieve** the result when ready — or answer a question the tool asks mid-run
-FastMCP handles all of this for you. Add `task=True` to your decorator, and your function gains full background execution with progress reporting, distributed processing, and horizontal scaling.
+FastMCP handles all of this for you. Add `task=True` to a tool decorator and register the tasks extension, and your function gains background execution with progress reporting, distributed processing, and horizontal scaling.
### MCP Background Tasks vs Python Concurrency
You can always use Python's concurrency primitives (asyncio, threads, multiprocessing) or external task queues in your FastMCP servers. FastMCP is just Python—run code however you like.
-MCP background tasks are different: they're **protocol-native**. This means MCP clients that support the task protocol can start operations, receive progress updates, and retrieve results through the standard MCP interface. The coordination happens at the protocol level, not inside your application code.
+MCP background tasks are different: they're **protocol-native**. This means MCP clients that support the tasks extension can start a call, poll it, and retrieve its result through the standard MCP interface. The coordination happens at the protocol level, not inside your application code.
## Enabling Background Tasks
- Background tasks require the `tasks` extra:
+Background tasks require the `fastmcp-tasks` package:
```bash
pip install "fastmcp[tasks]"
```
-Add `task=True` to any tool, resource, resource template, or prompt decorator. This marks the component as capable of background execution.
+Register `TasksExtension` on your server, then add `task=True` to a tool decorator. `task=True` marks the tool as *capable* of background execution; the extension is what actually runs it — a `task=True` tool on a server with no tasks extension registered raises at server startup.
-```python {6}
+```python {5,8}
import asyncio
from fastmcp import FastMCP
+from fastmcp_tasks import TasksExtension
mcp = FastMCP("MyServer")
+mcp.add_extension(TasksExtension())
@mcp.tool(task=True)
async def slow_computation(duration: int) -> str:
@@ -62,34 +63,38 @@ async def slow_computation(duration: int) -> str:
return f"Completed in {duration} seconds"
```
-When a client requests background execution, the call returns immediately with a task ID. The work executes in a background worker, and the client can poll for status or wait for the result.
+Whether a given call actually runs as a task depends on the client: it opts in per request, and the *server* decides based on the tool's execution mode (below). When it does run as a task, the call returns immediately with a task ID; the work executes in a background worker, and the client polls for the result. A [FastMCP client](/clients/tasks) does all of this transparently — `client.call_tool(...)` looks the same either way.
+
+Background tasks are a modern-protocol feature: the tasks capability is negotiated over `2026-07-28` connections, so a client pinned to `mode="legacy"` never triggers one — the tool always runs synchronously for it.
-Background tasks require async functions. Attempting to use `task=True` with a sync function raises a `ValueError` at registration time.
+Background tasks require async functions. Attempting to use `task=True` with a sync function raises a `ValueError` at registration time. Only tools can be task-enabled; resources, resource templates, and prompts do not carry `task=`.
## Execution Modes
-For fine-grained control over task execution behavior, use `TaskConfig` instead of the boolean shorthand. The MCP task protocol defines three execution modes:
+For fine-grained control over task execution behavior, use `TaskConfig` instead of the boolean shorthand. The tasks extension defines three execution modes:
-| Mode | Client calls without task | Client calls with task |
+| Mode | Client calls without the tasks capability | Client calls with the tasks capability |
|------|--------------------------|------------------------|
-| `"forbidden"` | Executes synchronously | Error: task not supported |
-| `"optional"` | Executes synchronously | Executes as background task |
-| `"required"` | Error: task required | Executes as background task |
+| `"forbidden"` | Executes synchronously | Executes synchronously (never tasked) |
+| `"optional"` | Executes synchronously | Executes as a background task |
+| `"required"` | Error: task required | Executes as a background task |
```python
from fastmcp import FastMCP
-from fastmcp.server.tasks import TaskConfig
+from fastmcp.utilities.tasks import TaskConfig
+from fastmcp_tasks import TasksExtension
mcp = FastMCP("MyServer")
+mcp.add_extension(TasksExtension())
# Supports both sync and background execution (default when task=True)
@mcp.tool(task=TaskConfig(mode="optional"))
async def flexible_task() -> str:
return "Works either way"
-# Requires background execution - errors if client doesn't request task
+# Requires background execution - errors if the client didn't opt in
@mcp.tool(task=TaskConfig(mode="required"))
async def must_be_background() -> str:
return "Only runs as a background task"
@@ -104,18 +109,20 @@ The boolean shortcuts map to these modes:
- `task=True` → `TaskConfig(mode="optional")`
- `task=False` → `TaskConfig(mode="forbidden")`
+When a `mode="required"` tool is called by a client that didn't opt in, FastMCP returns a "missing required capability" error rather than running it synchronously.
+
### Poll Interval
-
-
-When clients poll for task status, the server tells them how frequently to check back. By default, FastMCP suggests a 5-second interval, but you can customize this per component:
+When a client polls for task status, the server can suggest how frequently to check back:
```python
from datetime import timedelta
from fastmcp import FastMCP
-from fastmcp.server.tasks import TaskConfig
+from fastmcp.utilities.tasks import TaskConfig
+from fastmcp_tasks import TasksExtension
mcp = FastMCP("MyServer")
+mcp.add_extension(TasksExtension())
# Poll every 2 seconds for a fast-completing task
@mcp.tool(task=TaskConfig(mode="optional", poll_interval=timedelta(seconds=2)))
@@ -128,31 +135,34 @@ async def slow_task() -> str:
return "Eventually done"
```
-Shorter intervals give clients faster feedback but increase server load. Longer intervals reduce load but delay status updates.
+Shorter intervals give clients faster feedback but increase server load. The interval is a ceiling, not an exact cadence — the FastMCP client starts polling quickly and backs off toward it, so a fast task is still observed as done almost immediately.
### Server-Wide Default
-To enable background task support for all components by default, pass `tasks=True` to the constructor. Individual decorators can still override this with `task=False`.
+To enable background task support for all tools by default, pass `tasks=True` to the constructor. Individual decorators can still override this with `task=False`.
```python
mcp = FastMCP("MyServer", tasks=True)
```
-If your server defines any synchronous tools, resources, or prompts, you will need to explicitly set `task=False` on their decorators to avoid an error.
+If your server defines any synchronous tools, you will need to explicitly set `task=False` on their decorators to avoid an error.
-### Graceful Degradation
-
-When a client requests background execution but the component has `mode="forbidden"`, FastMCP executes synchronously and returns the result inline. This follows the SEP-1686 specification for graceful degradation—clients can always request background execution without worrying about server capabilities.
-
-Conversely, when a component has `mode="required"` but the client doesn't request background execution, FastMCP returns an error indicating that task execution is required.
-
### Configuration
+`TasksExtension` takes the backend configuration directly, with `FASTMCP_DOCKET_*` environment variables as defaults — so `TasksExtension()` works out of the box against an env-configured deployment:
+
+```python
+mcp.add_extension(TasksExtension(url="redis://localhost:6379/0", concurrency=20))
+```
+
| Environment Variable | Default | Description |
|---------------------|---------|-------------|
| `FASTMCP_DOCKET_URL` | `memory://` | Backend URL (`memory://` or `redis://host:port/db`) |
+| `FASTMCP_DOCKET_NAME` | `fastmcp` | Queue name. Servers and workers sharing a name and URL share a queue. |
+| `FASTMCP_DOCKET_CONCURRENCY` | `10` | Maximum concurrent tasks per worker. |
+| `FASTMCP_TASKS_ENCRYPTION_KEY` | (unset) | Encrypts [task context snapshots at rest](#credentials-at-rest). Every server and worker sharing a queue must set the same key. |
## Backends
@@ -173,28 +183,54 @@ The in-memory backend (`memory://`) requires zero configuration and works out of
### Redis Backend
-For production deployments, use Redis (or Valkey) as your backend by setting `FASTMCP_DOCKET_URL=redis://localhost:6379`.
+For production deployments, use Redis (or Valkey) as your backend:
+
+```python
+mcp.add_extension(TasksExtension(url="redis://localhost:6379/0"))
+```
**Advantages:**
- **Persistent**: Tasks survive server restarts
- **Fast**: Single-digit millisecond task pickup latency
- **Scalable**: Add workers to distribute load across processes or machines
-## Workers
+### Credentials at Rest
-Every FastMCP server with task-enabled components automatically starts an **embedded worker**. You do not need to start a separate worker process for tasks to execute.
+A background task runs long after the request that submitted it has ended, but it still needs to know who asked for the work. FastMCP captures that identity at submission time in a **task context snapshot**: the caller's access token and every inbound HTTP header, including `Authorization`. The worker restores the snapshot before the tool body runs, so `get_access_token()` and `get_http_headers()` return the submitting caller.
-To scale horizontally, add more workers using the CLI:
+That snapshot lives in the backend for the task's TTL. With `memory://` it never leaves the process. With Redis or Valkey it is a stored value, and by default it is stored as plaintext JSON. A `rediss://` URL encrypts the connection, not the data the backend holds. Anyone who can read the backend can read the tokens.
+
+Set `FASTMCP_TASKS_ENCRYPTION_KEY` to encrypt the snapshot before it is written:
```bash
-fastmcp tasks worker server.py
+export FASTMCP_TASKS_ENCRYPTION_KEY=$(python -c "import secrets; print(secrets.token_urlsafe(32))")
+```
+
+
+Every server and worker on the same queue must set the same key. The process that restores a snapshot is rarely the one that captured it, and a worker with the wrong key cannot recover the caller.
+
+
+With a key configured, restore **fails closed**: a worker that cannot decrypt a snapshot fails the task instead of running the tool with no identity. This matters for a tool whose behavior depends on the caller: running it as an anonymous user is worse than not running it. The failure is reported to the client as a task error, and the server log names the key mismatch.
+
+Two consequences of failing closed are worth planning for. Tasks submitted before the key was set fail when a worker with the key picks them up, so drain the queue before you roll a key out. Rotating a key does the same to tasks in flight under the old one.
+
+The key protects the snapshot only. Tool arguments and any answers a task gathers through [mid-task input](#gathering-input-mid-task) are still stored as plaintext, so treat the backend as sensitive regardless.
+
+## Workers
+
+Every FastMCP server with task-enabled tools automatically starts an **embedded worker**. You do not need to start a separate worker process for tasks to execute.
+
+To scale horizontally, add more workers:
+
+```bash
+python -m fastmcp_tasks.worker_cli worker server.py
```
Each additional worker pulls tasks from the same queue, distributing load across processes. Configure worker concurrency via environment:
```bash
export FASTMCP_DOCKET_CONCURRENCY=20
-fastmcp tasks worker server.py
+python -m fastmcp_tasks.worker_cli worker server.py
```
@@ -202,7 +238,52 @@ Additional workers only work with Redis/Valkey backends. The in-memory backend i
-Task-enabled components must be defined at server startup to be registered with all workers. Components added dynamically after the server starts will not be available for background execution.
+Task-enabled tools must be defined at server startup to be registered with all workers. Tools added dynamically after the server starts will not be available for background execution.
+
+
+## Gathering Input Mid-Task
+
+A tool can ask the client a question partway through — the same [guard pattern](/servers/elicitation#elicitation-on-the-modern-protocol) used for multi-round-trip input on foreground calls: instead of awaiting a response, the tool *returns* one, and FastMCP re-runs it once the client answers.
+
+```python
+from fastmcp import Context, FastMCP
+from fastmcp_tasks import TasksExtension
+from mcp.types import (
+ ElicitRequest,
+ ElicitRequestFormParams,
+ ElicitResult,
+ InputRequiredResult,
+)
+
+mcp = FastMCP("MyServer")
+mcp.add_extension(TasksExtension())
+
+@mcp.tool(task=True)
+async def plan_dinner(ctx: Context) -> str | InputRequiredResult:
+ responses = ctx.input_responses
+ if responses is None:
+ # First leg: ask a question and end here.
+ request = ElicitRequest(
+ params=ElicitRequestFormParams(
+ message="What are you in the mood for?",
+ requested_schema={"type": "object", "properties": {"cuisine": {"type": "string"}}},
+ )
+ )
+ return InputRequiredResult(
+ result_type="input_required",
+ input_requests={"prefs": request},
+ )
+
+ # Re-entered leg: the client's answer is on ctx.input_responses.
+ answer = responses["prefs"]
+ assert isinstance(answer, ElicitResult)
+ return f"Tonight: {answer.content['cuisine']}!"
+```
+
+Run as a task, this "ends" the tool's first leg entirely rather than blocking a worker on the client's answer: the task reports `input_required`, the client answers, and FastMCP re-invokes the tool with the answer attached. No worker ever sits idle waiting on a round-trip — the same tool works identically whether it's called synchronously or as a background task, and a [FastMCP client](/clients/tasks) answers the question automatically through its elicitation handler.
+
+
+Imperative `await ctx.elicit(...)` is not supported inside a background task — it would require blocking a worker for the length of a client round-trip. Use the guard pattern (return `InputRequiredResult`) instead; calling `ctx.elicit()` from a task-enabled tool raises with guidance toward the guard pattern.
## Progress Reporting
@@ -241,7 +322,8 @@ FastMCP exposes Docket's full dependency injection system within your task-enabl
```python
from docket import Docket, Worker
from fastmcp import FastMCP
-from fastmcp.dependencies import Progress, CurrentDocket, CurrentWorker
+from fastmcp.dependencies import Progress
+from fastmcp_tasks.dependencies import CurrentDocket, CurrentWorker
mcp = FastMCP("MyServer")
@@ -260,4 +342,4 @@ async def my_task(
return "Done"
```
-With `CurrentDocket()`, you can schedule additional background tasks, chain work together, and coordinate complex workflows. See the [Docket documentation](https://chrisguidry.github.io/docket/) for the complete API, including retry policies, timeouts, and custom dependencies.
+With `CurrentDocket()`, you can schedule additional background tasks, chain work together, and coordinate complex workflows. See the [Docket documentation](https://docket.lol/) for the complete API, including retry policies, timeouts, and custom dependencies.
diff --git a/docs/servers/telemetry.mdx b/docs/servers/telemetry.mdx
index 1055dc0c0..9573f07bd 100644
--- a/docs/servers/telemetry.mdx
+++ b/docs/servers/telemetry.mdx
@@ -6,17 +6,37 @@ icon: chart-line
tag: NEW
---
-FastMCP includes native OpenTelemetry instrumentation for observability. Traces are automatically generated for tool, prompt, resource, and resource template operations, providing visibility into server behavior, request handling, and provider delegation chains.
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+FastMCP includes native OpenTelemetry instrumentation for observability. Traces are automatically generated for tool, prompt, resource, resource template, and task management operations, providing visibility into server behavior, request handling, and provider delegation chains.
## How It Works
FastMCP uses the OpenTelemetry API for instrumentation. This means:
-- **Zero configuration required** - Instrumentation is always active
+- **On by default** - Instrumentation is active out of the box, no opt-in required
- **No overhead when unused** - Without an SDK, all operations are no-ops
- **Bring your own SDK** - You control collection, export, and sampling
- **Works with any OTEL backend** - Jaeger, Zipkin, Datadog, New Relic, etc.
+Because FastMCP only depends on the OpenTelemetry API, span creation is a no-op until you configure an SDK and exporter — so being on by default costs nothing until you opt into collection.
+
+### Telemetry Modes
+
+
+
+`FASTMCP_TELEMETRY_MODE` (or `fastmcp.settings.telemetry_mode`) controls how much of the instrumentation is active:
+
+| Mode | FastMCP spans | Trace context |
+|---|---|---|
+| `native` (default) | Emitted | Propagated |
+| `propagation_only` | Suppressed | Propagated |
+| `off` | Suppressed | Untouched |
+
+Use `off` to disable FastMCP's instrumentation entirely. No spans are created even if an SDK is configured, and FastMCP leaves the surrounding OpenTelemetry context exactly as it found it.
+
+Use `propagation_only` when another instrumentation layer already owns the MCP span hierarchy — see [Interoperability](#interoperability) below.
+
## Enabling Telemetry
The easiest way to export traces is using `opentelemetry-instrument`, which configures the SDK automatically:
@@ -61,14 +81,15 @@ The server creates spans for each operation using [MCP semantic conventions](htt
| Span Name | Description |
|-----------|-------------|
| `tools/call {name}` | Tool execution (e.g., `tools/call get_weather`) |
-| `resources/read {uri}` | Resource read (e.g., `resources/read config://database`) |
+| `resources/read` | Resource read (URI in `mcp.resource.uri` attribute, not span name) |
| `prompts/get {name}` | Prompt render (e.g., `prompts/get greeting`) |
+| `tasks/{operation}` | Task management (`tasks/get`, `tasks/update`, or `tasks/cancel`) |
For mounted servers, an additional `delegate {name}` span shows the delegation to the child server.
### Client Spans
-The FastMCP client creates spans for outgoing requests with the same naming pattern (`tools/call {name}`, `resources/read {uri}`, `prompts/get {name}`).
+The FastMCP client creates spans for outgoing requests with the same naming pattern (`tools/call {name}`, `resources/read`, `prompts/get {name}`, and `tasks/{operation}`).
### Span Hierarchy
@@ -89,6 +110,85 @@ tools/call remote_search (CLIENT)
└── [remote server spans via trace context propagation]
```
+### Background tasks
+
+Background task traces have two parts:
+
+- Task submission and management requests use normal client-to-server context propagation. `tasks/get`, `tasks/update`, and `tasks/cancel` server spans are descendants of the corresponding FastMCP client spans.
+- Deferred execution runs in a Docket worker. Docket records its `CONSUMER` span as a new trace root with a span link to the submission context, rather than making it a child of the submission span. Custom spans created inside the task are children of that worker span.
+
+Span links preserve the causal relationship without forcing worker sampling to inherit the submit trace's sampling decision. Some tracing backends do not display links prominently, so the worker trace may look disconnected even though the link is present.
+
+Frequent status polling can produce more detail than you need. You can drop those client and server spans with a sampler that checks the span name before delegating to `ParentBased`:
+
+```python
+from opentelemetry import trace
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace.sampling import (
+ ALWAYS_ON,
+ Decision,
+ ParentBased,
+ Sampler,
+ SamplingResult,
+)
+
+
+class DropTaskPolls(Sampler):
+ def __init__(self):
+ self._delegate = ParentBased(ALWAYS_ON)
+
+ def should_sample(self, parent_context, trace_id, name, *args, **kwargs):
+ if name in {"tasks/get"}:
+ return SamplingResult(Decision.DROP)
+ return self._delegate.should_sample(
+ parent_context,
+ trace_id,
+ name,
+ *args,
+ **kwargs,
+ )
+
+ def get_description(self):
+ return "DropTaskPolls"
+
+
+provider = TracerProvider(sampler=DropTaskPolls())
+trace.set_tracer_provider(provider)
+```
+
+The name check must happen before `ParentBased` delegates. If the name-based sampler is nested inside `ParentBased`, it is not consulted for child spans whose parent was already sampled.
+
+## Interoperability
+
+
+
+FastMCP assumes it owns the MCP span hierarchy. When something else already owns it — an MCP-aware OpenTelemetry instrumentation library, or a service mesh that understands the protocol — FastMCP's spans duplicate what that layer already emits, and the same request shows up twice in your traces.
+
+Setting `propagation_only` resolves the duplication in FastMCP's favor of the other layer:
+
+```bash
+export FASTMCP_TELEMETRY_MODE=propagation_only
+```
+
+The distinction from `off` matters here. Both emit no FastMCP spans, but `off` is fully transparent, while `propagation_only` still extracts the trace context arriving in `_meta` and attaches it for the duration of the request. Spans created downstream — by your tool handlers, or by the instrumentation layer that owns the hierarchy — are parented to the calling trace rather than starting a new one. Outbound requests still carry `traceparent` and `tracestate` in `_meta`.
+
+### Suppressing spans for a single block
+
+Library authors embedding FastMCP inside their own instrumented stack often want to own the hierarchy for one specific operation rather than process-wide. `suppress_fastmcp_telemetry()` applies `propagation_only` semantics to a block:
+
+```python
+from fastmcp import Client
+from fastmcp.telemetry import suppress_fastmcp_telemetry
+
+async def search(client: Client, query: str):
+ with suppress_fastmcp_telemetry():
+ return await client.call_tool("search", {"query": query})
+```
+
+This is narrower than OpenTelemetry's global instrumentation suppression: only FastMCP's spans are skipped, so nested instrumentation for HTTP clients, databases, and everything else keeps emitting normally.
+
+The context manager has no effect when `telemetry_mode` is already `off`. A request to skip FastMCP's spans cannot re-enable the context propagation that `off` deliberately omits.
+
## Programmatic Configuration
For more control, configure the SDK in your Python code before importing FastMCP:
@@ -176,6 +276,73 @@ async def complex_operation(input: str) -> str:
return result
```
+### Where custom spans help most
+
+Custom spans are most useful around work that is expensive or hard to debug:
+
+- External calls such as databases, vector stores, HTTP APIs, or queue operations
+- Multi-step tool logic where one stage dominates latency
+- Prompt or resource generation that fans out to other systems
+- LLM calls a tool makes to a model provider
+
+Avoid wrapping every small helper function or simple in-memory transformation. That usually adds noise without making traces easier to interpret.
+
+### Recommended naming and attributes
+
+- Use `{tool_name}.{operation}` or `{resource_name}.{operation}` for child spans such as `search.fetch`, `search.rank`, or `docs.render`
+- Add attributes that explain workload shape, such as counts, sizes, cache hits, or IDs
+- Do not record secrets, prompts with sensitive user data, or raw tokens as span attributes
+- Let exceptions propagate unless you have a specific recovery path; FastMCP's server spans already mark failures and record exceptions
+
+### Instrumenting tools, prompts, and resources
+
+```python
+from fastmcp import FastMCP
+from fastmcp.telemetry import get_tracer
+
+mcp = FastMCP("my-server")
+
+@mcp.tool
+async def search(query: str) -> str:
+ tracer = get_tracer()
+
+ with tracer.start_as_current_span("search.fetch") as span:
+ span.set_attribute("search.query_length", len(query))
+ results = await fetch_results(query)
+ span.set_attribute("search.result_count", len(results))
+
+ with tracer.start_as_current_span("search.rank"):
+ ranked = rank_results(results)
+
+ return format_results(ranked)
+
+@mcp.prompt
+async def summarize_prompt(topic: str) -> str:
+ tracer = get_tracer()
+ with tracer.start_as_current_span("summarize_prompt.render") as span:
+ span.set_attribute("prompt.topic_length", len(topic))
+ return f"Summarize the latest updates about {topic}."
+
+@mcp.resource("docs://{slug}")
+async def docs_resource(slug: str) -> str:
+ tracer = get_tracer()
+ with tracer.start_as_current_span("docs_resource.load") as span:
+ span.set_attribute("docs.slug", slug)
+ return await load_doc(slug)
+```
+
+### LLM calls inside tools
+
+A tool that [calls an LLM directly](/servers/sampling) should keep the model work nested under the tool span, so traces show application logic and model latency together.
+
+For providers with their own OTEL integrations, prefer enabling that instrumentation rather than manually creating a span around every model call. For example, if you use Google GenAI, `logfire.instrument_google_genai()` will emit child spans with token and request metadata under the active FastMCP tool span.
+
+### Exporter choices
+
+- For local debugging, `ConsoleSpanExporter` or `otel-desktop-viewer` gives quick feedback with minimal setup
+- For shared environments, use OTLP exporters to backends like Logfire, Jaeger, Tempo, Datadog, or New Relic
+- If traces are too noisy, tune sampling in your OpenTelemetry SDK instead of removing FastMCP instrumentation
+
## Error Handling
When errors occur, spans are automatically marked with error status and the exception is recorded:
@@ -186,21 +353,16 @@ def risky_operation() -> str:
raise ValueError("Something went wrong")
# The span will have:
-# - status = ERROR
+# - status = ERROR with exception message as description
+# - error.type = "tool_error" (or exception class name for non-tool errors)
# - exception event with stack trace
```
## Attributes Reference
-### RPC Semantic Conventions
-
-Standard [RPC semantic conventions](https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/):
-
-| Attribute | Value |
-|-----------|-------|
-| `rpc.system` | `"mcp"` |
-| `rpc.service` | Server name |
-| `rpc.method` | MCP protocol method |
+
+**Migrating from v3.1 or earlier:** The `rpc.system`, `rpc.service`, and `rpc.method` span attributes were removed in favor of the [MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/) listed below. If you have dashboards or alerts keyed on those `rpc.*` attributes, update them to use `mcp.method.name` and the `fastmcp.*` attributes instead.
+
### MCP Semantic Conventions
@@ -208,9 +370,13 @@ FastMCP implements the [OpenTelemetry MCP semantic conventions](https://opentele
| Attribute | Description |
|-----------|-------------|
-| `mcp.method.name` | The MCP method being called (`tools/call`, `resources/read`, `prompts/get`) |
+| `mcp.method.name` | The MCP method being called (`tools/call`, `resources/read`, `prompts/get`, `tasks/get`, etc.) |
+| `mcp.protocol.version` | The negotiated MCP protocol version for the request |
| `mcp.session.id` | Session identifier for the MCP connection |
| `mcp.resource.uri` | The resource URI (for resource operations) |
+| `gen_ai.tool.name` | Tool name (on `tools/call` spans) |
+| `gen_ai.prompt.name` | Prompt name (on `prompts/get` spans) |
+| `error.type` | Error classification (`tool_error` for ToolError, otherwise exception class name) |
### Auth Attributes
@@ -229,7 +395,7 @@ All custom attributes use the `fastmcp.` prefix for features unique to FastMCP:
|-----------|-------------|
| `fastmcp.server.name` | Server name |
| `fastmcp.component.type` | `tool`, `resource`, `prompt`, or `resource_template` |
-| `fastmcp.component.key` | Full component identifier (e.g., `tool:greet`) |
+| `fastmcp.component.key` | Full component key, including type and version delimiter (e.g., `tool:greet@` or `tool:greet@v2`) |
| `fastmcp.provider.type` | Provider class (`LocalProvider`, `FastMCPProvider`, `ProxyProvider`) |
Provider-specific attributes for delegation context:
diff --git a/docs/servers/tool-fingerprinting.mdx b/docs/servers/tool-fingerprinting.mdx
new file mode 100644
index 000000000..b8c06c04e
--- /dev/null
+++ b/docs/servers/tool-fingerprinting.mdx
@@ -0,0 +1,156 @@
+---
+title: Tool Fingerprinting
+sidebarTitle: Tool Fingerprinting
+description: Build stable fingerprints for tool identity and schema change detection
+icon: fingerprint
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx";
+
+
+
+Downstream systems like routers, gateways, and audit loggers often need to detect whether a tool's schema changed between deployments. Rather than each system inventing its own JSON normalization and hashing logic, you can build stable fingerprints from FastMCP's existing API surface.
+
+FastMCP does not define a single "contract hash" because the inclusion policy is necessarily application-specific: some systems care only about the input schema, others include the description, metadata, tags, or version. Instead, this recipe shows how to assemble a fingerprint payload from the parts you care about, then hash it deterministically.
+
+## The Recipe
+
+The two key building blocks are:
+
+- **`tool.key`** — FastMCP's canonical component identity, encoding type, name, and version (e.g. `tool:greet@1.0` or `tool:greet@`)
+- **`tool.to_mcp_tool()`** — the protocol-facing tool object that MCP clients see, including the input schema
+
+Combine them into a payload, serialize deterministically, and hash:
+
+```python
+import hashlib
+import json
+
+from fastmcp import FastMCP
+
+mcp = FastMCP("demo")
+
+
+@mcp.tool()
+def greet(name: str) -> str:
+ """Say hello."""
+ return f"Hello {name}"
+
+
+async def fingerprint_tool(server: FastMCP, tool_name: str) -> str:
+ tool = await server.get_tool(tool_name)
+ if tool is None:
+ raise ValueError(f"Tool {tool_name!r} not found")
+
+ mcp_tool = tool.to_mcp_tool()
+ dumped = mcp_tool.model_dump(mode="json", by_alias=True, exclude_none=True)
+
+ payload = {
+ "key": tool.key,
+ "inputSchema": dumped["inputSchema"],
+ }
+
+ canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
+```
+
+The fingerprint is stable across process restarts as long as the tool's name, version, and input schema remain the same.
+
+## Why `tool.key`?
+
+`tool.key` is FastMCP's canonical component identity. It encodes the component type, identifier, and version into a single string:
+
+```
+tool:greet@1.0 # versioned tool
+tool:greet@ # unversioned tool
+```
+
+Using `key` rather than just the tool name ensures that two versions of the same tool produce distinct fingerprints, and that a tool and a resource with the same name cannot collide.
+
+## Why `to_mcp_tool()`?
+
+`to_mcp_tool()` returns the protocol-facing representation — the shape that MCP clients actually receive. This matters because routers and gateways typically operate on the protocol layer, not FastMCP internals. The `model_dump(mode="json", by_alias=True, exclude_none=True)` call produces a clean, serializable dictionary using the MCP protocol field names.
+
+## Customizing the Payload
+
+You own the inclusion policy. Add or remove fields depending on what constitutes a "contract" in your system:
+
+```python
+async def custom_fingerprint(server: FastMCP, tool_name: str) -> str:
+ tool = await server.get_tool(tool_name)
+ if tool is None:
+ raise ValueError(f"Tool {tool_name!r} not found")
+
+ mcp_tool = tool.to_mcp_tool()
+ dumped = mcp_tool.model_dump(mode="json", by_alias=True, exclude_none=True)
+
+ # Include description to detect documentation drift
+ payload = {
+ "key": tool.key,
+ "inputSchema": dumped["inputSchema"],
+ "description": dumped.get("description"),
+ }
+
+ canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
+```
+
+Common variations:
+
+| Field | When to include |
+| -------------- | -------------------------------------------------------------------------- |
+| `inputSchema` | Always — this is the core contract |
+| `description` | When documentation drift matters (e.g. LLM routing decisions depend on it) |
+| `outputSchema` | When downstream consumers validate response shapes |
+| `annotations` | When behavioral hints (read-only, destructive) affect routing |
+| `_meta` | When custom metadata drives policy decisions |
+
+## Detecting Schema Drift in CI
+
+Store fingerprints as artifacts and compare between deployments:
+
+```python
+import json
+import hashlib
+from pathlib import Path
+
+from fastmcp import FastMCP
+
+
+async def generate_manifest(server: FastMCP) -> dict[str, str]:
+ """Generate a fingerprint manifest for all tools."""
+ manifest = {}
+
+ for tool in await server.list_tools():
+ mcp_tool = tool.to_mcp_tool()
+ dumped = mcp_tool.model_dump(mode="json", by_alias=True, exclude_none=True)
+
+ payload = {
+ "key": tool.key,
+ "inputSchema": dumped["inputSchema"],
+ }
+
+ canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
+ manifest[tool.key] = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
+
+ return manifest
+
+
+async def check_drift(server: FastMCP, baseline_path: Path) -> list[str]:
+ """Compare current fingerprints against a stored baseline."""
+ current = await generate_manifest(server)
+ baseline = json.loads(baseline_path.read_text())
+
+ changed = []
+ for key, fingerprint in current.items():
+ if baseline.get(key) != fingerprint:
+ changed.append(key)
+
+ for key in baseline:
+ if key not in current:
+ changed.append(key)
+
+ return changed
+```
+
+Run `generate_manifest` in CI after each build and compare against the previous run. Any differences indicate a schema change that downstream consumers should be aware of.
diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx
index 91d96d1be..4a9c4918d 100644
--- a/docs/servers/tools.mdx
+++ b/docs/servers/tools.mdx
@@ -36,7 +36,7 @@ def add(a: int, b: int) -> int:
When this tool is registered, FastMCP automatically:
- Uses the function name (`add`) as the tool name.
-- Uses the function's docstring (`Adds two integer numbers...`) as the tool description.
+- Parses the function's docstring for the tool description and, if present, per-parameter descriptions (see [Docstring Descriptions](#docstring-descriptions)).
- Generates an input schema based on the function's parameters and type annotations.
- Handles parameter validation and error reporting.
@@ -70,17 +70,17 @@ def search_products_implementation(query: str, category: str | None = None) -> l
- Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose
+ Provides the description exposed via MCP. If set, the function's docstring is ignored for the tool description, though docstring-derived parameter descriptions still apply (see [Docstring Descriptions](#docstring-descriptions)).
+
+
+
+ A human-readable display title for the tool. If omitted, FastMCP falls back to `annotations.title` when present, then to a title derived from the tool's name (e.g. `find_products` becomes "Find Products") — some MCP clients drop tools that have no title at all.
A set of strings used to categorize the tool. These can be used by the server and, in some cases, by clients to filter or group available tools.
-
- Deprecated in v3.0.0. Use `mcp.enable()` / `mcp.disable()` at the server level instead.
- A boolean to enable or disable the tool. See [Component Visibility](#component-visibility) for the recommended approach.
-
@@ -132,6 +132,10 @@ def search_products_implementation(query: str, category: str | None = None) -> l
Optional JSON schema for the tool's output. When provided, the tool must return structured output matching this schema. If not provided, FastMCP automatically generates a schema from the function's return type annotation. See [Output Schemas](#output-schemas) for details.
+
+
+ Applies to sync tool functions only. When `True` (default), sync functions are dispatched to a thread pool so they don't block the event loop. Set to `False` to run the function inline on the event loop thread — useful for libraries with thread affinity like Windows COM (`pywin32`, `uiautomation`, `comtypes`), `tkinter`, or certain GPU/driver bindings. Ignored for async functions, which always run on the event loop. See [Thread affinity](#thread-affinity) for details.
+
### Using with Methods
@@ -175,6 +179,28 @@ def slow_tool(x: int) -> int:
For I/O-bound operations like network requests or database queries, async tools are still preferred since they're more efficient than threadpool dispatch. Use sync tools when working with synchronous libraries or for simple operations where the threading overhead doesn't matter.
+### Thread affinity
+
+This section applies to sync tools only. Async tools already run on the event loop and are not affected.
+
+Some libraries bind state to the thread they're first used from and break when called from a different thread. The most common case is Windows COM — libraries like `uiautomation`, `comtypes`, and parts of `pywin32` require `CoInitialize` to have been called on the current thread, and worker-pool threads don't initialize COM by default. Similar constraints apply to `tkinter`, some GPU bindings (CUDA contexts), and certain hardware drivers.
+
+For these cases, pass `run_in_thread=False` so FastMCP invokes the sync function inline on the event loop thread instead of dispatching it to a worker:
+
+```python
+import uiautomation as auto
+
+@mcp.tool(run_in_thread=False)
+def list_windows() -> list[str]:
+ """List desktop windows via Windows UI Automation (COM)."""
+ desktop = auto.GetRootControl()
+ return [w.Name for w in desktop.GetChildren()[:5]]
+```
+
+The tradeoff is that the event loop is blocked for the duration of the call — other in-flight requests wait until the tool returns. Keep `run_in_thread=False` reserved for tools that genuinely need thread affinity, and prefer short-running calls in that path.
+
+Inline sync calls have no cancellation checkpoints, so `timeout` cannot interrupt them. Combining `timeout` with `run_in_thread=False` on a sync function is rejected at registration — drop one or the other.
+
## Arguments
By default, FastMCP converts Python functions into MCP tools by inspecting the function's signature and type annotations. This allows you to use standard Python type annotations for your tools. In general, the framework strives to "just work": idiomatic Python behaviors like parameter defaults and type annotations are automatically translated into MCP schemas. However, there are a number of ways to customize the behavior of your tools.
@@ -289,6 +315,33 @@ The default flexible validation mode is recommended for most use cases as it han
You can provide additional metadata about parameters in several ways:
+#### Docstring Descriptions
+
+
+
+FastMCP parses your function's docstring to extract both the tool description and per-parameter descriptions. Google, NumPy, and Sphinx docstring styles are all supported — the parser tries each and uses whichever finds parameter descriptions:
+
+```python
+@mcp.tool
+def process_image(
+ image_url: str,
+ resize: bool = False,
+ width: int = 800,
+) -> dict:
+ """Process an image with optional resizing.
+
+ Args:
+ image_url: URL of the image to process.
+ resize: Whether to resize the image.
+ width: Target width in pixels.
+ """
+ # Implementation...
+```
+
+The free-form text above the `Args` section — whether a single line or multiple paragraphs — becomes the tool description, and each parameter's docstring entry becomes the description for that parameter in the generated schema. Sections like `Returns`, `Raises`, and `Example` are excluded from the description but otherwise ignored.
+
+If a parameter already has an explicit description — via `Annotated[x, "..."]` or `Field(description=...)` — that description takes precedence over the docstring. This makes it safe to adopt docstring-based descriptions incrementally: existing annotations keep working, and docstrings fill in the gaps.
+
#### Simple String Descriptions
@@ -378,7 +431,7 @@ def get_user_details(user_id: str = Depends(get_user_id)) -> str:
return f"Details for {user_id}"
```
-See [Custom Dependencies](/servers/context#custom-dependencies) for more details on dependency injection.
+See [Custom Dependencies](/servers/dependency-injection#custom-dependencies) for more details on dependency injection.
## Return Values
@@ -669,7 +722,7 @@ Schema generation works for most common types including basic types, collections
For complete control over tool responses, return a `ToolResult` object. This gives you explicit control over all aspects of the tool's output: traditional content, structured data, and metadata.
```python
-from fastmcp.tools.tool import ToolResult
+from fastmcp.tools import ToolResult
from mcp.types import TextContent
@mcp.tool
@@ -693,7 +746,7 @@ ToolResult(content="Hello, world!")
# List of content blocks
ToolResult(content=[
TextContent(type="text", text="Result: 42"),
- ImageContent(type="image", data="base64...", mimeType="image/png")
+ ImageContent(type="image", data="base64...", mime_type="image/png")
])
```
@@ -735,7 +788,7 @@ When you need custom serialization (like YAML, Markdown tables, or specialized f
```python
import yaml
from fastmcp import FastMCP
-from fastmcp.tools.tool import ToolResult
+from fastmcp.tools import ToolResult
mcp = FastMCP("MyServer")
@@ -865,7 +918,7 @@ def public_action() -> str:
return "Done"
# Disable specific tools by key
-mcp.disable(keys={"tool:admin_action"})
+mcp.disable(names={"admin_action"})
# Disable tools by tag
mcp.disable(tags={"admin"})
@@ -888,15 +941,17 @@ Annotations serve several purposes in client applications:
- Describing the safety profile of tools (destructive vs. non-destructive)
- Signaling if tools interact with external systems
-You can add annotations to a tool using the `annotations` parameter in the `@mcp.tool` decorator:
+You can add annotations to a tool using the `annotations` parameter in the `@mcp.tool` decorator. FastMCP accepts either a plain dict or `ToolAnnotations`; the examples below use `ToolAnnotations` for consistency and stronger editor/type support.
```python
+from mcp.types import ToolAnnotations
+
@mcp.tool(
- annotations={
- "title": "Calculate Sum",
- "readOnlyHint": True,
- "openWorldHint": False
- }
+ annotations=ToolAnnotations(
+ title="Calculate Sum",
+ readOnlyHint=True,
+ openWorldHint=False,
+ )
)
def calculate_sum(a: float, b: float) -> float:
"""Add two numbers together."""
@@ -932,7 +987,7 @@ from mcp.types import ToolAnnotations
mcp = FastMCP("Data Server")
-@mcp.tool(annotations={"readOnlyHint": True})
+@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
def get_user(user_id: str) -> dict:
"""Retrieve user information by ID."""
return {"id": user_id, "name": "Alice"}
@@ -954,7 +1009,7 @@ def update_user(user_id: str, name: str) -> dict:
"""Update user information."""
return {"id": user_id, "name": name, "updated": True}
-@mcp.tool(annotations={"destructiveHint": True})
+@mcp.tool(annotations=ToolAnnotations(destructiveHint=True))
def delete_user(user_id: str) -> dict:
"""Permanently delete a user account."""
return {"deleted": user_id}
@@ -979,8 +1034,8 @@ def example_tool() -> str:
# These operations trigger notifications:
mcp.add_tool(example_tool) # Sends tools/list_changed notification
-mcp.disable(keys={"tool:example_tool"}) # Sends tools/list_changed notification
-mcp.enable(keys={"tool:example_tool"}) # Sends tools/list_changed notification
+mcp.disable(names={"example_tool"}) # Sends tools/list_changed notification
+mcp.enable(names={"example_tool"}) # Sends tools/list_changed notification
mcp.local_provider.remove_tool("example_tool") # Sends tools/list_changed notification
```
@@ -1001,22 +1056,14 @@ mcp = FastMCP(name="ContextDemo")
async def process_data(data_uri: str, ctx: Context) -> dict:
"""Process data from a resource with progress reporting."""
await ctx.info(f"Processing data from {data_uri}")
-
- # Read a resource
- resource = await ctx.read_resource(data_uri)
- data = resource[0].content if resource else ""
-
- # Report progress
+
+ result = await ctx.read_resource(data_uri)
+ data = result.contents[0].content if result.contents else ""
await ctx.report_progress(progress=50, total=100)
-
- # Example request to the client's LLM for help
- summary = await ctx.sample(f"Summarize this in 10 words: {data[:200]}")
-
+
+ summary = str(data)[:200]
await ctx.report_progress(progress=100, total=100)
- return {
- "length": len(data),
- "summary": summary.text
- }
+ return {"length": len(data), "summary": summary}
```
The Context object provides access to:
@@ -1024,7 +1071,6 @@ The Context object provides access to:
- **Logging**: `ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`
- **Progress Reporting**: `ctx.report_progress(progress, total)`
- **Resource Access**: `ctx.read_resource(uri)`
-- **LLM Sampling**: `ctx.sample(...)`
- **Request Information**: `ctx.request_id`, `ctx.client_id`
For full documentation on the Context object and all its capabilities, see the [Context documentation](/servers/context).
@@ -1035,22 +1081,22 @@ For full documentation on the Context object and all its capabilities, see the [
-You can control how the FastMCP server behaves if you try to register multiple tools with the same name. This is configured using the `on_duplicate_tools` argument when creating the `FastMCP` instance.
+You can control how the FastMCP server behaves if you register the same component twice. Identity is the component's type, name, and version together, so a tool and a prompt may share a name, and two versions of one tool coexist. Only an exact repeat of all three counts as a duplicate. The `on_duplicate` argument sets that policy once for every component type.
```python
from fastmcp import FastMCP
mcp = FastMCP(
name="StrictServer",
- # Configure behavior for duplicate tool names
- on_duplicate_tools="error"
+ # Configure behavior for exact component duplicates
+ on_duplicate="error"
)
@mcp.tool
def my_tool(): return "Version 1"
# This will now raise a ValueError because 'my_tool' already exists
-# and on_duplicate_tools is set to "error".
+# and on_duplicate is set to "error".
# @mcp.tool
# def my_tool(): return "Version 2"
```
diff --git a/docs/servers/transforms/code-mode.mdx b/docs/servers/transforms/code-mode.mdx
index f09c22b6f..0e7e4f50d 100644
--- a/docs/servers/transforms/code-mode.mdx
+++ b/docs/servers/transforms/code-mode.mdx
@@ -140,7 +140,7 @@ You can cap result count with `default_limit`. The LLM can also override the lim
Search(default_limit=5) # return at most 5 results per search
```
-If your tools use [tags](/servers/tools#tags), Search also accepts a `tags` parameter so the LLM can narrow results to specific categories before searching.
+If your tools use [tags](/servers/visibility#tags), Search also accepts a `tags` parameter so the LLM can narrow results to specific categories before searching.
### GetSchemas
@@ -148,7 +148,7 @@ If your tools use [tags](/servers/tools#tags), Search also accepts a `tags` para
### GetTags
-`GetTags` lets the LLM browse tools by category using [tag](/servers/tools#tags) metadata. At brief detail, the LLM sees tag names with counts. At full detail, it sees tools listed under each tag:
+`GetTags` lets the LLM browse tools by category using [tag](/servers/visibility#tags) metadata. At brief detail, the LLM sees tag names with counts. At full detail, it sees tools listed under each tag:
```
- math (3 tools)
@@ -187,7 +187,7 @@ from fastmcp.experimental.transforms.code_mode import CodeMode
mcp = FastMCP("Server", transforms=[CodeMode()])
```
-If your tools use [tags](/servers/tools#tags), add `GetTags` so the LLM can browse by category before searching — giving it four stages of progressive disclosure:
+If your tools use [tags](/servers/visibility#tags), add `GetTags` so the LLM can browse by category before searching — giving it four stages of progressive disclosure:
```python
from fastmcp import FastMCP
@@ -250,7 +250,7 @@ Here's a minimal example:
from fastmcp.experimental.transforms.code_mode import CodeMode
from fastmcp.experimental.transforms.code_mode import GetToolCatalog, GetSchemas
from fastmcp.server.context import Context
-from fastmcp.tools.tool import Tool
+from fastmcp.tools import Tool
def list_all_tools(get_catalog: GetToolCatalog) -> Tool:
async def list_tools(ctx: Context) -> str:
@@ -285,7 +285,17 @@ mcp = FastMCP("Server", transforms=[code_mode])
### Resource Limits
-The default `MontySandboxProvider` can enforce execution limits — timeouts, memory caps, recursion depth, and more. Without limits, LLM-generated scripts can run indefinitely.
+The default `MontySandboxProvider` enforces execution limits — timeouts, memory caps, recursion depth, and more.
+
+Constructed with no arguments, it applies a conservative baseline so the out-of-box configuration is not unbounded: `max_duration_secs=30` and `max_memory=100_000_000` (100 MB). Pass an explicit `limits` dict to override it, or `limits=None` to run with no limits at all:
+
+```python
+from fastmcp.experimental.transforms.code_mode import MontySandboxProvider
+
+MontySandboxProvider() # baseline: 30s, 100 MB
+MontySandboxProvider(limits={...}) # your own limits
+MontySandboxProvider(limits=None) # explicitly uncapped
+```
```python
from fastmcp.experimental.transforms.code_mode import CodeMode
@@ -308,6 +318,18 @@ All keys are optional — omit any to leave that dimension uncapped:
| `max_recursion_depth` | `int` | Maximum recursion depth |
| `gc_interval` | `int` | Garbage collection frequency |
+### Tool Call Limits
+
+A single `execute` block can issue many `call_tool()` invocations — a loop in LLM-generated code can fan out into a large number of backend operations from one request. `CodeMode` caps this at `max_tool_calls` (default `50`); exceeding it raises a `ToolError`. Pass `None` for no cap:
+
+```python
+from fastmcp.experimental.transforms.code_mode import CodeMode
+
+CodeMode() # default: 50 call_tool() calls per execute()
+CodeMode(max_tool_calls=200) # raise the cap
+CodeMode(max_tool_calls=None) # no cap
+```
+
### Custom Sandbox Providers
You can replace the default sandbox with any object implementing the `SandboxProvider` protocol:
diff --git a/docs/servers/transforms/namespace.mdx b/docs/servers/transforms/namespace.mdx
index 715bebd39..fdb0d1c7f 100644
--- a/docs/servers/transforms/namespace.mdx
+++ b/docs/servers/transforms/namespace.mdx
@@ -3,7 +3,6 @@ title: Namespace Transform
sidebarTitle: Namespace
description: Prefix component names to prevent conflicts
icon: tag
-tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
diff --git a/docs/servers/transforms/prompts-as-tools.mdx b/docs/servers/transforms/prompts-as-tools.mdx
index b68c0891d..6a9ab1b47 100644
--- a/docs/servers/transforms/prompts-as-tools.mdx
+++ b/docs/servers/transforms/prompts-as-tools.mdx
@@ -21,7 +21,11 @@ This means any client that can call tools can now access prompts, even if the cl
## Basic Usage
-Pass your server to `PromptsAsTools` when adding the transform. The transform queries that server for prompts whenever the generated tools are called.
+Pass your FastMCP server to `PromptsAsTools` when adding the transform. The generated tools route through the server at runtime, which means all server middleware — auth, visibility, rate limiting — applies to prompt operations automatically, exactly as it would for direct `prompts/get` calls.
+
+
+`PromptsAsTools` (and `ResourcesAsTools`) should be applied to a FastMCP server instance, not a raw Provider. The generated tools call back into the server's middleware chain at runtime, so they need a server to route through. If you want to expose only a subset of prompts, create a dedicated FastMCP server for those prompts and apply the transform there.
+
```python
from fastmcp import FastMCP
diff --git a/docs/servers/transforms/resources-as-tools.mdx b/docs/servers/transforms/resources-as-tools.mdx
index 8c7e8bbed..b79980dcc 100644
--- a/docs/servers/transforms/resources-as-tools.mdx
+++ b/docs/servers/transforms/resources-as-tools.mdx
@@ -21,7 +21,11 @@ This means any client that can call tools can now access resources, even if the
## Basic Usage
-Pass your server to `ResourcesAsTools` when adding the transform. The transform queries that server for resources whenever the generated tools are called.
+Pass your FastMCP server to `ResourcesAsTools` when adding the transform. The generated tools route through the server at runtime, which means all server middleware — auth, visibility, rate limiting — applies to resource operations automatically, exactly as it would for direct `resources/read` calls.
+
+
+`ResourcesAsTools` (and `PromptsAsTools`) should be applied to a FastMCP server instance, not a raw Provider. The generated tools call back into the server's middleware chain at runtime, so they need a server to route through. If you want to expose only a subset of resources, create a dedicated FastMCP server for those resources and apply the transform there.
+
```python
from fastmcp import FastMCP
@@ -45,6 +49,8 @@ mcp.add_transform(ResourcesAsTools(mcp))
Clients now see three tools: whatever tools you defined directly, plus `list_resources` and `read_resource`.
+Both generated tools are annotated with `readOnlyHint=True`, since they only read data. Clients that respect tool annotations (like Cursor) can use this to auto-confirm these tool calls without prompting the user.
+
## Static Resources vs Templates
Resources come in two forms, and the `list_resources` tool distinguishes between them in its JSON output.
diff --git a/docs/servers/transforms/tool-search.mdx b/docs/servers/transforms/tool-search.mdx
index 204004f5c..c3f44a3cf 100644
--- a/docs/servers/transforms/tool-search.mdx
+++ b/docs/servers/transforms/tool-search.mdx
@@ -153,6 +153,8 @@ Tools discovered through search can also be called directly via `client.call_too
Search results respect the full authorization pipeline. Tools filtered by middleware, visibility transforms, or component-level auth checks won't appear in search results.
+App-only tools are excluded too. A [MCP app](/apps/overview) can declare backend tools that only its UI may call, and normally the host keeps those from the model. A search result is tool output rather than an advertised listing, so no host filtering applies to it — the exclusion happens here instead. The `call_tool` proxy enforces the same boundary, since it executes a name the model supplies.
+
The search tool queries `list_tools()` through the complete pipeline at search time, so the same filtering that controls what a client sees in the listing also controls what they can discover through search.
```python
diff --git a/docs/servers/transforms/tool-transformation.mdx b/docs/servers/transforms/tool-transformation.mdx
index 393b47b5a..a50513f87 100644
--- a/docs/servers/transforms/tool-transformation.mdx
+++ b/docs/servers/transforms/tool-transformation.mdx
@@ -3,7 +3,6 @@ title: Tool Transformation
sidebarTitle: Tool Transformation
description: Modify tool schemas - rename, reshape arguments, and customize behavior
icon: wrench
-tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
diff --git a/docs/servers/transforms/transforms.mdx b/docs/servers/transforms/transforms.mdx
index 4008f86b4..fc8f0ceda 100644
--- a/docs/servers/transforms/transforms.mdx
+++ b/docs/servers/transforms/transforms.mdx
@@ -3,7 +3,6 @@ title: Transforms Overview
sidebarTitle: Overview
description: Modify components as they flow through your server
icon: wand-magic-sparkles
-tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
@@ -119,7 +118,7 @@ Create custom transforms by subclassing `Transform` and overriding the methods y
```python
from collections.abc import Sequence
from fastmcp.server.transforms import Transform, GetToolNext
-from fastmcp.tools.tool import Tool
+from fastmcp.tools import Tool
class TagFilter(Transform):
"""Filter tools to only those with specific tags."""
diff --git a/docs/servers/versioning.mdx b/docs/servers/versioning.mdx
index 4c44a73bd..0dfa0bc69 100644
--- a/docs/servers/versioning.mdx
+++ b/docs/servers/versioning.mdx
@@ -114,7 +114,7 @@ def process(data: str, mode: str = "default") -> str:
return data.upper()
```
-Both versions are registered. When a client lists tools, they see only `process` with version 2.0 (the highest). When they invoke `process`, version 2.0 executes. The same pattern applies to resources and prompts.
+Both versions are registered. Server-side `list_tools()` returns every registered version, while MCP client list operations deduplicate by identifier and expose the highest version with metadata describing the available versions. When a client invokes `process` without specifying a version, version 2.0 executes. The same pattern applies to resources and prompts.
### Versioned vs Unversioned Components
@@ -189,7 +189,7 @@ By default, clients receive and invoke the highest version of each component. Wh
### FastMCP Client
-The FastMCP client's `call_tool` and `get_prompt` methods accept an optional `version` parameter. When specified, the server executes that exact version instead of the highest.
+The FastMCP client's `call_tool`, `read_resource`, and `get_prompt` methods accept an optional `version` parameter. When specified, the server executes that exact version instead of the highest.
```python
from fastmcp import Client
@@ -201,6 +201,9 @@ async with Client(server) as client:
# Call a specific version
result_v1 = await client.call_tool("calculate", {"x": 1, "y": 2}, version="1.0")
+ # Read a specific resource version
+ data = await client.read_resource("config://app", version="1.0")
+
# Get a specific prompt version
prompt = await client.get_prompt("summarize", {"text": "..."}, version="1.0")
```
@@ -209,13 +212,16 @@ If the requested version doesn't exist, the server raises a `NotFoundError`. Thi
### MCP Protocol
-For generic MCP clients that don't have built-in version support, pass the version through the `_meta` field in arguments. FastMCP servers extract the version from `_meta.fastmcp.version` before processing.
+For generic MCP clients that don't have built-in version support, pass the version through the request params `_meta` field. FastMCP servers extract the version from `_meta.fastmcp.version` before processing.
-```json Tool Call Arguments
+```json Tool Call Request Params
{
- "x": 1,
- "y": 2,
+ "name": "calculate",
+ "arguments": {
+ "x": 1,
+ "y": 2
+ },
"_meta": {
"fastmcp": {
"version": "1.0"
@@ -224,9 +230,12 @@ For generic MCP clients that don't have built-in version support, pass the versi
}
```
-```json Prompt Arguments
+```json Prompt Request Params
{
- "text": "Summarize this document...",
+ "name": "summarize",
+ "arguments": {
+ "text": "Summarize this document..."
+ },
"_meta": {
"fastmcp": {
"version": "1.0"
diff --git a/docs/servers/visibility.mdx b/docs/servers/visibility.mdx
index 509bd7068..edd3127d4 100644
--- a/docs/servers/visibility.mdx
+++ b/docs/servers/visibility.mdx
@@ -57,29 +57,27 @@ mcp.enable(tags={"admin"})
# Clients now see all three tools
```
-## Keys and Tags
+## Targeting Components
-Visibility filtering works with two identifiers: keys (for specific components) and tags (for groups).
+Every filter parameter narrows the same way: `enable()` and `disable()` act on the components matching all the criteria you supply. Reach for the simplest one that expresses your intent — usually names or tags.
-### Component Keys
+### Names
-Every component has a unique key in the format `{type}:{identifier}`.
-
-| Component | Key Format | Example |
-|-----------|------------|---------|
-| Tool | `tool:{name}` | `tool:delete_everything` |
-| Resource | `resource:{uri}` | `resource:data://config` |
-| Template | `template:{uri}` | `template:file://{path}` |
-| Prompt | `prompt:{name}` | `prompt:analyze` |
-
-Use keys to target specific components.
+`names` matches components by their name, or by their URI for resources and templates. This is the common case.
```python
# Disable a specific tool
-mcp.disable(keys={"tool:delete_everything"})
+mcp.disable(names={"delete_everything"})
-# Disable multiple specific components
-mcp.disable(keys={"tool:reset_system", "resource:data://secrets"})
+# Disable several components at once
+mcp.disable(names={"reset_system", "data://secrets"})
+```
+
+A name matches across component types, so a tool and a prompt that share a name are both affected. Add `components` when you want only one type.
+
+```python
+# Disable only the tool named "config", leaving the resource alone
+mcp.disable(names={"config"}, components={"tool"})
```
### Tags
@@ -112,15 +110,62 @@ mcp.disable(tags={"dangerous"})
A component is disabled if it has **any** of the disabled tags. The component doesn't need all the tags; one match is enough.
-### Combining Keys and Tags
+### Versions
-You can specify both keys and tags in a single call. The filters combine additively.
+When a component has several registered versions, `names` matches every one of them. To act on a particular version, filter by `version` with a [`VersionSpec`](/servers/versioning).
```python
-# Disable specific tools AND all dangerous-tagged components
-mcp.disable(keys={"tool:debug_info"}, tags={"dangerous"})
+from fastmcp.utilities.versions import VersionSpec
+
+# Retire v1 of every versioned component, leaving later versions live
+mcp.disable(version=VersionSpec(eq="v1"))
```
+### Component Keys
+
+`keys` targets components by their canonical key, which encodes type, identifier, and version together. It is the only filter that can single out **one specific version of one specific component** — use it when `names` would sweep too broadly and `version` would sweep across too many components.
+
+```python
+# Disable only v1 of search, leaving v1 of every other component untouched
+mcp.disable(keys={"tool:search@v1"})
+```
+
+Keys take the form `{type}:{identifier}@{version}`, where the `@` separates the identifier from the version and is **always present**. An unversioned component has an empty version, so its key ends in a bare `@`.
+
+| Component | Key Format | Example |
+|-----------|------------|---------|
+| Tool | `tool:{name}@{version}` | `tool:delete_everything@` |
+| Resource | `resource:{uri}@{version}` | `resource:data://config@` |
+| Template | `template:{uri_template}@{version}` | `template:file://{path}@` |
+| Prompt | `prompt:{name}@{version}` | `prompt:analyze@` |
+
+The delimiter is unconditional because resource URIs may themselves contain `@`. Always emitting it means a key is parsed by splitting on the last `@`, so `resource:data://user@example.com/profile@` is unambiguous.
+
+
+Keys are matched by exact string equality, so a key that omits the trailing `@` — `tool:delete_everything` rather than `tool:delete_everything@` — matches nothing. FastMCP raises a `UserWarning` when it sees a key with no `@`, since such a key can never match. Prefer `names` unless you need version-level precision, and read a key off `component.key` rather than assembling it by hand.
+
+
+### Combining Filters
+
+Criteria in a single call **narrow** each other: a component must satisfy every one of them to match. Combining a name with a tag targets the intersection, not the union.
+
+```python
+# Disables debug_info only if it is ALSO tagged "dangerous"
+mcp.disable(names={"debug_info"}, tags={"dangerous"})
+```
+
+To act on a union, make one call per criterion. Because later calls override earlier ones only where they overlap, successive disables accumulate.
+
+```python
+# Disables debug_info AND everything tagged "dangerous"
+mcp.disable(names={"debug_info"})
+mcp.disable(tags={"dangerous"})
+```
+
+
+Intersection is easy to misread as union, and a rule that matches nothing fails silently. `disable(names={"debug_info"}, tags={"dangerous"})` disables nothing at all when `debug_info` lacks that tag — the components you meant to hide stay exposed.
+
+
## Allowlist Mode
By default, visibility filtering uses blocklist mode: everything is enabled unless explicitly disabled. The `only=True` parameter switches to allowlist mode, where **only** specified components are enabled.
@@ -165,7 +210,7 @@ When you call `enable(only=True)`:
```python
# Start fresh - only enable these specific tools
-mcp.enable(keys={"tool:safe_read", "tool:safe_write"}, only=True)
+mcp.enable(names={"safe_read", "safe_write"}, only=True)
# Later, switch to a different allowlist
mcp.enable(tags={"production"}, only=True)
@@ -177,7 +222,7 @@ Later `enable()` and `disable()` calls override earlier ones. This lets you crea
```python
mcp.enable(tags={"api"}, only=True) # Allow all api-tagged
-mcp.disable(keys={"tool:api_admin"}) # Later disable overrides for this tool
+mcp.disable(names={"api_admin"}) # Later disable overrides for this tool
# api_admin is disabled because the later disable() overrides the allowlist
```
@@ -322,7 +367,7 @@ The session visibility methods accept the same filter criteria as `server.enable
| Parameter | Description |
|-----------|-------------|
| `names` | Component names or URIs to match |
-| `keys` | Component keys (e.g., `{"tool:my_tool"}`) |
+| `keys` | Component keys (e.g., `{"tool:my_tool@"}` for an unversioned tool, or `{"tool:my_tool@v1"}` for a versioned tool) |
| `tags` | Tags to match (component must have at least one) |
| `version` | Version specification to match |
| `components` | Component types (`{"tool"}`, `{"resource"}`, `{"prompt"}`, `{"template"}`) |
diff --git a/docs/snippets/prefab-demo-frame.mdx b/docs/snippets/prefab-demo-frame.mdx
new file mode 100644
index 000000000..c7d639ece
--- /dev/null
+++ b/docs/snippets/prefab-demo-frame.mdx
@@ -0,0 +1,66 @@
+export const PrefabDemoFrame = ({ demo, height, title }) => {
+ const [blobUrl, setBlobUrl] = React.useState(null);
+
+ React.useEffect(() => {
+ let active = true;
+ let objectUrl = null;
+ let payloadsPromise = window.__FASTMCP_PREFAB_DEMOS_PROMISE__;
+
+ if (window.__FASTMCP_PREFAB_DEMOS__) {
+ payloadsPromise = Promise.resolve(window.__FASTMCP_PREFAB_DEMOS__);
+ } else if (!payloadsPromise) {
+ payloadsPromise = new Promise((resolve, reject) => {
+ const script = document.createElement("script");
+ script.src = "/prefab-demo-payloads.js";
+ script.onload = () => resolve(window.__FASTMCP_PREFAB_DEMOS__);
+ script.onerror = reject;
+ document.head.appendChild(script);
+ });
+ window.__FASTMCP_PREFAB_DEMOS_PROMISE__ = payloadsPromise;
+ }
+
+ payloadsPromise
+ .then((payloads) => {
+ const html = payloads[demo];
+ if (!html) {
+ throw new Error(`Unknown Prefab demo: ${demo}`);
+ }
+ objectUrl = URL.createObjectURL(
+ new Blob([html], { type: "text/html" }),
+ );
+ if (active) {
+ setBlobUrl(objectUrl);
+ } else {
+ URL.revokeObjectURL(objectUrl);
+ }
+ });
+
+ return () => {
+ active = false;
+ if (objectUrl) {
+ URL.revokeObjectURL(objectUrl);
+ }
+ };
+ }, [demo]);
+
+ if (!blobUrl) {
+ return
;
+ }
+
+ return (
+
+ );
+};
diff --git a/docs/snippets/prefab-pin-warning.mdx b/docs/snippets/prefab-pin-warning.mdx
new file mode 100644
index 000000000..098181fba
--- /dev/null
+++ b/docs/snippets/prefab-pin-warning.mdx
@@ -0,0 +1,3 @@
+
+[Prefab](https://prefab.prefect.io) is under active development with frequent breaking changes. FastMCP sets a minimum `prefab-ui` version but does not pin an upper bound — **pin `prefab-ui` to a specific version in your own dependencies** before deploying.
+
diff --git a/docs/tutorials/mcp.mdx b/docs/tutorials/mcp.mdx
index fd3995fff..34b1c86c2 100644
--- a/docs/tutorials/mcp.mdx
+++ b/docs/tutorials/mcp.mdx
@@ -21,7 +21,7 @@ The answer lies in **standardization**. The AI ecosystem is fragmented. Every mo
1. **Interoperability:** Build one MCP server, and it can be used by any MCP-compliant client (Claude, Gemini, OpenAI, custom agents, etc.) without custom integration code. This is the protocol's most important promise.
2. **Discoverability:** Clients can dynamically ask a server what it's capable of at runtime. They receive a structured, machine-readable "menu" of tools and resources.
-3. **Security & Safety:** MCP provides a clear, sandboxed boundary. An LLM can't execute arbitrary code on your server; it can only *request* to run the specific, typed, and validated functions you explicitly expose.
+3. **Explicit boundaries:** MCP gives hosts and servers a typed inventory of the capabilities they expose. That creates a clear place to apply authorization, user confirmation, input validation, and sandboxing; the protocol defines the interface, while your application supplies those security policies.
4. **Composability:** You can build small, specialized MCP servers and combine them to create powerful, complex applications.
## Core MCP Components
@@ -111,10 +111,6 @@ def summarize_text(text_to_summarize: str) -> str:
## Advanced Capabilities
-Beyond the core components, MCP also supports more advanced interaction patterns, such as a server requesting that the *client's* LLM generate a completion (known as **sampling**), or a server sending asynchronous **notifications** to a client. These features enable more complex, bidirectional workflows and are fully supported by FastMCP.
+Beyond tools, resources, and prompts, MCP supports richer interaction patterns such as notifications, progress updates, user elicitation, and argument completion. Extensions add capabilities such as durable background tasks.
-## Next Steps
-
-Now that you understand the core concepts of the Model Context Protocol, you're ready to start building. The best place to begin is our step-by-step tutorial.
-
-[**Tutorial: How to Create an MCP Server in Python →**](/tutorials/create-mcp-server)
+FastMCP exposes these patterns through typed Python APIs. For example, [elicitation](/servers/elicitation) lets tools request missing information or confirmation, while [background tasks](/servers/tasks) let long-running work continue after the original request returns.
diff --git a/docs/tutorials/rest-api.mdx b/docs/tutorials/rest-api.mdx
index a0857aca9..6942f20e5 100644
--- a/docs/tutorials/rest-api.mdx
+++ b/docs/tutorials/rest-api.mdx
@@ -32,7 +32,7 @@ For this tutorial, we'll use the [JSONPlaceholder API](https://jsonplaceholder.t
## Step 2: Create the MCP Server
-Now for the magic. We'll use `FastMCP.from_openapi`. This method takes an `httpx.AsyncClient` configured for your API and its OpenAPI specification, and automatically converts **every endpoint** into a callable MCP `Tool`.
+Now for the magic. We'll use `FastMCP.from_openapi`. This method takes an `httpx2.AsyncClient` configured for your API and its OpenAPI specification, and automatically converts **every endpoint** into a callable MCP `Tool`.
Learn more about working with OpenAPI specs in the [OpenAPI integration docs](/integrations/openapi).
@@ -45,11 +45,11 @@ For this tutorial, we'll use a simplified OpenAPI spec directly in the code. In
Create a file named `api_server.py`:
```python api_server.py {31-35}
-import httpx
+import httpx2
from fastmcp import FastMCP
# Create an HTTP client for the target API
-client = httpx.AsyncClient(base_url="https://jsonplaceholder.typicode.com")
+client = httpx2.AsyncClient(base_url="https://jsonplaceholder.typicode.com")
# Define a simplified OpenAPI spec for JSONPlaceholder
openapi_spec = {
@@ -150,13 +150,13 @@ Learn more about route maps in the [OpenAPI integration docs](/integrations/open
Here’s how you can add custom route maps to turn `GET` requests into `Resources` and `ResourceTemplates` (if they have path parameters):
```python api_server_with_resources.py {3, 37-42}
-import httpx
+import httpx2
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
# Create an HTTP client for the target API
-client = httpx.AsyncClient(base_url="https://jsonplaceholder.typicode.com")
+client = httpx2.AsyncClient(base_url="https://jsonplaceholder.typicode.com")
# Define a simplified OpenAPI spec for JSONPlaceholder
openapi_spec = {
diff --git a/docs/unify-intent.js b/docs/unify-intent.js
index 25eafe494..b51e57b5a 100644
--- a/docs/unify-intent.js
+++ b/docs/unify-intent.js
@@ -1,10 +1,16 @@
-// Load Unify intent tag on authentication pages only
+// Load Unify intent tag on selected pages
(function () {
if (typeof window === "undefined") return;
- function isAuthPage() {
+ function isTaggedPage() {
var path = window.location.pathname;
- return path.includes("/servers/auth/") || path.includes("/clients/auth/");
+ return (
+ path.includes("/servers/auth/") ||
+ path.includes("/clients/auth/") ||
+ path.includes("/deployment/running-server") ||
+ path.includes("/deployment/http") ||
+ path.includes("/deployment/prefect-horizon")
+ );
}
function loadUnify() {
@@ -45,9 +51,9 @@
}
function update() {
- if (isAuthPage() && !document.getElementById("unifytag")) {
+ if (isTaggedPage() && !document.getElementById("unifytag")) {
loadUnify();
- } else if (!isAuthPage() && document.getElementById("unifytag")) {
+ } else if (!isTaggedPage() && document.getElementById("unifytag")) {
document.getElementById("unifytag").remove();
}
}
diff --git a/docs/updates.mdx b/docs/updates.mdx
index e3134fb61..26e83c917 100644
--- a/docs/updates.mdx
+++ b/docs/updates.mdx
@@ -5,6 +5,232 @@ icon: "sparkles"
tag: NEW
---
+
+
+FastMCP 3.4.6 adds trusted-proxy support for SSRF-protected OAuth metadata and JWKS fetches on the 3.x line. Deployments can route these requests through a mandated corporate proxy while preserving custom CA certificates, and FastMCP refuses the fetch when no proxy is configured instead of risking an unprotected direct request.
+
+
+
+
+
+FastMCP 4 makes stateful MCP applications work on the sessionless protocol while one deployment continues serving handshake-era clients. The engine underneath changed completely, but FastMCP absorbs nearly all of it — most FastMCP 3 servers upgrade untouched.
+
+🌐 **Every protocol era** — one server answers both the sessionless `2026-07-28` protocol and the older session-based handshake, negotiated per connection.
+
+💬 **Interactive tools** — tools ask follow-up questions across complete request-response rounds, with shared request-state keys for load balancing and worker restarts.
+
+💾 **State without a session** — `UserSession` and `SessionId` give tools explicit server-side state on a protocol that deliberately has none, keyed per user when the request is authenticated.
+
+⏳ **Background tasks** — the `io.modelcontextprotocol/tasks` extension in the new `fastmcp-tasks` package, on the same Docket engine FastMCP 3 used.
+
+🧩 **Server extensions** — `add_extension()` turns capability-negotiated protocol features into a supported plugin surface.
+
+🔐 **Enterprise auth** — server-side identity assertion (SEP-990), `require_roles`, scope step-up challenges, and DCR `application_type`.
+
+⚠️ **Breaking** — server-initiated sampling and roots are removed from the server API, and the 3.x-era compatibility shims are gone. See the [upgrade guide](/getting-started/upgrading/from-fastmcp-3).
+
+
+
+
+
+A maintenance release for the 3.x line. A single unrecognized JWKS key — Ed25519, which Rauthy and Ory Hydra publish by default — no longer poisons the entire key cache, alongside fixes for Azure scope fallback, OpenAPI `deepObject` query serialization, schema compression, and transformed tool `required` ordering.
+
+
+
+
+
+A compatibility patch for HTTP deployments affected by the 3.4.3 Host/Origin guard defaults. FastMCP 3.x now keeps strict Host and Origin validation available for explicit opt-in deployments without rejecting existing ASGI, serverless, and reverse-proxy traffic by default.
+
+🌐 **HTTP compatibility restored** — existing hosted deployments keep accepting their public Host headers unless strict host/origin protection is configured.
+
+🔐 **Guard remains available** — deployments that know their public host and browser origins can still enable strict validation with `host_origin_protection=True`, `allowed_hosts`, and `allowed_origins`.
+
+🤗 **Hugging Face auth** — new OAuth provider support covers public and private Hugging Face apps, with docs and examples for PKCE, Dynamic Client Registration, and CIMD.
+
+
+
+
+
+A month of SSRF and OAuth hardening lands in one patch. NAT64, 6to4, Teredo, and ISATAP transition addresses can no longer smuggle private IPv4 targets past the SSRF allow-list, Streamable HTTP validates Host and Origin before session handling to block DNS rebinding, and OAuth redirect validation rejects unsafe schemes and unregistered DCR redirect URIs.
+
+🛡️ **SSRF allow-list hardening** — every IPv6 transition form (NAT64, 6to4, Teredo, ISATAP) now unwraps to its embedded IPv4 target and gets checked against the same policy.
+
+🌐 **DNS rebinding protection** — Streamable HTTP validates `Host` and browser `Origin` before session handling, closing a path to localhost-bound unauthenticated servers.
+
+🔐 **Stricter OAuth redirects** — unsafe redirect schemes are rejected before registration, and DCR clients are bound to the redirect URIs they registered.
+
+🧵 **Reliability fixes** — proxy session teardown races, discriminator-tag handling in JSON schema conversion, and several smaller fixes across middleware and resource templates.
+
+
+
+
+
+A compatibility patch. `JWTVerifier` now accepts JWTs carrying private, non-critical JWS header parameters (like Clerk's `cat`) instead of rejecting them before signature and claim validation, while unsupported critical headers are still rejected.
+
+
+
+
+
+A security patch. FastMCP now floors Starlette at `>=1.0.1`, so installs can no longer resolve to a version affected by CVE-2026-48710 — previously the dependency was only constrained transitively through `mcp`. OAuthProxy also logs refresh-token cache misses instead of failing silently.
+
+
+
+
+
+The remote release. `fastmcp-remote` is a standalone bridge that connects stdio-only MCP hosts to servers hosted over HTTP, with OAuth enabled automatically for HTTPS. The proxy layer underneath it is hardened so bridges fail loudly on a missing or misconfigured upstream, and FastMCP-issued tokens can now outlive short-lived upstream tokens to survive long idle periods.
+
+🌉 **fastmcp-remote** — `uvx fastmcp-remote https://example.com/mcp` bridges a remote server back to a stdio-only host.
+
+🔌 **Bridges fail loudly** — proxies forward `initialize` upstream, so a missing backend or wrong URL fails the handshake instead of returning an empty-but-connected proxy.
+
+🔐 **Longer-lived tokens** — `fastmcp_access_token_expiry_seconds` decouples the client-facing token lifetime from a short upstream `expires_in`.
+
+⚠️ **Returnable tool errors** — `ToolResult(..., is_error=True)` hands back rich errors the model can act on instead of only raising.
+
+
+
+
+
+Hotfix for the 3.3 packaging split: standalone component imports like `from fastmcp.tools import tool` no longer pull in the server stack or trip a circular import. Component-level auth and task primitives moved to lightweight utility modules, with the old import paths preserved as compatibility re-exports.
+
+
+
+
+
+The `fastmcp-slim` release. A dependency-light distribution that ships FastMCP's client and transport layer without Starlette, Uvicorn, or the server stack — the import namespace is unchanged. It also closes out a backlog of OAuth proxy hardening, MCP-compliant OTEL instrumentation, and auth additions.
+
+🪶 **fastmcp-slim** — install the client without the server footprint for CI, agents, and library dependencies.
+
+🔐 **OAuth proxy hardening** — silent-consent AS-in-the-middle guard, dot-segment redirect rejection, and per-token response cache partitioning.
+
+🔑 **Auth additions** — `AzureB2CProvider` user flows and a public `update_scopes()` API on `OAuthProxy`.
+
+🧵 **Thread affinity** — `@mcp.tool(run_in_thread=False)` for tools bound to a specific thread.
+
+
+
+
+
+A grab bag of fixes and hardening. Background tasks are now scoped to the authorization context instead of the MCP session — a breaking change for anyone relying on session-scoped semantics — and parameter descriptions are extracted from docstrings automatically.
+
+🔐 **Security** — `FileUpload` validates decoded base64 size, the proxy stops forwarding inbound headers to unrelated servers, and AuthKit binds token audience per RFC 8707.
+
+🔑 **Keycloak** — new OAuth provider for enterprise auth and local dev.
+
+
+
+
+
+Pins `fakeredis<2.35.0` in the `tasks` extra: a 2.35.0 rename broke pydocket's `memory://` backend and made `fastmcp[tasks]` installs fail at startup with an `ImportError`.
+
+
+
+
+
+Fixes the Azure audience regression from 3.2.1 — both the bare client ID GUID and a custom `identifier_uri` are now accepted as the token audience.
+
+
+
+
+
+A patch focused on auth-provider audience validation: Cognito validates on `client_id`, Azure honors `identifier_uri`, and consent cookies are LRU-capped to avoid header overflow. Also fixes OpenAPI 3.0 `nullable` fields leaking into tool input schemas and server-variable substitution in base URLs.
+
+
+
+
+
+The Apps release. Your tools can return interactive UIs — charts, dashboards, forms, maps — rendered right inside the conversation.
+
+🎨 **FastMCPApp** — separate the tools the LLM sees (`@app.ui()`) from the backend tools the UI calls (`@app.tool()`), built on Prefab.
+
+🧩 **Built-in providers** — FileUpload, Approval, Choice, FormInput, and GenerativeUI.
+
+🖥️ **Dev server** — `fastmcp dev apps` previews app tools in the browser with an MCP message inspector.
+
+🔒 **Security pass** — SSRF/path-traversal prevention, JWT algorithm restrictions, OAuth scope enforcement, and CSRF fixes.
+
+
+
+
+
+Pins `pydantic-monty<0.0.8` to fix a breaking change in Monty that affects code mode.
+
+
+
+
+
+The Code Mode release. Instead of loading the entire tool catalog into context, `CodeMode` gives LLMs meta-tools: search for relevant tools on demand, inspect their schemas, then write Python that chains `call_tool()` calls in a sandbox. Also ships search transforms, early Prefab Apps integration, `MultiAuth` for composing multiple token verification sources, and PropelAuth support.
+
+
+
+
+
+A 2.x backport of the fakeredis pin: fakeredis 2.35.0 renamed a connection class that pydocket's `memory://` backend relied on, crashing `fastmcp[tasks]` installs at startup. Caps `fakeredis<2.35.0` on the 2.x line.
+
+
+
+
+
+v2.14.4 backported `dereference_refs()` but never wired it into the tool schema pipeline — `$ref` and `$defs` were still sent to MCP clients. Now fixed: schemas are fully inlined before reaching clients.
+
+
+
-
diff --git a/docs/v2-navigation.json b/docs/v2-navigation.json
new file mode 100644
index 000000000..17865edfd
--- /dev/null
+++ b/docs/v2-navigation.json
@@ -0,0 +1,197 @@
+{
+ "dropdowns": [
+ {
+ "dropdown": "Documentation",
+ "groups": [
+ {
+ "group": "Get Started",
+ "pages": [
+ "v2/getting-started/welcome",
+ "v2/getting-started/installation",
+ "v2/getting-started/quickstart",
+ "v2/updates"
+ ]
+ },
+ {
+ "group": "Servers",
+ "pages": [
+ "v2/servers/server",
+ {
+ "group": "Core Components",
+ "icon": "toolbox",
+ "pages": [
+ "v2/servers/tools",
+ "v2/servers/resources",
+ "v2/servers/prompts"
+ ]
+ },
+ {
+ "group": "Advanced Features",
+ "icon": "stars",
+ "pages": [
+ "v2/servers/composition",
+ "v2/servers/context",
+ "v2/servers/elicitation",
+ "v2/servers/icons",
+ "v2/servers/logging",
+ "v2/servers/middleware",
+ "v2/servers/progress",
+ "v2/servers/proxy",
+ "v2/servers/sampling",
+ "v2/servers/storage-backends",
+ "v2/servers/tasks"
+ ]
+ },
+ {
+ "group": "Authentication",
+ "icon": "shield-check",
+ "pages": [
+ "v2/servers/auth/authentication",
+ "v2/servers/auth/token-verification",
+ "v2/servers/auth/remote-oauth",
+ "v2/servers/auth/oauth-proxy",
+ "v2/servers/auth/oidc-proxy",
+ "v2/servers/auth/full-oauth-server"
+ ]
+ },
+ {
+ "group": "Deployment",
+ "icon": "rocket",
+ "pages": [
+ "v2/deployment/running-server",
+ "v2/deployment/http",
+ "deployment/prefect-horizon",
+ "v2/deployment/server-configuration"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Clients",
+ "pages": [
+ {
+ "group": "Essentials",
+ "icon": "cube",
+ "pages": [
+ "v2/clients/client",
+ "v2/clients/transports"
+ ]
+ },
+ {
+ "group": "Core Operations",
+ "icon": "handshake",
+ "pages": [
+ "v2/clients/tools",
+ "v2/clients/resources",
+ "v2/clients/prompts"
+ ]
+ },
+ {
+ "group": "Advanced Features",
+ "icon": "stars",
+ "pages": [
+ "v2/clients/elicitation",
+ "v2/clients/logging",
+ "v2/clients/progress",
+ "v2/clients/sampling",
+ "v2/clients/tasks",
+ "v2/clients/messages",
+ "v2/clients/roots"
+ ]
+ },
+ {
+ "group": "Authentication",
+ "icon": "user-shield",
+ "pages": [
+ "v2/clients/auth/oauth",
+ "v2/clients/auth/bearer"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Integrations",
+ "pages": [
+ {
+ "group": "Authentication",
+ "icon": "key",
+ "pages": [
+ "v2/integrations/auth0",
+ "v2/integrations/authkit",
+ "v2/integrations/aws-cognito",
+ "v2/integrations/azure",
+ "v2/integrations/descope",
+ "v2/integrations/discord",
+ "v2/integrations/github",
+ "v2/integrations/google",
+ "v2/integrations/oci",
+ "v2/integrations/scalekit",
+ "v2/integrations/supabase",
+ "v2/integrations/workos"
+ ]
+ },
+ {
+ "group": "Authorization",
+ "icon": "shield-check",
+ "pages": [
+ "v2/integrations/eunomia-authorization",
+ "v2/integrations/permit"
+ ]
+ },
+ {
+ "group": "AI Assistants",
+ "icon": "robot",
+ "pages": [
+ "v2/integrations/chatgpt",
+ "v2/integrations/claude-code",
+ "v2/integrations/claude-desktop",
+ "v2/integrations/cursor",
+ "v2/integrations/gemini-cli",
+ "v2/integrations/mcp-json-configuration"
+ ]
+ },
+ {
+ "group": "AI SDKs",
+ "icon": "code",
+ "pages": [
+ "v2/integrations/anthropic",
+ "v2/integrations/gemini",
+ "v2/integrations/openai"
+ ]
+ },
+ {
+ "group": "API Integration",
+ "icon": "globe",
+ "pages": [
+ "v2/integrations/fastapi",
+ "v2/integrations/openapi"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Patterns",
+ "pages": [
+ "v2/patterns/tool-transformation",
+ "v2/patterns/decorating-methods",
+ "v2/patterns/cli",
+ "v2/patterns/contrib",
+ "v2/patterns/testing"
+ ]
+ },
+ {
+ "group": "Development",
+ "pages": [
+ "v2/development/contributing",
+ "v2/development/tests",
+ "v2/development/releases",
+ "v2/development/upgrade-guide",
+ "v2/changelog"
+ ]
+ }
+ ],
+ "icon": "book"
+ }
+ ],
+ "version": "v2.14.5"
+}
diff --git a/docs/v2/changelog.mdx b/docs/v2/changelog.mdx
index f46b08b04..e167f016b 100644
--- a/docs/v2/changelog.mdx
+++ b/docs/v2/changelog.mdx
@@ -4,6 +4,36 @@ icon: "list-check"
rss: true
---
+
+
+**[v2.14.7: Fake It Till You Break It](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.7)**
+
+A 2.x backport of the fakeredis pin: fakeredis 2.35.0 renamed a connection class that pydocket's `memory://` backend depended on, crashing `fastmcp[tasks]` installs at startup. This caps `fakeredis<2.35.0` on the 2.x line.
+
+## What's Changed
+### Fixes 🐞
+* fix(deps): cap fakeredis to `<2.35.0` to prevent startup crash on 2.x by [@vincent067](https://github.com/vincent067) in [#3883](https://github.com/PrefectHQ/fastmcp/pull/3883)
+
+**Full Changelog**: [v2.14.6...v2.14.7](https://github.com/PrefectHQ/fastmcp/compare/v2.14.6...v2.14.7)
+
+
+
+
+
+**[v2.14.6: $Ref Dead Redemption](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.6)**
+
+v2.14.4 backported `dereference_refs()` but never wired it into the tool schema pipeline — `$ref` and `$defs` were still sent to MCP clients. Now fixed: `compress_schema()` dereferences at both tool schema creation sites, so schemas are fully inlined before reaching clients.
+
+## What's Changed
+### Fixes 🐞
+* Updated deprecation URL for V2 by [@SrzStephen](https://github.com/SrzStephen) in [#3109](https://github.com/PrefectHQ/fastmcp/pull/3109)
+* Use MemoryStore for OAuth proxy tests by [@SrzStephen](https://github.com/SrzStephen) in [#3111](https://github.com/PrefectHQ/fastmcp/pull/3111)
+* fix: wire up dereference_refs() in tool schema pipeline by [@jlowin](https://github.com/jlowin) in [#3170](https://github.com/PrefectHQ/fastmcp/pull/3170)
+
+**Full Changelog**: [v2.14.5...v2.14.6](https://github.com/PrefectHQ/fastmcp/compare/v2.14.5...v2.14.6)
+
+
+
**[v2.14.5: Sealed Docket](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.5)**
@@ -520,7 +550,7 @@ Thank you to our new contributors and everyone who tested preview builds. Your f
* Add configurable redirect URI validation for OAuth providers by [@jlowin](https://github.com/jlowin) in [#1582](https://github.com/PrefectHQ/fastmcp/pull/1582)
* Remove invalid-argument-type ignore and fix type errors by [@jlowin](https://github.com/jlowin) in [#1588](https://github.com/PrefectHQ/fastmcp/pull/1588)
* Remove generate-schema from public CLI by [@jlowin](https://github.com/jlowin) in [#1591](https://github.com/PrefectHQ/fastmcp/pull/1591)
-* Skip flaky windows test / mulit-client garbage collection by [@jlowin](https://github.com/jlowin) in [#1592](https://github.com/PrefectHQ/fastmcp/pull/1592)
+* Skip flaky windows test / multi-client garbage collection by [@jlowin](https://github.com/jlowin) in [#1592](https://github.com/PrefectHQ/fastmcp/pull/1592)
* Add setting to disable logging configuration by [@isra17](https://github.com/isra17) in [#1575](https://github.com/PrefectHQ/fastmcp/pull/1575)
* Improve debug logging for nested Servers / Clients by [@strawgate](https://github.com/strawgate) in [#1604](https://github.com/PrefectHQ/fastmcp/pull/1604)
* Add GitHub pull request template by [@strawgate](https://github.com/strawgate) in [#1581](https://github.com/PrefectHQ/fastmcp/pull/1581)
@@ -2309,4 +2339,4 @@ This release is highlighted by the ability to handle complex JSON objects as MCP
The very first release of FastMCP! 🎉
**Full Changelog**: [Initial commits](https://github.com/PrefectHQ/fastmcp/commits/v0.1.0)
-
\ No newline at end of file
+
diff --git a/docs/v2/clients/sampling.mdx b/docs/v2/clients/sampling.mdx
index 1bfea5b18..8f72de597 100644
--- a/docs/v2/clients/sampling.mdx
+++ b/docs/v2/clients/sampling.mdx
@@ -212,7 +212,7 @@ client = Client(
```
-Install the OpenAI handler with `pip install fastmcp[openai]`.
+Install the OpenAI handler with `pip install 'fastmcp[openai]'`.
### Anthropic Handler
@@ -246,7 +246,7 @@ client = Client(
```
-Install the Anthropic handler with `pip install fastmcp[anthropic]`.
+Install the Anthropic handler with `pip install 'fastmcp[anthropic]'`.
### Tool Execution
@@ -254,5 +254,5 @@ Install the Anthropic handler with `pip install fastmcp[anthropic]`.
Tool execution happens on the server side. The client's role is to pass tools to the LLM and return the LLM's response (which may include tool use requests). The server then executes the tools and may send follow-up sampling requests with tool results.
-To implement a custom sampling handler, see the [handler source code](https://github.com/PrefectHQ/fastmcp/tree/main/src/fastmcp/client/sampling/handlers) as a reference.
+To implement a custom sampling handler, see the [handler source code](https://github.com/PrefectHQ/fastmcp/tree/main/fastmcp_slim/fastmcp/client/sampling/handlers) as a reference.
\ No newline at end of file
diff --git a/docs/v2/deployment/http.mdx b/docs/v2/deployment/http.mdx
index eafa773b3..35960891d 100644
--- a/docs/v2/deployment/http.mdx
+++ b/docs/v2/deployment/http.mdx
@@ -574,7 +574,7 @@ if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
```
-For more details on OAuth authentication, see the [Authentication guide](/v2/servers/auth).
+For more details on OAuth authentication, see the [Authentication guide](/v2/servers/auth/authentication).
## Production Deployment
@@ -650,17 +650,17 @@ FASTMCP_STATELESS_HTTP=true uvicorn app:app --host 0.0.0.0 --port 8000 --workers
Production deployments should never hardcode sensitive information like API keys or authentication tokens. Instead, use environment variables to configure your server at runtime. This keeps your code secure and makes it easy to deploy the same code to different environments with different configurations.
-Here's an example using bearer token authentication (though OAuth is recommended for production):
+Here's an example using static token authentication for development (OAuth is recommended for production):
```python
import os
from fastmcp import FastMCP
-from fastmcp.server.auth import BearerTokenAuth
+from fastmcp.server.auth import StaticTokenVerifier
# Read configuration from environment
auth_token = os.environ.get("MCP_AUTH_TOKEN")
if auth_token:
- auth = BearerTokenAuth(token=auth_token)
+ auth = StaticTokenVerifier(tokens={auth_token: {"sub": "admin", "client_id": "cli"}})
mcp = FastMCP("Production Server", auth=auth)
else:
mcp = FastMCP("Production Server")
diff --git a/docs/v2/development/tests.mdx b/docs/v2/development/tests.mdx
index 6a9973fe8..4653368be 100644
--- a/docs/v2/development/tests.mdx
+++ b/docs/v2/development/tests.mdx
@@ -33,7 +33,7 @@ Tests should complete in under 1 second unless marked as integration tests. This
### Test Organization
-Our test organization mirrors the `src/` directory structure, creating a predictable mapping between code and tests. When you're working on `src/fastmcp/server/auth.py`, you'll find its tests in `tests/server/test_auth.py`. In rare cases tests are split further - for example, the OpenAPI tests are so comprehensive they're split across multiple files.
+Our test organization mirrors the source package structure, creating a predictable mapping between code and tests. When you're working on `fastmcp_slim/fastmcp/server/auth.py`, you'll find its tests in `tests/server/test_auth.py`. In rare cases tests are split further - for example, the OpenAPI tests are so comprehensive they're split across multiple files.
### Test Markers
@@ -393,4 +393,4 @@ just docs
mintlify dev
```
-The local server watches for changes and automatically refreshes. This preview catches formatting issues and helps you see documentation as users will experience it.
\ No newline at end of file
+The local server watches for changes and automatically refreshes. This preview catches formatting issues and helps you see documentation as users will experience it.
diff --git a/docs/v2/development/upgrade-guide.mdx b/docs/v2/development/upgrade-guide.mdx
index 2e98b4a02..93b314829 100644
--- a/docs/v2/development/upgrade-guide.mdx
+++ b/docs/v2/development/upgrade-guide.mdx
@@ -19,11 +19,11 @@ The experimental OpenAPI parser is now the standard implementation. The legacy p
**If you were using the experimental parser:** Update your imports from the experimental module to the standard location:
-```python Before
+```python test="skip" Before
from fastmcp.experimental.server.openapi import FastMCPOpenAPI, RouteMap, MCPType
```
-```python After
+```python test="skip" After
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap, MCPType
```
@@ -36,7 +36,7 @@ The following deprecated features have been removed in v2.14.0:
**BearerAuthProvider** (deprecated in v2.11):
-```python Before
+```python test="skip" Before
from fastmcp.server.auth.providers.bearer import BearerAuthProvider
```
@@ -47,7 +47,7 @@ from fastmcp.server.auth.providers.jwt import JWTVerifier
**Context.get_http_request()** (deprecated in v2.2.11):
-```python Before
+```python test="skip" Before
request = context.get_http_request()
```
@@ -59,7 +59,7 @@ request = get_http_request()
**Top-level Image import** (deprecated in v2.8.1):
-```python Before
+```python test="skip" Before
from fastmcp import Image
```
diff --git a/docs/v2/getting-started/quickstart.mdx b/docs/v2/getting-started/quickstart.mdx
index 678d8cd5e..117efcd0a 100644
--- a/docs/v2/getting-started/quickstart.mdx
+++ b/docs/v2/getting-started/quickstart.mdx
@@ -119,7 +119,7 @@ Note that:
## Deploy to Prefect Horizon
-[Prefect Horizon](https://horizon.prefect.io) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides managed hosting, authentication, access control, and observability for MCP servers.
+[Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides managed hosting, authentication, access control, and observability for MCP servers.
Horizon is **free for personal projects** and offers enterprise governance for teams.
@@ -128,7 +128,7 @@ Horizon is **free for personal projects** and offers enterprise governance for t
To deploy your server, you'll need a [GitHub account](https://github.com). Once you have one, you can deploy your server in three steps:
1. Push your `my_server.py` file to a GitHub repository
-2. Sign in to [Prefect Horizon](https://horizon.prefect.io) with your GitHub account
+2. Sign in to [Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) with your GitHub account
3. Create a new project from your repository and enter `my_server.py:mcp` as the server entrypoint
That's it! Horizon will build and deploy your server, making it available at a URL like `https://your-project.fastmcp.app/mcp`. You can chat with it to test its functionality, or connect to it from any LLM client that supports the MCP protocol.
diff --git a/docs/v2/getting-started/welcome.mdx b/docs/v2/getting-started/welcome.mdx
index c00788f72..b8213d484 100644
--- a/docs/v2/getting-started/welcome.mdx
+++ b/docs/v2/getting-started/welcome.mdx
@@ -73,7 +73,7 @@ FastMCP handles all the complex protocol details so you can focus on building. I
🔍 **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 [Prefect Horizon](https://horizon.prefect.io) (free for personal projects), or to your own infrastructure.
+FastMCP provides the shortest path from idea to production. Deploy locally, to the cloud with [Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) (free for personal projects), or to your own infrastructure.
**This documentation reflects FastMCP's `main` branch**, meaning it always reflects the latest development version. Features are generally marked with version badges (e.g. `New in version: 2.13.1`) to indicate when they were introduced. Note that this may include features that are not yet released.
@@ -96,7 +96,7 @@ from fastmcp import Client
async def main():
async with Client("https://gofastmcp.com/mcp") as client:
result = await client.call_tool(
- name="SearchFastMcp",
+ name="search_fast_mcp",
arguments={"query": "deploy a FastMCP server"}
)
print(result)
diff --git a/docs/v2/integrations/anthropic.mdx b/docs/v2/integrations/anthropic.mdx
index 7490bcf35..7d2d38dc1 100644
--- a/docs/v2/integrations/anthropic.mdx
+++ b/docs/v2/integrations/anthropic.mdx
@@ -181,7 +181,7 @@ if __name__ == "__main__":
If you try to call the authenticated server with the same Anthropic code we wrote earlier, you'll get an error indicating that the server rejected the request because it's not authenticated.
-```python
+```text
Error code: 400 - {
"type": "error",
"error": {
diff --git a/docs/v2/integrations/chatgpt.mdx b/docs/v2/integrations/chatgpt.mdx
index 18028c69d..d40855551 100644
--- a/docs/v2/integrations/chatgpt.mdx
+++ b/docs/v2/integrations/chatgpt.mdx
@@ -93,10 +93,12 @@ The connector must be explicitly enabled in each chat session through Developer
### Skip Confirmations
-Use `annotations={"readOnlyHint": True}` to skip confirmation prompts for read-only tools:
+Use `annotations=ToolAnnotations(readOnlyHint=True)` to skip confirmation prompts for read-only tools:
```python
-@mcp.tool(annotations={"readOnlyHint": True})
+from mcp.types import ToolAnnotations
+
+@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
def get_status() -> str:
"""Check system status."""
return "All systems operational"
@@ -154,4 +156,3 @@ def fetch(id: str) -> dict:
5. Ask research questions
ChatGPT will use your `search` and `fetch` tools to find and cite relevant information.
-
diff --git a/docs/v2/integrations/descope.mdx b/docs/v2/integrations/descope.mdx
index abba9069d..14bade5f4 100644
--- a/docs/v2/integrations/descope.mdx
+++ b/docs/v2/integrations/descope.mdx
@@ -64,8 +64,8 @@ from fastmcp.server.auth.providers.descope import DescopeProvider
# The DescopeProvider automatically discovers Descope endpoints
# and configures JWT token validation
auth_provider = DescopeProvider(
- config_url=https://.../.well-known/openid-configuration, # Your MCP Server .well-known URL
- base_url=SERVER_URL, # Your server's public URL
+ config_url="https://.../.well-known/openid-configuration", # Your MCP Server .well-known URL
+ base_url=SERVER_URL, # Your server's public URL
)
# Create FastMCP server with auth
diff --git a/docs/v2/integrations/fastapi.mdx b/docs/v2/integrations/fastapi.mdx
index 3737345f1..67d5d06de 100644
--- a/docs/v2/integrations/fastapi.mdx
+++ b/docs/v2/integrations/fastapi.mdx
@@ -216,7 +216,7 @@ Because FastMCP's FastAPI integration is based on its [OpenAPI integration](/v2/
```python
# Assumes the FastAPI app from above is already defined
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
# Custom mapping rules
mcp = FastMCP.from_fastapi(
diff --git a/docs/v2/integrations/gemini.mdx b/docs/v2/integrations/gemini.mdx
index b037c1110..901e7f850 100644
--- a/docs/v2/integrations/gemini.mdx
+++ b/docs/v2/integrations/gemini.mdx
@@ -89,7 +89,7 @@ Okay, I rolled 3 dice and got a 5, 4, and 1.
### Remote & Authenticated Servers
-In the above example, we connected to our local server using `stdio` transport. Because we're using a FastMCP client, you can also connect to any local or remote MCP server, using any [transport](/v2/clients/transports) or [auth](/v2/clients/auth) method supported by FastMCP, simply by changing the client configuration.
+In the above example, we connected to our local server using `stdio` transport. Because we're using a FastMCP client, you can also connect to any local or remote MCP server, using any [transport](/v2/clients/transports) or [auth](/v2/clients/auth/oauth) method supported by FastMCP, simply by changing the client configuration.
For example, to connect to a remote, authenticated server, you can use the following client:
diff --git a/docs/v2/integrations/images/permit/role_assignement.png b/docs/v2/integrations/images/permit/role_assignment.png
similarity index 100%
rename from docs/v2/integrations/images/permit/role_assignement.png
rename to docs/v2/integrations/images/permit/role_assignment.png
diff --git a/docs/v2/integrations/mcp-json-configuration.mdx b/docs/v2/integrations/mcp-json-configuration.mdx
index a9b758fb7..b85bf9e4f 100644
--- a/docs/v2/integrations/mcp-json-configuration.mdx
+++ b/docs/v2/integrations/mcp-json-configuration.mdx
@@ -357,6 +357,98 @@ echo "$CONFIG" | jq '."CI Server".command'
# Output: "uv"
```
+### UV-Managed Project Dependencies
+
+For servers that live inside a uv-managed project (with `pyproject.toml`), use the `--project` flag to run within that project's environment:
+
+```bash
+fastmcp install mcp-json server.py --project .
+```
+
+Output:
+```json
+{
+ "My Server": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--project",
+ "/absolute/path/to/project",
+ "--with",
+ "fastmcp",
+ "fastmcp",
+ "run",
+ "/absolute/path/to/project/server.py"
+ ]
+ }
+}
+```
+
+You can also use `fastmcp.json` with a local project:
+
+```json fastmcp.json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py"
+ },
+ "environment": {
+ "project": "."
+ }
+}
+```
+
+If your server needs additional packages beyond those in `pyproject.toml`, add them via the `dependencies` array or `--with`.
+
+### Published Packages with `uvx`
+
+If your team publishes MCP servers as pip packages, you can configure clients to run them with `uvx` directly instead of `uv run`. For example, if your package is called `my-mcp-server` and provides a CLI entry point of the same name:
+
+```json
+{
+ "mcpServers": {
+ "My Server": {
+ "command": "uvx",
+ "args": ["my-mcp-server"]
+ }
+ }
+}
+```
+
+If the package name differs from the CLI command (e.g., package `weather-mcp` with command `weather-server`):
+
+```json
+{
+ "mcpServers": {
+ "Weather": {
+ "command": "uvx",
+ "args": ["--from", "weather-mcp", "weather-server"]
+ }
+ }
+}
+```
+
+You can also pin Python versions or add extra dependencies:
+
+```json
+{
+ "mcpServers": {
+ "My Server": {
+ "command": "uvx",
+ "args": [
+ "--python", "3.12",
+ "--with", "requests",
+ "my-mcp-server"
+ ]
+ }
+ }
+}
+```
+
+
+`fastmcp install mcp-json` generates `uv run` configurations for local development. For published packages, you'll typically write the `uvx` configuration manually or generate it through your own packaging workflow.
+
+
## Integration with MCP Clients
The generated configuration works with any MCP-compatible application:
diff --git a/docs/v2/integrations/openai.mdx b/docs/v2/integrations/openai.mdx
index 63b5e28b4..af41528f9 100644
--- a/docs/v2/integrations/openai.mdx
+++ b/docs/v2/integrations/openai.mdx
@@ -178,8 +178,8 @@ if __name__ == "__main__":
If you try to call the authenticated server with the same OpenAI code we wrote earlier, you'll get an error like this:
-```python
-pythonAPIStatusError: Error code: 424 - {
+```text
+APIStatusError: Error code: 424 - {
"error": {
"message": "Error retrieving tool list from MCP server: 'dice_server'. Http status code: 401 (Unauthorized)",
"type": "external_connector_error",
diff --git a/docs/v2/integrations/openapi.mdx b/docs/v2/integrations/openapi.mdx
index 8662a0c05..0fd38cbd0 100644
--- a/docs/v2/integrations/openapi.mdx
+++ b/docs/v2/integrations/openapi.mdx
@@ -81,7 +81,7 @@ Each `RouteMap` specifies a combination of methods, patterns, and tags, as well
Here is FastMCP's default rule:
```python
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
DEFAULT_ROUTE_MAPPINGS = [
# All routes become tools
@@ -97,7 +97,7 @@ For example, prior to FastMCP 2.8.0, GET requests were automatically mapped to `
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
# Restore pre-2.8.0 semantic mapping
semantic_maps = [
@@ -120,7 +120,7 @@ Here is a more complete example that uses custom route maps to convert all `GET`
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -160,7 +160,7 @@ You can use this to remove sensitive or internal routes by targeting them specif
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -176,7 +176,7 @@ Or you can use a catch-all rule to exclude everything that your maps don't handl
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -208,7 +208,8 @@ The `route_map_fn` is called on all routes, even those that matched `MCPType.EXC
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType, HTTPRoute
+from fastmcp.server.providers.openapi import RouteMap, MCPType
+from fastmcp.utilities.openapi import HTTPRoute
def custom_route_mapper(route: HTTPRoute, mcp_type: MCPType) -> MCPType | None:
"""Advanced route type mapping."""
@@ -273,7 +274,7 @@ FastMCP provides several ways to add tags to your MCP components, allowing you t
You can add custom tags to components created from specific routes using the `mcp_tags` parameter in `RouteMap`. These tags will be applied to all components created from routes that match that particular route map.
```python
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -364,12 +365,12 @@ Your `mcp_component_fn` is expected to modify the component in-place, not to ret
```python
-from fastmcp.server.openapi import (
- HTTPRoute,
+from fastmcp.server.providers.openapi import (
OpenAPITool,
OpenAPIResource,
OpenAPIResourceTemplate,
)
+from fastmcp.utilities.openapi import HTTPRoute
def customize_components(
route: HTTPRoute,
diff --git a/docs/v2/integrations/permit.mdx b/docs/v2/integrations/permit.mdx
index 066f5b1ea..ddda7cd2c 100644
--- a/docs/v2/integrations/permit.mdx
+++ b/docs/v2/integrations/permit.mdx
@@ -31,7 +31,7 @@ The middleware automatically maps MCP methods to Permit.io resources and actions
> **Note:**
> Don't forget to assign the relevant role (e.g., Admin, User) to the user authenticating to your MCP server (such as the user in the JWT) in the Permit.io Directory. Without the correct role assignment, users will not have access to the resources and actions you've configured in your policies.
>
-> 
+> 
>
> *Example: In Permit.io Directory, both 'client' and 'admin' users are assigned the 'Admin' role, granting them the permissions defined in your policy mapping.*
diff --git a/docs/v2/integrations/scalekit.mdx b/docs/v2/integrations/scalekit.mdx
index abe41a2ba..5a877330f 100644
--- a/docs/v2/integrations/scalekit.mdx
+++ b/docs/v2/integrations/scalekit.mdx
@@ -173,22 +173,15 @@ logging.basicConfig(level=logging.DEBUG)
You can inspect JWT tokens in your tools to understand the user context:
```python
-from fastmcp.server.context import request_ctx
-import jwt
+from fastmcp.server.dependencies import get_access_token
@mcp.tool
def inspect_token() -> dict:
"""Inspect the current JWT token claims."""
- context = request_ctx.get()
+ token = get_access_token()
+ if token is None:
+ return {"error": "No token found"}
- # Extract token from Authorization header
- if hasattr(context, 'request') and hasattr(context.request, 'headers'):
- auth_header = context.request.headers.get('authorization', '')
- if auth_header.startswith('Bearer '):
- token = auth_header[7:]
- # Decode without verification (already verified by provider)
- claims = jwt.decode(token, options={"verify_signature": False})
- return claims
-
- return {"error": "No token found"}
+ # Claims were already verified by the auth provider.
+ return token.claims
```
diff --git a/docs/v2/patterns/contrib.mdx b/docs/v2/patterns/contrib.mdx
index d2f812f52..04ef45aff 100644
--- a/docs/v2/patterns/contrib.mdx
+++ b/docs/v2/patterns/contrib.mdx
@@ -12,13 +12,13 @@ FastMCP includes a `contrib` package that holds community-contributed modules. T
Contrib modules provide additional features, integrations, or patterns that complement the core FastMCP library. They offer a way for the community to share useful extensions while keeping the core library focused and maintainable.
-The available modules can be viewed in the [contrib directory](https://github.com/PrefectHQ/fastmcp/tree/main/src/fastmcp/contrib).
+The available modules can be viewed in the [contrib directory](https://github.com/PrefectHQ/fastmcp/tree/main/fastmcp_slim/fastmcp/contrib).
## Usage
To use a contrib module, import it from the `fastmcp.contrib` package:
-```python
+```python test="skip"
from fastmcp.contrib import my_module
```
@@ -32,7 +32,7 @@ from fastmcp.contrib import my_module
We welcome contributions to the `contrib` package! If you have a module that extends FastMCP in a useful way, consider contributing it:
-1. Create a new directory in `src/fastmcp/contrib/` for your module
+1. Create a new directory in `fastmcp_slim/fastmcp/contrib/` for your module
3. Add proper tests for your module in `tests/contrib/`
2. Include comprehensive documentation in a README.md file, including usage and examples, as well as any additional dependencies or installation instructions
5. Submit a pull request
diff --git a/docs/v2/servers/auth/authentication.mdx b/docs/v2/servers/auth/authentication.mdx
index c42bfa8bf..c6b829bfe 100644
--- a/docs/v2/servers/auth/authentication.mdx
+++ b/docs/v2/servers/auth/authentication.mdx
@@ -161,7 +161,7 @@ The implementation provides all required OAuth endpoints including authorization
```python
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.oauth import MyOAuthProvider
+from fastmcp.server.auth import OAuthProvider
auth = MyOAuthProvider(
user_store=your_user_database,
diff --git a/docs/v2/servers/auth/oauth-proxy.mdx b/docs/v2/servers/auth/oauth-proxy.mdx
index b9bc03430..678c396b5 100644
--- a/docs/v2/servers/auth/oauth-proxy.mdx
+++ b/docs/v2/servers/auth/oauth-proxy.mdx
@@ -115,6 +115,12 @@ mcp = FastMCP(name="My Server", auth=auth)
This URL is used to construct OAuth callback URLs and operational endpoints. When mounting under a path prefix, include that prefix in `base_url`. Use `issuer_url` separately to specify where auth server metadata is located (typically at root level).
+
+ Optional public base URL for the protected resource metadata and token audience.
+
+ Use this when your OAuth callbacks and operational endpoints need to live under one public URL, but the protected MCP resource should be advertised under another. FastMCP will still append the MCP mount path (for example, `/mcp`) to this base URL.
+
+
Path for OAuth callbacks. Must match the redirect URI configured in your OAuth
application
@@ -281,14 +287,22 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
-
- Whether to require user consent before authorizing MCP clients. When enabled (default), users see a consent screen that displays which client is requesting access, preventing [confused deputy attacks](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#confused-deputy-problem) by ensuring users explicitly approve new clients.
+
+ Consent screen behavior for authorization requests. The consent page displays which client is requesting access, defending against [confused deputy and AS-in-the-middle attacks](#confused-deputy-attacks) by requiring explicit user approval.
- **Default behavior (True):**
- Users see a consent screen on first authorization. Consent choices are remembered via signed cookies, so users only need to approve each client once. This protects against malicious clients impersonating the user.
+ **`True` (default) — always prompt:**
+ Users see the consent screen on every authorization. Strongest protection against AS-in-the-middle attacks where a malicious MCP server redirects the victim's browser into a legitimate proxy and relies on a previously-remembered approval to silently complete the flow.
- **Disabling consent (False):**
- Authorization proceeds directly to the upstream provider without user confirmation. Only use this for local development or testing environments where the security trade-off is acceptable.
+ **`"remember"` — silent consent on return:**
+ Users see the consent screen on first authorization; subsequent flows from the same browser for the same `(client_id, redirect_uri)` are silently approved via a signed cookie. Cross-site navigations (detected via `Sec-Fetch-Site`) fall back to the prompt. `Sec-Fetch-Site` is a browser-level heuristic rather than a protocol guarantee: an attacker who finds a way to initiate a non-cross-site navigation (XSS on a sibling origin, a same-site redirect chain, etc.) can reach the silent-consent path. `True` does not depend on this signal. See [Confused Deputy Attacks](#confused-deputy-attacks) for the underlying attack class.
+
+ **`"external"` — externally managed:**
+ Follows the same authorization path as `False`: FastMCP skips its consent page and associated browser-binding protections, then redirects directly to the upstream provider. The difference is logging. `False` emits a security warning, while `"external"` suppresses that warning as an explicit acknowledgment that the operator is enforcing equivalent consent and transaction-binding protections elsewhere. FastMCP does not provide or verify those external protections.
+
+ Ordinary upstream OAuth consent is generally not equivalent. It typically authorizes FastMCP's shared upstream application without identifying the downstream MCP client or binding approval to that client's transaction. Use `"external"` only when your surrounding authorization system supplies those protections.
+
+ **`False` — disable entirely:**
+ Authorization proceeds directly to the upstream provider without any consent UI. Logs a security warning. Only for local development or testing.
```python
# Development/testing only - skip consent screen
@@ -296,10 +310,16 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
...,
require_authorization_consent=False # ⚠️ Security warning: only for local/testing
)
+
+ # Convenience mode - silent consent on return visits (less safe than True)
+ auth = OAuthProxy(
+ ...,
+ require_authorization_consent="remember",
+ )
```
- Disabling consent removes an important security layer. Only disable for local development or testing environments where you fully control all connecting clients.
+ Both `False` and `"external"` disable FastMCP's consent and browser-binding protections. `False` warns about this configuration; `"external"` suppresses the warning because it is an operator acknowledgment that equivalent protections exist elsewhere. Prefer the default `True` unless you own that external authorization flow.
@@ -548,13 +568,17 @@ The OAuth proxy works by bridging DCR clients to traditional auth providers, whi
#### Mitigation
-FastMCP's OAuth proxy requires you to explicitly consent whenever any new or unrecognized client attempts to connect to your server. Before any authorization happens, you see a consent page showing the client's details, redirect URI, and requested scopes. This gives you the opportunity to review and deny suspicious requests. Once you approve a client, it's remembered so you don't see the consent page again for that client. The consent mechanism is implemented with CSRF tokens and cryptographically signed cookies to prevent tampering.
+FastMCP's OAuth proxy requires you to explicitly consent whenever a client attempts to connect to your server. Before any authorization happens, you see a consent page showing the client's details, redirect URI, and requested scopes. This gives you the opportunity to review and deny suspicious requests. By default (`require_authorization_consent=True`), the page is shown on every flow, which is the strongest protection. Setting `require_authorization_consent="remember"` approves previously-approved `(client_id, redirect_uri)` pairs silently on return visits, trading some protection for UX (see below). The consent mechanism is implemented with CSRF tokens and cryptographically signed cookies to prevent tampering.

The consent page automatically displays your server's name, icon, and website URL, if available. These visual identifiers help users confirm they're authorizing the correct server.
+#### AS-in-the-middle variant
+A related attack works by positioning a malicious authorization server between an MCP client and a legitimate proxy: a malicious MCP server advertises its own authorization server, which redirects the victim's browser into the legitimate proxy's `/authorize` endpoint. Because the victim's browser carries the prior-approval cookie throughout, a `"remember"`-mode proxy would silently complete the flow. The defense is the consent prompt itself: if consent is shown (`require_authorization_consent=True`), the victim sees the benign MCP server's name on the consent page — which doesn't match the malicious server they thought they were connecting to — and can deny.
+
+`require_authorization_consent="remember"` adds a `Sec-Fetch-Site` check to keep this path safe for legitimate return flows (the attack navigation lands as `cross-site` and falls back to the prompt), but this is a browser-level heuristic. For the strongest defense, leave `require_authorization_consent=True`.
**Learn more:**
- [MCP Security Best Practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#confused-deputy-problem) - Official specification guidance
diff --git a/docs/v2/servers/auth/oidc-proxy.mdx b/docs/v2/servers/auth/oidc-proxy.mdx
index 0b3a21d71..a7988995e 100644
--- a/docs/v2/servers/auth/oidc-proxy.mdx
+++ b/docs/v2/servers/auth/oidc-proxy.mdx
@@ -79,6 +79,12 @@ mcp = FastMCP(name="My Server", auth=auth)
Public URL of your FastMCP server (e.g., `https://your-server.com`)
+
+ Optional public base URL for the protected resource metadata and token audience.
+
+ Use this when your OAuth callbacks and operational endpoints need to live under one public URL, but the protected MCP resource should be advertised under another. FastMCP will still append the MCP mount path (for example, `/mcp`) to this base URL.
+
+
Strict flag for configuration validation. When True, requires all OIDC
mandatory fields.
@@ -192,8 +198,8 @@ auth = OIDCProxy(
-
- Whether to require user consent before authorizing MCP clients. When enabled (default), users see a consent screen that displays which client is requesting access. See [OAuthProxy documentation](/v2/servers/auth/oauth-proxy#confused-deputy-attacks) for details on confused deputy attack protection.
+
+ Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (same authorization path as `False`, but the warning is suppressed because the operator asserts that equivalent protections are enforced externally), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/v2/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs.
diff --git a/docs/v2/servers/proxy.mdx b/docs/v2/servers/proxy.mdx
index c60b628f7..b5ceb4b97 100644
--- a/docs/v2/servers/proxy.mdx
+++ b/docs/v2/servers/proxy.mdx
@@ -52,7 +52,7 @@ The recommended way to create a proxy is using `ProxyClient`, which provides ful
```python
from fastmcp import FastMCP
-from fastmcp.server.proxy import ProxyClient
+from fastmcp.server.providers.proxy import ProxyClient
# Create a proxy with full MCP feature support
proxy = FastMCP.as_proxy(
@@ -86,7 +86,7 @@ FastMCP proxies provide session isolation to ensure safe concurrent operations.
When you pass a disconnected client (which is the normal case), each request gets its own isolated backend session:
```python
-from fastmcp.server.proxy import ProxyClient
+from fastmcp.server.providers.proxy import ProxyClient
# Each request creates a fresh backend session (recommended)
proxy = FastMCP.as_proxy(ProxyClient("backend_server.py"))
@@ -121,7 +121,7 @@ A common use case is bridging transports - exposing a server running on one tran
```python
from fastmcp import FastMCP
-from fastmcp.server.proxy import ProxyClient
+from fastmcp.server.providers.proxy import ProxyClient
# Bridge remote SSE server to local stdio
remote_proxy = FastMCP.as_proxy(
@@ -164,7 +164,7 @@ if __name__ == "__main__":
- **Progress**: Forwards progress notifications during long operations
```python
-from fastmcp.server.proxy import ProxyClient
+from fastmcp.server.providers.proxy import ProxyClient
# ProxyClient automatically handles all these features
backend = ProxyClient("advanced_backend.py")
@@ -304,7 +304,7 @@ Internally, `FastMCP.as_proxy()` uses the `FastMCPProxy` class. You generally do
### Direct Usage
```python
-from fastmcp.server.proxy import FastMCPProxy, ProxyClient
+from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient
# Provide a client factory for explicit session control
def create_client():
diff --git a/docs/v2/servers/resources.mdx b/docs/v2/servers/resources.mdx
index a76734f12..f472df5ae 100644
--- a/docs/v2/servers/resources.mdx
+++ b/docs/v2/servers/resources.mdx
@@ -401,7 +401,7 @@ FastMCP implements [RFC 6570 URI Templates](https://datatracker.ietf.org/doc/htm
-Resource templates support wildcard parameters that can match multiple path segments. While standard parameters (`{param}`) only match a single path segment and don't cross "/" boundaries, wildcard parameters (`{param*}`) can capture multiple segments including slashes. Wildcards capture all subsequent path segments *up until* the defined part of the URI template (whether literal or another parameter). This allows you to have multiple wildcard parameters in a single URI template.
+Resource templates support wildcard parameters that can match multiple path segments. Standard parameters (`{param}`) match a single URI segment before decoding and do not cross literal "/" boundaries in the request URI. Wildcard parameters (`{param*}`) can capture multiple segments including slashes. Wildcards capture all subsequent path segments *up until* the defined part of the URI template (whether literal or another parameter). This allows you to have multiple wildcard parameters in a single URI template.
```python {15, 23}
from fastmcp import FastMCP
@@ -446,6 +446,34 @@ Wildcard parameters are useful when:
Note that like regular parameters, each wildcard parameter must still be a named parameter in your function signature, and all required function parameters must appear in the URI template.
+#### Filesystem Path Safety
+
+Template parameters are decoded before your function receives them. A standard `{filename}` parameter matches one URI segment before decoding, so a request like `files://a%2Fb` passes `filename="a/b"` to the handler. Treat template values as untrusted decoded URI data whenever they determine filesystem paths.
+
+Validate the final resolved path against an allowed root before reading:
+
+```python
+from pathlib import Path
+
+from fastmcp import FastMCP
+from fastmcp.exceptions import ResourceError
+
+mcp = FastMCP(name="DocsServer")
+DOCS_ROOT = Path("docs").resolve()
+
+
+@mcp.resource("docs://{filename}")
+def read_doc(filename: str) -> str:
+ requested_path = (DOCS_ROOT / filename).resolve()
+
+ if not requested_path.is_relative_to(DOCS_ROOT) or not requested_path.is_file():
+ raise ResourceError("Document not found")
+
+ return requested_path.read_text(encoding="utf-8")
+```
+
+Use wildcard parameters (`{path*}`) for resources whose URI shape intentionally includes slashes, and apply the same containment check before accessing the filesystem.
+
#### Query Parameters
@@ -634,4 +662,4 @@ The duplicate behavior options are:
- `"warn"` (default): Logs a warning, and the new resource/template replaces the old one.
- `"error"`: Raises a `ValueError`, preventing the duplicate registration.
- `"replace"`: Silently replaces the existing resource/template with the new one.
-- `"ignore"`: Keeps the original resource/template and ignores the new registration attempt.
\ No newline at end of file
+- `"ignore"`: Keeps the original resource/template and ignores the new registration attempt.
diff --git a/docs/v2/servers/sampling.mdx b/docs/v2/servers/sampling.mdx
index 5c21a0bde..150c9ac5f 100644
--- a/docs/v2/servers/sampling.mdx
+++ b/docs/v2/servers/sampling.mdx
@@ -446,7 +446,7 @@ Client support for sampling is optional—some clients may not implement it. To
FastMCP provides built-in handlers for [OpenAI and Anthropic APIs](/v2/clients/sampling#built-in-handlers). These handlers support the full sampling API including tools, automatically converting your Python functions to each provider's format.
-Install handlers with `pip install fastmcp[openai]` or `pip install fastmcp[anthropic]`.
+Install handlers with `pip install 'fastmcp[openai]'` or `pip install 'fastmcp[anthropic]'`.
```python
diff --git a/docs/v2/servers/storage-backends.mdx b/docs/v2/servers/storage-backends.mdx
index cd14ab80e..25b8580b0 100644
--- a/docs/v2/servers/storage-backends.mdx
+++ b/docs/v2/servers/storage-backends.mdx
@@ -236,13 +236,13 @@ middleware = ResponseCachingMiddleware(cache_storage=namespaced_store)
The [FastMCP Client](/v2/clients/client) uses storage for persisting OAuth tokens locally. By default, tokens are stored in memory:
```python
-from fastmcp.client.auth import OAuthClientProvider
+from fastmcp.client.auth import OAuth
from key_value.aio.stores.disk import DiskStore
# Store tokens on disk for persistence across restarts
token_storage = DiskStore(directory="~/.local/share/fastmcp/tokens")
-oauth_provider = OAuthClientProvider(
+oauth_provider = OAuth(
mcp_url="https://your-mcp-server.com/mcp/sse",
token_storage=token_storage
)
diff --git a/docs/v2/servers/tools.mdx b/docs/v2/servers/tools.mdx
index 1ec8d62be..3cdcf3be5 100644
--- a/docs/v2/servers/tools.mdx
+++ b/docs/v2/servers/tools.mdx
@@ -792,15 +792,17 @@ Annotations serve several purposes in client applications:
- Describing the safety profile of tools (destructive vs. non-destructive)
- Signaling if tools interact with external systems
-You can add annotations to a tool using the `annotations` parameter in the `@mcp.tool` decorator:
+You can add annotations to a tool using the `annotations` parameter in the `@mcp.tool` decorator. FastMCP accepts either a plain dict or `ToolAnnotations`; the examples below use `ToolAnnotations` for consistency and stronger editor/type support.
```python
+from mcp.types import ToolAnnotations
+
@mcp.tool(
- annotations={
- "title": "Calculate Sum",
- "readOnlyHint": True,
- "openWorldHint": False
- }
+ annotations=ToolAnnotations(
+ title="Calculate Sum",
+ readOnlyHint=True,
+ openWorldHint=False,
+ )
)
def calculate_sum(a: float, b: float) -> float:
"""Add two numbers together."""
@@ -836,7 +838,7 @@ from mcp.types import ToolAnnotations
mcp = FastMCP("Data Server")
-@mcp.tool(annotations={"readOnlyHint": True})
+@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
def get_user(user_id: str) -> dict:
"""Retrieve user information by ID."""
return {"id": user_id, "name": "Alice"}
@@ -858,7 +860,7 @@ def update_user(user_id: str, name: str) -> dict:
"""Update user information."""
return {"id": user_id, "name": name, "updated": True}
-@mcp.tool(annotations={"destructiveHint": True})
+@mcp.tool(annotations=ToolAnnotations(destructiveHint=True))
def delete_user(user_id: str) -> dict:
"""Permanently delete a user account."""
return {"deleted": user_id}
diff --git a/docs/v2/tutorials/rest-api.mdx b/docs/v2/tutorials/rest-api.mdx
index 362eb0026..6524a2335 100644
--- a/docs/v2/tutorials/rest-api.mdx
+++ b/docs/v2/tutorials/rest-api.mdx
@@ -152,7 +152,7 @@ Here’s how you can add custom route maps to turn `GET` requests into `Resource
```python api_server_with_resources.py {3, 37-42}
import httpx
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
# Create an HTTP client for the target API
diff --git a/docs/v2/updates.mdx b/docs/v2/updates.mdx
index 65e212e8d..903745a01 100644
--- a/docs/v2/updates.mdx
+++ b/docs/v2/updates.mdx
@@ -5,6 +5,26 @@ icon: "sparkles"
tag: NEW
---
+
+
+A 2.x backport of the fakeredis pin: fakeredis 2.35.0 renamed a connection class that pydocket's `memory://` backend relied on, crashing `fastmcp[tasks]` installs at startup. Caps `fakeredis<2.35.0` on the 2.x line.
+
+
+
+
+
+v2.14.4 backported `dereference_refs()` but never wired it into the tool schema pipeline — `$ref` and `$defs` were still sent to MCP clients. Now fixed: schemas are fully inlined before reaching clients.
+
+
+
{
+ if (location.href !== lastUrl) {
+ lastUrl = location.href;
+ setTimeout(addBanner, 100);
+ }
+ }).observe(document.body, {subtree: true, childList: true});
+})();
diff --git a/docs/v3-navigation.json b/docs/v3-navigation.json
new file mode 100644
index 000000000..24c451b2a
--- /dev/null
+++ b/docs/v3-navigation.json
@@ -0,0 +1,312 @@
+{
+ "dropdowns": [
+ {
+ "dropdown": "Documentation",
+ "groups": [
+ {
+ "group": "Get Started",
+ "pages": [
+ "v3/getting-started/welcome",
+ "v3/getting-started/installation",
+ "v3/getting-started/quickstart"
+ ]
+ },
+ {
+ "group": "Servers",
+ "pages": [
+ "v3/servers/server",
+ {
+ "collapsed": true,
+ "group": "Core Components",
+ "icon": "toolbox",
+ "pages": [
+ "v3/servers/tools",
+ "v3/servers/resources",
+ "v3/servers/prompts",
+ "v3/servers/context"
+ ]
+ },
+ {
+ "collapsed": true,
+ "group": "Working with Tools",
+ "icon": "wand-magic-sparkles",
+ "pages": [
+ "v3/servers/transforms/transforms",
+ "v3/servers/transforms/tool-transformation",
+ "v3/servers/transforms/code-mode",
+ "v3/servers/transforms/tool-search",
+ "v3/servers/transforms/namespace",
+ "v3/servers/visibility",
+ "v3/servers/transforms/resources-as-tools",
+ "v3/servers/transforms/prompts-as-tools",
+ "v3/servers/tool-fingerprinting"
+ ]
+ },
+ {
+ "collapsed": true,
+ "group": "MCP Providers",
+ "icon": "layer-group",
+ "pages": [
+ "v3/servers/providers/overview",
+ "v3/servers/providers/local",
+ "v3/servers/providers/filesystem",
+ "v3/servers/providers/proxy",
+ "v3/servers/providers/skills",
+ "v3/servers/composition",
+ "v3/servers/providers/custom"
+ ]
+ },
+ {
+ "collapsed": true,
+ "group": "Interactivity",
+ "icon": "comments",
+ "pages": [
+ "v3/servers/elicitation",
+ "v3/servers/sampling",
+ "v3/servers/progress",
+ "v3/servers/logging",
+ "v3/servers/pagination",
+ "v3/servers/icons"
+ ]
+ },
+ {
+ "collapsed": true,
+ "group": "Extensibility",
+ "icon": "puzzle-piece",
+ "pages": [
+ "v3/servers/middleware",
+ "v3/servers/dependency-injection",
+ "v3/servers/lifespan",
+ "v3/servers/storage-backends",
+ "v3/servers/tasks",
+ "v3/servers/versioning"
+ ]
+ },
+ {
+ "collapsed": true,
+ "group": "Auth",
+ "icon": "shield-check",
+ "pages": [
+ {
+ "collapsed": true,
+ "group": "Authentication",
+ "icon": "key",
+ "pages": [
+ "v3/servers/auth/authentication",
+ "v3/servers/auth/token-verification",
+ "v3/servers/auth/remote-oauth",
+ "v3/servers/auth/oauth-proxy",
+ "v3/servers/auth/oidc-proxy",
+ "v3/servers/auth/full-oauth-server",
+ "v3/servers/auth/multi-auth"
+ ]
+ },
+ "v3/servers/authorization"
+ ]
+ },
+ {
+ "collapsed": true,
+ "group": "Deployment",
+ "icon": "rocket",
+ "pages": [
+ "v3/deployment/running-server",
+ "v3/deployment/http",
+ "v3/deployment/sandboxed-agents",
+ "v3/deployment/prefect-horizon",
+ "v3/deployment/server-configuration",
+ "v3/servers/testing",
+ "v3/servers/telemetry"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Apps",
+ "pages": [
+ "v3/apps/overview",
+ "v3/apps/quickstart",
+ "v3/apps/fastmcp-app",
+ "v3/apps/prefab",
+ "v3/apps/generative",
+ "v3/apps/low-level",
+ {
+ "collapsed": true,
+ "group": "Reference",
+ "icon": "book",
+ "pages": [
+ {
+ "collapsed": true,
+ "group": "Prefab Providers",
+ "icon": "cube",
+ "pages": [
+ "v3/apps/providers/approval",
+ "v3/apps/providers/choice",
+ "v3/apps/providers/file-upload",
+ "v3/apps/providers/form"
+ ]
+ },
+ "v3/apps/development",
+ "v3/apps/examples",
+ "v3/apps/architecture"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Clients",
+ "pages": [
+ "v3/clients/client",
+ "v3/clients/client-only-package",
+ "v3/clients/transports",
+ "v3/clients/fastmcp-remote",
+ {
+ "collapsed": true,
+ "group": "Operations",
+ "icon": "toolbox",
+ "pages": [
+ "v3/clients/tools",
+ "v3/clients/resources",
+ "v3/clients/prompts",
+ "v3/clients/sampling",
+ "v3/clients/elicitation",
+ "v3/clients/tasks",
+ "v3/clients/progress",
+ "v3/clients/logging",
+ "v3/clients/roots",
+ "v3/clients/notifications"
+ ],
+ "tag": "UPDATED"
+ },
+ {
+ "collapsed": true,
+ "group": "Authentication",
+ "icon": "key",
+ "pages": [
+ "v3/clients/auth/oauth",
+ "v3/clients/auth/cimd",
+ "v3/clients/auth/bearer"
+ ],
+ "tag": "UPDATED"
+ }
+ ]
+ },
+ {
+ "group": "Integrations",
+ "pages": [
+ {
+ "collapsed": true,
+ "group": "Auth",
+ "icon": "key",
+ "pages": [
+ "v3/integrations/auth0",
+ "v3/integrations/authkit",
+ "v3/integrations/aws-cognito",
+ "v3/integrations/azure",
+ "v3/integrations/descope",
+ "v3/integrations/discord",
+ "v3/integrations/eunomia-authorization",
+ "v3/integrations/github",
+ "v3/integrations/google",
+ "v3/integrations/huggingface",
+ "v3/integrations/keycloak",
+ "v3/integrations/oci",
+ "v3/integrations/permit",
+ "v3/integrations/propelauth",
+ "v3/integrations/scalekit",
+ "v3/integrations/supabase",
+ "v3/integrations/workos"
+ ]
+ },
+ {
+ "collapsed": true,
+ "group": "Web Frameworks",
+ "icon": "code",
+ "pages": [
+ "v3/integrations/fastapi",
+ "v3/integrations/openapi"
+ ]
+ },
+ {
+ "collapsed": true,
+ "group": "AI Assistants",
+ "icon": "robot",
+ "pages": [
+ "v3/integrations/chatgpt",
+ "v3/integrations/claude-code",
+ "v3/integrations/claude-desktop",
+ "v3/integrations/cursor",
+ "v3/integrations/gemini-cli",
+ "v3/integrations/goose"
+ ]
+ },
+ {
+ "collapsed": true,
+ "group": "AI SDKs",
+ "icon": "microchip",
+ "pages": [
+ "v3/integrations/anthropic",
+ "v3/integrations/gemini",
+ "v3/integrations/openai",
+ "v3/integrations/pydantic-ai"
+ ]
+ },
+ "v3/integrations/mcp-json-configuration"
+ ]
+ },
+ {
+ "group": "More",
+ "pages": [
+ "v3/more/settings",
+ {
+ "collapsed": true,
+ "group": "CLI",
+ "icon": "terminal",
+ "pages": [
+ "v3/cli/overview",
+ "v3/cli/running",
+ "v3/cli/install-mcp",
+ "v3/cli/inspecting",
+ "v3/cli/client",
+ "v3/cli/generate-cli",
+ "v3/cli/auth"
+ ]
+ },
+ {
+ "collapsed": true,
+ "group": "Upgrading",
+ "icon": "up",
+ "pages": [
+ "v3/getting-started/upgrading/from-fastmcp-2",
+ "v3/getting-started/upgrading/from-mcp-sdk",
+ "v3/getting-started/upgrading/from-low-level-sdk"
+ ]
+ },
+ {
+ "collapsed": true,
+ "group": "Development",
+ "icon": "code",
+ "pages": [
+ "v3/development/contributing",
+ "v3/development/tests",
+ "v3/development/releases",
+ "v3/patterns/contrib"
+ ]
+ },
+ {
+ "collapsed": true,
+ "group": "What's New",
+ "icon": "sparkles",
+ "pages": [
+ "v3/updates",
+ "v3/changelog"
+ ]
+ },
+ "v3/more/faq"
+ ]
+ }
+ ],
+ "icon": "book"
+ }
+ ],
+ "version": "v3.4.4"
+}
diff --git a/docs/v3/apps/architecture.mdx b/docs/v3/apps/architecture.mdx
new file mode 100644
index 000000000..7ecab2aa3
--- /dev/null
+++ b/docs/v3/apps/architecture.mdx
@@ -0,0 +1,118 @@
+---
+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'
+
+
+
+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 checks whether the tool's return type is a Prefab type (`PrefabApp`, `Component`, or unions containing them). If it qualifies, FastMCP expands `True` into a full `AppConfig` — setting the renderer URI, CSP headers, and visibility — and stores it in the tool's `meta["ui"]` dict.
+
+This expansion also registers the shared Prefab renderer resource (below). The tool and the renderer are linked through a `resourceUri` field in the metadata: the tool says "render me with `ui://prefab/renderer.html`" and the host fetches that resource when it displays the result.
+
+Type inference works the same way. If the return type is a Prefab type and you haven't set `app` explicitly, FastMCP auto-wires the metadata as if you'd written `app=True`.
+
+### 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. This is how `CallTool(save_contact)` becomes `CallTool("save_contact")` on the wire. 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.
+
+### The `_meta.fastmcp.app` tag
+
+After `to_json()` produces the tree, FastMCP injects `_meta.fastmcp.app` with the app's name (if the tool belongs to a `FastMCPApp`). This tag rides along inside `structuredContent` all the way to the renderer.
+
+When the renderer calls a backend tool, it includes `_meta.fastmcp.app` in the `CallTool` request. The server sees this tag and routes the call through a special path that bypasses transforms (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
+
+Normal tool calls go through the provider chain, which applies transforms (namespace prefixes, visibility filters) before resolving by name. App UI calls need a different path.
+
+### The `get_app_tool` bypass
+
+Backend tools are typically hidden from the model (`visibility=["app"]`). Visibility transforms would filter them out of normal resolution. And namespace transforms might rename them — `save_contact` becomes `contacts_save_contact` — while the renderer still uses the original name.
+
+`get_app_tool` solves both problems. When the server sees `_meta.fastmcp.app` on an incoming `CallTool` request, it calls `get_app_tool(app_name, tool_name)` instead of the normal `get_tool(name)`. This walks the provider tree directly, skipping transforms. It finds the tool by its original registered name and verifies that its `meta["fastmcp"]["app"]` matches the expected app.
+
+That's why `CallTool("save_contact")` keeps working when the server is mounted under a namespace. The renderer sends the original name plus the app identity; the server uses `get_app_tool` to find it without transforms in the way.
+
+Authorization still applies. `get_app_tool` bypasses transforms but runs auth checks against the tool's `auth` config before executing.
+
+### Provider delegation
+
+`get_app_tool` 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 `get_app_tool`. 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.
+
+### The shared resource
+
+FastMCP registers the renderer as a `ui://prefab/renderer.html` resource 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. All Prefab tools on a server share this single resource.
+
+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, including `_meta.fastmcp.app` for routing.
+
+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.
diff --git a/docs/v3/apps/demos/bar-chart.py b/docs/v3/apps/demos/bar-chart.py
new file mode 100644
index 000000000..e2430b981
--- /dev/null
+++ b/docs/v3/apps/demos/bar-chart.py
@@ -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,
+ )
diff --git a/docs/v3/apps/demos/contacts.py b/docs/v3/apps/demos/contacts.py
new file mode 100644
index 000000000..0cbe60c0b
--- /dev/null
+++ b/docs/v3/apps/demos/contacts.py
@@ -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")
diff --git a/docs/v3/apps/demos/dashboard.py b/docs/v3/apps/demos/dashboard.py
new file mode 100644
index 000000000..06fe6285d
--- /dev/null
+++ b/docs/v3/apps/demos/dashboard.py
@@ -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,
+ )
diff --git a/docs/v3/apps/demos/data-table.py b/docs/v3/apps/demos/data-table.py
new file mode 100644
index 000000000..5100237bf
--- /dev/null
+++ b/docs/v3/apps/demos/data-table.py
@@ -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,
+ )
diff --git a/docs/v3/apps/demos/hitchhikers.py b/docs/v3/apps/demos/hitchhikers.py
new file mode 100644
index 000000000..1554e5165
--- /dev/null
+++ b/docs/v3/apps/demos/hitchhikers.py
@@ -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 3–4: 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,
+ )
diff --git a/docs/v3/apps/demos/pie-chart.py b/docs/v3/apps/demos/pie-chart.py
new file mode 100644
index 000000000..c1fb489e4
--- /dev/null
+++ b/docs/v3/apps/demos/pie-chart.py
@@ -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,
+ )
diff --git a/docs/v3/apps/demos/reactive.py b/docs/v3/apps/demos/reactive.py
new file mode 100644
index 000000000..16f2f9829
--- /dev/null
+++ b/docs/v3/apps/demos/reactive.py
@@ -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",
+ )
diff --git a/docs/v3/apps/demos/team-directory-reactive.py b/docs/v3/apps/demos/team-directory-reactive.py
new file mode 100644
index 000000000..b6aa004f7
--- /dev/null
+++ b/docs/v3/apps/demos/team-directory-reactive.py
@@ -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"))
diff --git a/docs/v3/apps/demos/team-directory.py b/docs/v3/apps/demos/team-directory.py
new file mode 100644
index 000000000..7cfe21bc9
--- /dev/null
+++ b/docs/v3/apps/demos/team-directory.py
@@ -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,
+ )
diff --git a/docs/v3/apps/development.mdx b/docs/v3/apps/development.mdx
new file mode 100644
index 000000000..0d3a71ac7
--- /dev/null
+++ b/docs/v3/apps/development.mdx
@@ -0,0 +1,65 @@
+---
+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'
+
+
+
+
+
+
+
+`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 |
+
+## 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
+```
diff --git a/docs/v3/apps/examples.mdx b/docs/v3/apps/examples.mdx
new file mode 100644
index 000000000..5078120e7
--- /dev/null
+++ b/docs/v3/apps/examples.mdx
@@ -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'
+
+
+
+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.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## 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.
diff --git a/docs/v3/apps/fastmcp-app.mdx b/docs/v3/apps/fastmcp-app.mdx
new file mode 100644
index 000000000..55b3b7ed7
--- /dev/null
+++ b/docs/v3/apps/fastmcp-app.mdx
@@ -0,0 +1,470 @@
+---
+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'
+
+
+
+
+
+
+
+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 by default. Backend tools get globally stable identifiers that survive namespacing, and `CallTool` accepts function references, so references stay valid when you compose servers.
+
+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
diff --git a/docs/v3/apps/generative.mdx b/docs/v3/apps/generative.mdx
new file mode 100644
index 000000000..b6293d32b
--- /dev/null
+++ b/docs/v3/apps/generative.mdx
@@ -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'
+
+
+
+
+
+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`
diff --git a/docs/v3/apps/images/app-approval.png b/docs/v3/apps/images/app-approval.png
new file mode 100644
index 000000000..162f4847f
Binary files /dev/null and b/docs/v3/apps/images/app-approval.png differ
diff --git a/docs/v3/apps/images/app-chart.png b/docs/v3/apps/images/app-chart.png
new file mode 100644
index 000000000..cfc816d0e
Binary files /dev/null and b/docs/v3/apps/images/app-chart.png differ
diff --git a/docs/v3/apps/images/app-choice.png b/docs/v3/apps/images/app-choice.png
new file mode 100644
index 000000000..178f6a2b0
Binary files /dev/null and b/docs/v3/apps/images/app-choice.png differ
diff --git a/docs/v3/apps/images/app-contacts.png b/docs/v3/apps/images/app-contacts.png
new file mode 100644
index 000000000..5d74f7cb9
Binary files /dev/null and b/docs/v3/apps/images/app-contacts.png differ
diff --git a/src/fastmcp/experimental/__init__.py b/docs/v3/apps/images/app-datatable.png
similarity index 100%
rename from src/fastmcp/experimental/__init__.py
rename to docs/v3/apps/images/app-datatable.png
diff --git a/docs/v3/apps/images/app-example-map.png b/docs/v3/apps/images/app-example-map.png
new file mode 100644
index 000000000..5859c59c2
Binary files /dev/null and b/docs/v3/apps/images/app-example-map.png differ
diff --git a/docs/v3/apps/images/app-example-quiz.png b/docs/v3/apps/images/app-example-quiz.png
new file mode 100644
index 000000000..b16bcaf43
Binary files /dev/null and b/docs/v3/apps/images/app-example-quiz.png differ
diff --git a/docs/v3/apps/images/app-example-sales-dashboard.png b/docs/v3/apps/images/app-example-sales-dashboard.png
new file mode 100644
index 000000000..e0fe709a9
Binary files /dev/null and b/docs/v3/apps/images/app-example-sales-dashboard.png differ
diff --git a/docs/v3/apps/images/app-example-system-dashboard.png b/docs/v3/apps/images/app-example-system-dashboard.png
new file mode 100644
index 000000000..7b85d7ac1
Binary files /dev/null and b/docs/v3/apps/images/app-example-system-dashboard.png differ
diff --git a/docs/v3/apps/images/app-file-upload.png b/docs/v3/apps/images/app-file-upload.png
new file mode 100644
index 000000000..1178c09af
Binary files /dev/null and b/docs/v3/apps/images/app-file-upload.png differ
diff --git a/docs/v3/apps/images/app-form.png b/docs/v3/apps/images/app-form.png
new file mode 100644
index 000000000..30567e37e
Binary files /dev/null and b/docs/v3/apps/images/app-form.png differ
diff --git a/docs/v3/apps/images/app-greet.png b/docs/v3/apps/images/app-greet.png
new file mode 100644
index 000000000..70a0e4412
Binary files /dev/null and b/docs/v3/apps/images/app-greet.png differ
diff --git a/docs/v3/apps/images/app-overview.png b/docs/v3/apps/images/app-overview.png
new file mode 100644
index 000000000..35f68fd58
Binary files /dev/null and b/docs/v3/apps/images/app-overview.png differ
diff --git a/docs/v3/apps/images/app-quickstart-dev-2.png b/docs/v3/apps/images/app-quickstart-dev-2.png
new file mode 100644
index 000000000..f04d96d72
Binary files /dev/null and b/docs/v3/apps/images/app-quickstart-dev-2.png differ
diff --git a/docs/v3/apps/images/app-quickstart-dev.png b/docs/v3/apps/images/app-quickstart-dev.png
new file mode 100644
index 000000000..d043f0ed3
Binary files /dev/null and b/docs/v3/apps/images/app-quickstart-dev.png differ
diff --git a/docs/v3/apps/images/app-quickstart.png b/docs/v3/apps/images/app-quickstart.png
new file mode 100644
index 000000000..ddca745cf
Binary files /dev/null and b/docs/v3/apps/images/app-quickstart.png differ
diff --git a/docs/v3/apps/images/app-showcase.png b/docs/v3/apps/images/app-showcase.png
new file mode 100644
index 000000000..c03294bdb
Binary files /dev/null and b/docs/v3/apps/images/app-showcase.png differ
diff --git a/docs/v3/apps/images/dev-app.png b/docs/v3/apps/images/dev-app.png
new file mode 100644
index 000000000..fdb05d69e
Binary files /dev/null and b/docs/v3/apps/images/dev-app.png differ
diff --git a/docs/v3/apps/images/generative-ui.mp4 b/docs/v3/apps/images/generative-ui.mp4
new file mode 100644
index 000000000..ca610181e
Binary files /dev/null and b/docs/v3/apps/images/generative-ui.mp4 differ
diff --git a/docs/v3/apps/low-level.mdx b/docs/v3/apps/low-level.mdx
new file mode 100644
index 000000000..ccef52b0a
--- /dev/null
+++ b/docs/v3/apps/low-level.mdx
@@ -0,0 +1,304 @@
+---
+title: Custom HTML Apps
+sidebarTitle: Custom HTML
+description: Build apps with your own HTML, CSS, and JavaScript using the MCP Apps extension directly.
+icon: code
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Everything on this page is for when you want full control: your own HTML, your own JavaScript framework, a map library, a 3D viewer, custom video playback. [Interactive Tools](/apps/prefab) wrap the MCP Apps extension so you never have to think about it — this page is what you reach for when you need to think about it.
+
+You'll be working with two things: the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) JavaScript SDK for host communication, and FastMCP's `AppConfig` for resources and CSP.
+
+## How it works
+
+An MCP App has two parts:
+
+1. A **tool** that does the work and returns data
+2. A **`ui://` resource** containing the HTML that renders that data
+
+The tool declares which resource to use via `AppConfig`. When the host calls the tool, it also fetches the linked resource, renders it in a sandboxed iframe, and pushes the tool result into the app via `postMessage`. The app can also call tools back, enabling interactive workflows.
+
+```python
+import json
+
+from fastmcp import FastMCP
+from fastmcp.apps import AppConfig, ResourceCSP
+
+mcp = FastMCP("My App Server")
+
+# The tool does the work
+@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
+def generate_chart(data: list[float]) -> str:
+ return json.dumps({"values": data})
+
+# The resource provides the UI
+@mcp.resource("ui://my-app/view.html")
+def chart_view() -> str:
+ return "..."
+```
+
+## AppConfig
+
+`AppConfig` controls how a tool or resource participates in the Apps extension. Import it from `fastmcp.server.apps`:
+
+```python
+from fastmcp.apps import AppConfig
+```
+
+On **tools**, you'll typically set `resource_uri` to point to the UI resource:
+
+```python
+@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
+def my_tool() -> str:
+ return "result"
+```
+
+You can also pass a raw dict with camelCase keys, matching the wire format:
+
+```python
+@mcp.tool(app={"resourceUri": "ui://my-app/view.html"})
+def my_tool() -> str:
+ return "result"
+```
+
+### Tool visibility
+
+The `visibility` field controls where a tool appears:
+
+- `["model"]` — visible to the LLM (the default behavior)
+- `["app"]` — only callable from within the app UI, hidden from the LLM
+- `["model", "app"]` — both
+
+This is useful when you have tools that only make sense as part of the app's interactive flow, not as standalone LLM actions.
+
+```python
+@mcp.tool(
+ app=AppConfig(
+ resource_uri="ui://my-app/view.html",
+ visibility=["app"],
+ )
+)
+def refresh_data() -> str:
+ """Only callable from the app UI, not by the LLM."""
+ return fetch_latest()
+```
+
+### AppConfig fields
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `resource_uri` | `str` | URI of the UI resource. Tools only. |
+| `visibility` | `list[str]` | Where the tool appears: `"model"`, `"app"`, or both. Tools only. |
+| `csp` | `ResourceCSP` | Content Security Policy for the iframe. |
+| `permissions` | `ResourcePermissions` | Iframe sandbox permissions. |
+| `domain` | `str` | Stable sandbox origin for the iframe. |
+| `prefers_border` | `bool` | Whether the UI prefers a visible border. |
+
+
+On **resources**, `resource_uri` and `visibility` must not be set — the resource *is* the UI. Use `AppConfig` on resources only for `csp`, `permissions`, and other display settings.
+
+
+## UI resources
+
+Resources using the `ui://` scheme are automatically served with the MIME type `text/html;profile=mcp-app`. No need to set it manually.
+
+```python
+@mcp.resource("ui://my-app/view.html")
+def my_view() -> str:
+ return "..."
+```
+
+The HTML can be anything — a full single-page app, a simple display, or a complex interactive tool. The host renders it in a sandboxed iframe and establishes a `postMessage` channel for communication.
+
+### Writing the app HTML
+
+Your HTML app communicates with the host using the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) JavaScript SDK. The simplest approach is to load it from a CDN:
+
+```html
+
+```
+
+The `App` object provides:
+
+- **`app.ontoolresult`** — callback that receives tool results pushed by the host
+- **`app.callServerTool({name, arguments})`** — call a tool on the server from within the app
+- **`app.onhostcontextchanged`** — callback for host context changes (e.g., safe area insets)
+- **`app.getHostContext()`** — get current host context
+
+See the full [ext-apps SDK documentation](https://github.com/modelcontextprotocol/ext-apps) for the complete API reference.
+
+
+If your HTML loads external scripts, styles, or makes API calls, you need to declare those domains in the CSP configuration. See [Security](#security) below.
+
+
+## Security
+
+Apps run in sandboxed iframes with a deny-by-default Content Security Policy. By default, only inline scripts and styles are allowed — no external network access.
+
+### Content Security Policy
+
+If your app needs to load external resources (CDN scripts, API calls, embedded iframes), declare the allowed domains with `ResourceCSP`:
+
+```python
+from fastmcp.apps import AppConfig, ResourceCSP
+
+@mcp.resource(
+ "ui://my-app/view.html",
+ app=AppConfig(
+ csp=ResourceCSP(
+ resource_domains=["https://unpkg.com", "https://cdn.example.com"],
+ connect_domains=["https://api.example.com"],
+ )
+ ),
+)
+def my_view() -> str:
+ return "..."
+```
+
+| CSP Field | Controls |
+|-----------|----------|
+| `connect_domains` | `fetch`, XHR, WebSocket (`connect-src`) |
+| `resource_domains` | Scripts, images, styles, fonts (`script-src`, etc.) |
+| `frame_domains` | Nested iframes (`frame-src`) |
+| `base_uri_domains` | Document base URI (`base-uri`) |
+
+### Permissions
+
+If your app needs browser capabilities like camera or clipboard access, request them via `ResourcePermissions`:
+
+```python
+from fastmcp.apps import AppConfig, ResourcePermissions
+
+@mcp.resource(
+ "ui://my-app/view.html",
+ app=AppConfig(
+ permissions=ResourcePermissions(
+ camera={},
+ clipboard_write={},
+ )
+ ),
+)
+def my_view() -> str:
+ return "..."
+```
+
+Hosts may or may not grant these permissions. Your app should use JavaScript feature detection as a fallback.
+
+## Example: a QR code server
+
+This example creates a tool that generates QR codes and an app that renders them as images. It's based on the [official MCP Apps example](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server). Requires the `qrcode[pil]` package.
+
+```python expandable
+import base64
+import io
+
+import qrcode
+from mcp import types
+
+from fastmcp import FastMCP
+from fastmcp.apps import AppConfig, ResourceCSP
+from fastmcp.tools import ToolResult
+
+mcp = FastMCP("QR Code Server")
+
+VIEW_URI = "ui://qr-server/view.html"
+
+
+@mcp.tool(app=AppConfig(resource_uri=VIEW_URI))
+def generate_qr(text: str = "https://gofastmcp.com") -> ToolResult:
+ """Generate a QR code from text."""
+ qr = qrcode.QRCode(version=1, box_size=10, border=4)
+ qr.add_data(text)
+ qr.make(fit=True)
+
+ img = qr.make_image()
+ buffer = io.BytesIO()
+ img.save(buffer, format="PNG")
+ b64 = base64.b64encode(buffer.getvalue()).decode()
+
+ return ToolResult(
+ content=[types.ImageContent(type="image", data=b64, mimeType="image/png")]
+ )
+
+
+@mcp.resource(
+ VIEW_URI,
+ app=AppConfig(csp=ResourceCSP(resource_domains=["https://unpkg.com"])),
+)
+def view() -> str:
+ """Interactive QR code viewer."""
+ return """\
+
+
+
+
+
+
+
+
+
+
+"""
+```
+
+The tool generates a QR code as a base64 PNG. The resource loads the MCP Apps JS SDK from unpkg (declared in the CSP), listens for tool results, and renders the image. The host wires them together — when the LLM calls `generate_qr`, the QR code appears in an interactive frame inside the conversation.
+
+## Checking client support
+
+Not all hosts support the Apps extension. You can check at runtime using the tool's [context](/servers/context):
+
+```python
+from fastmcp import Context
+from fastmcp.apps import AppConfig, UI_EXTENSION_ID
+
+@mcp.tool(app=AppConfig(resource_uri="ui://my-app/view.html"))
+async def my_tool(ctx: Context) -> str:
+ if ctx.client_supports_extension(UI_EXTENSION_ID):
+ # Return data optimized for UI rendering
+ return rich_response()
+ else:
+ # Fall back to plain text
+ return plain_text_response()
+```
diff --git a/docs/v3/apps/overview.mdx b/docs/v3/apps/overview.mdx
new file mode 100644
index 000000000..ff9557058
--- /dev/null
+++ b/docs/v3/apps/overview.mdx
@@ -0,0 +1,73 @@
+---
+title: Apps
+sidebarTitle: Overview
+description: Give your tools interactive UIs rendered directly in the conversation.
+icon: grid-2
+mode: center
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+import PrefabPinWarning from '/snippets/prefab-pin-warning.mdx'
+import { PrefabDemoFrame } from '/snippets/prefab-demo-frame.mdx'
+
+
+
+A FastMCP app is a tool that returns an interactive UI instead of text. When the host calls it, the user sees a chart, a table, a form, or a whole dashboard rendered right inside the conversation, with working sort, search, tooltips, and state.
+
+
+
+The dashboard above is a [Prefab](https://prefab.prefect.io) showcase — a taste of what you can deliver from a FastMCP tool. Every card, chart, slider, dialog, and carousel is a Python component. Build a composition like this, add `@mcp.tool(app=True)`, and the host renders it inside the conversation.
+
+Under the hood, FastMCP builds on the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) and uses Prefab to describe UIs in Python.
+
+```bash
+pip install "fastmcp[apps]"
+```
+
+
+
+## Pick your path
+
+Four patterns cover almost everything you'd want to build. Most apps start with Interactive Tools; you only reach for the others when you've hit a specific limit.
+
+### [Interactive Tools](/apps/prefab) — start here
+
+Add `app=True` to a tool and return a Prefab component. Charts, tables, dashboards, and client-side interactivity (toggles, tabs, filtering) all work without any server round-trips.
+
+```python
+@mcp.tool(app=True)
+def team_directory() -> DataTable:
+ return DataTable(columns=[...], rows=employees, search=True)
+```
+
+### [FastMCPApp](/apps/fastmcp-app) — when the UI calls back to the server
+
+Forms that save data, buttons that trigger backend work, search that hits a database. `FastMCPApp` manages the wiring between UI actions and backend tools, with stable tool identifiers that survive server composition.
+
+### [Generative UI](/apps/generative) — when the LLM writes the UI
+
+Register one provider and the model can write Prefab code tailored to the current data and request. The user watches the UI build up as the model generates it.
+
+```python
+mcp.add_provider(GenerativeUI())
+```
+
+### [Custom HTML](/apps/low-level) — when you need full control
+
+Write your own HTML, CSS, and JavaScript. Use a specific framework, drop in a map or 3D viewer, embed video. You're talking to the MCP Apps protocol directly.
+
+## What's next
+
+- **[Quickstart](/apps/quickstart)** — build a working app in a minute
+- **[Examples](/apps/examples)** — complete working servers you can run today
+- **[Providers](/apps/providers/approval)** — ready-made capabilities (approvals, choice pickers, file upload, forms) you add with one line
+- **[Development](/apps/development)** — preview app tools locally with `fastmcp dev apps`
diff --git a/docs/v3/apps/prefab.mdx b/docs/v3/apps/prefab.mdx
new file mode 100644
index 000000000..e6ff7070f
--- /dev/null
+++ b/docs/v3/apps/prefab.mdx
@@ -0,0 +1,297 @@
+---
+title: Interactive Tools
+sidebarTitle: Interactive Tools
+description: Turn your tools into interactive UIs with charts, tables, and dashboards.
+icon: palette
+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'
+
+
+
+
+
+
+
+Believe it or not, that dashboard is a FastMCP tool. The chart has tooltips. The table is sortable. The badges are styled by deal stage. The whole thing is about 40 lines of Python, and the user sees it right inside their conversation instead of a wall of JSON.
+
+The pattern behind every example on this page is the same: add `app=True` to your tool, build a UI with [Prefab](https://prefab.prefect.io) components, and return it as a `PrefabApp`. Prefab has [100+ components](https://prefab.prefect.io/docs/components), from data tables and charts to forms and progress bars. You compose them in Python; the host renders them as a live, interactive application.
+
+## Start with a table
+
+Most tools return data the user wants to explore. A `DataTable` is often the smallest useful upgrade — your data goes from a JSON blob to a searchable, sortable table:
+
+
+
+```python
+from prefab_ui.components import DataTable, DataTableColumn
+from fastmcp import FastMCP
+
+mcp = FastMCP("Directory")
+
+
+@mcp.tool(app=True)
+def team_directory() -> DataTable:
+ """Browse the team directory."""
+ 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"},
+ ]
+
+ return 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,
+ )
+```
+
+That's it. Add `app=True`, return a Prefab component instead of raw dicts. FastMCP handles the rendering, sandboxing, and security. No wrapper class needed for simple cases like this.
+
+## Add charts
+
+When numbers tell a better story as a visual, swap in a chart. The API is the same: pass your data as a list of dicts, tell the chart which keys to plot.
+
+
+
+```python
+@mcp.tool(app=True)
+def quarterly_revenue(year: int) -> BarChart:
+ """Show quarterly revenue as a bar chart."""
+ 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},
+ ]
+
+ return BarChart(
+ data=data,
+ series=[
+ ChartSeries(data_key="revenue", label="Revenue"),
+ ChartSeries(data_key="costs", label="Costs"),
+ ],
+ x_axis="quarter",
+ show_legend=True,
+ )
+```
+
+Each `ChartSeries` plots a different key from the data. `BarChart`, `LineChart`, `AreaChart`, `PieChart`, `RadarChart`, and `RadialChart` all follow the same pattern. Hover over the bars to see tooltips.
+
+
+
+```python
+@mcp.tool(app=True)
+def ticket_breakdown() -> PieChart:
+ """Show open tickets by category."""
+ data = [
+ {"category": "Bug", "count": 42},
+ {"category": "Feature", "count": 28},
+ {"category": "Docs", "count": 15},
+ {"category": "Infra", "count": 10},
+ ]
+
+ return PieChart(
+ data=data,
+ data_key="count",
+ name_key="category",
+ inner_radius=50,
+ show_legend=True,
+ )
+```
+
+See the [Prefab chart docs](https://prefab.prefect.io/docs/components) for stacking, curves, custom colors, and more.
+
+## Compose a dashboard
+
+Tables and charts are useful on their own, but the real power comes from composing them. `Column` stacks children vertically, `Row` lays them out side by side, and `with` blocks establish nesting — the indentation is the layout.
+
+
+
+```python expandable
+@mcp.tool(app=True)
+def sales_dashboard() -> PrefabApp:
+ """Show sales KPIs, trends, and deals."""
+ 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,
+ )
+ Separator()
+ DataTable(
+ columns=[
+ DataTableColumn(key="account", header="Account", sortable=True),
+ DataTableColumn(key="value", header="Value", sortable=True),
+ DataTableColumn(key="stage", header="Stage"),
+ ],
+ rows=rows,
+ )
+
+ return app
+```
+
+Notice how `Badge` components can be placed inside table cells — any Prefab component works as a cell value, so you can put progress bars, icons, or buttons in your tables too.
+
+## Make it reactive
+
+Everything above renders once from the data your Python provides. But interactive tools can also respond to user input in real time, without any server round-trips. Prefab's state system lets components read and write client-side values, so the UI updates instantly as the user interacts with it.
+
+
+
+Try switching regions in the dropdown, and toggling the switch on and off.
+
+```python expandable
+from prefab_ui.rx import Rx
+
+@mcp.tool(app=True)
+def regional_sales() -> PrefabApp:
+ """Sales by region with a live filter."""
+ 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",
+ )
+ with If(Rx("show_target")):
+ Metric(label="Q1 Target", value="$75,000")
+
+ return app
+```
+
+The `state` dict on `PrefabApp` declares initial values. The `Select` writes to the `region` key on every change. A `let` binding picks the matching dataset, and the chart re-renders. The `Switch` toggles a `Metric` on and off through `If(Rx("show_target"))`. All of this happens in the browser — no calls back to your server.
+
+`Rx` is a reactive reference: `Rx("region")` compiles to an expression the renderer evaluates live. It supports arithmetic, comparisons, formatting pipes (`.currency()`, `.percent()`), and ternary conditionals (`.then()`). For the full state system, see the [Prefab state docs](https://prefab.prefect.io/docs/concepts/state) and [expression docs](https://prefab.prefect.io/docs/concepts/expressions).
+
+## Content Security Policy
+
+Interactive tools render in a sandboxed iframe with a strict CSP. If your tool loads external resources — embedding iframes, fetching from APIs, loading scripts — add the required domains:
+
+```python
+from fastmcp.apps import PrefabAppConfig, ResourceCSP
+
+@mcp.tool(app=PrefabAppConfig(
+ csp=ResourceCSP(frame_domains=["https://example.com"]),
+))
+def dashboard_with_embed() -> PrefabApp:
+ ...
+```
+
+`PrefabAppConfig()` with no arguments is equivalent to `app=True`.
+
+## Giving the LLM context
+
+By default, the LLM sees `"[Rendered Prefab UI]"` as the tool result. If the model needs to reason about the data, return a `ToolResult` with a text summary alongside the UI:
+
+```python
+from fastmcp.tools import ToolResult
+
+@mcp.tool(app=True)
+def sales_overview(year: int) -> ToolResult:
+ """Show sales visually, summarize for the model."""
+ data = get_sales_data(year)
+ total = sum(row["revenue"] for row in data)
+
+ with Column(gap=4, css_class="p-6") as view:
+ BarChart(data=data, series=[ChartSeries(data_key="revenue")])
+
+ return ToolResult(
+ content=f"Total revenue for {year}: ${total:,} across {len(data)} quarters",
+ structured_content=view,
+ )
+```
+
+The user sees the chart. The model sees the summary.
+
+## Next steps
+
+- **[FastMCPApp](/apps/fastmcp-app)** — when your UI needs to call backend tools (forms, search, CRUD)
+- **[Generative UI](/apps/generative)** — let the LLM design the UI at runtime
+- **[Custom HTML](/apps/low-level)** — when Prefab isn't enough (maps, 3D, your own framework)
+- **[Examples](/apps/examples)** — complete working servers you can run today
+- **[Development](/apps/development)** — preview your tools locally with `fastmcp dev apps`
+- **[Prefab UI](https://prefab.prefect.io)** — full component reference with 100+ components, theming, and advanced patterns
diff --git a/docs/v3/apps/providers/approval.mdx b/docs/v3/apps/providers/approval.mdx
new file mode 100644
index 000000000..8ac7b8dd1
--- /dev/null
+++ b/docs/v3/apps/providers/approval.mdx
@@ -0,0 +1,80 @@
+---
+title: Approval
+sidebarTitle: Approval
+description: Human-in-the-loop approval gates for agent actions
+icon: shield-check
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+`Approval` adds a human-in-the-loop confirmation step to any server. The LLM presents what it's about to do, the user approves or rejects via buttons, and the decision flows back into the conversation as a message.
+
+
+
+
+
+```python
+from fastmcp import FastMCP
+from fastmcp.apps.approval import Approval
+
+mcp = FastMCP("My Server")
+mcp.add_provider(Approval())
+```
+
+This registers a single tool:
+
+| Tool | Visibility | Purpose |
+|------|-----------|---------|
+| `request_approval` | Model | Shows an approval card, sends the user's decision back as a message |
+
+The LLM calls `request_approval` with a summary (and optional details) whenever it's about to take a significant action. The user sees a card with Approve and Reject buttons. Clicking either sends a message back into the conversation via `SendMessage`, which triggers the LLM's next turn.
+
+The message looks like it came from the user:
+
+```
+"Deploy v3.2 to production" — I selected: Approve
+```
+
+
+Approval is an advisory gate, not an enforcement mechanism. The conversation isn't blocked while the card is open — the user can keep typing, and a determined LLM could proceed without waiting. Think of it as a strong UX signal that encourages confirmation, not a security boundary. For hard enforcement, implement approval logic server-side in your tool implementations.
+
+
+## Configuration
+
+The constructor sets defaults; the LLM can override all of these per-call via tool arguments.
+
+```python
+Approval(
+ name="Approval", # App name
+ title="Approval Required", # Card heading
+ approve_text="Approve", # Approve button label
+ reject_text="Reject", # Reject button label
+ approve_variant="default", # "default", "destructive", "success", "info"
+ reject_variant="outline", # same options plus "outline"
+)
+```
+
+The LLM can customize each invocation:
+
+```python
+request_approval(
+ summary="Delete 47 files from /tmp",
+ details="This cannot be undone.",
+ title="Destructive Action",
+ approve_text="Delete",
+ approve_variant="destructive",
+ reject_text="Keep files",
+)
+```
+
+## How it works
+
+When the user clicks a button, two things happen:
+
+1. `SendMessage` pushes the decision into the conversation as a user message
+2. `SetState("decided", True)` replaces the buttons with "Response sent."
+
+The tool description instructs the LLM to stop and wait for the "I selected:" message before proceeding. If approved, it continues. If rejected, it acknowledges and asks how to proceed.
diff --git a/docs/v3/apps/providers/choice.mdx b/docs/v3/apps/providers/choice.mdx
new file mode 100644
index 000000000..c29c1b2bc
--- /dev/null
+++ b/docs/v3/apps/providers/choice.mdx
@@ -0,0 +1,72 @@
+---
+title: Choice
+sidebarTitle: Choice
+description: Present clickable options instead of free-text responses
+icon: list-check
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+`Choice` lets the LLM present a set of options as clickable buttons instead of asking the user to type a response. The selection flows back into the conversation as a message, giving the LLM clean structured input.
+
+
+
+
+
+```python
+from fastmcp import FastMCP
+from fastmcp.apps.choice import Choice
+
+mcp = FastMCP("My Server")
+mcp.add_provider(Choice())
+```
+
+This registers a single tool:
+
+| Tool | Visibility | Purpose |
+|------|-----------|---------|
+| `choose` | Model | Shows a card with clickable options, sends the selection back as a message |
+
+The LLM calls `choose` with a prompt and a list of options. The user sees a card with one button per option. Clicking one sends a message back into the conversation:
+
+```
+"Which deployment strategy?" — I selected: Blue-green
+```
+
+
+This is an advisory interaction, not an enforcement mechanism. The conversation isn't blocked while the card is open — the user can keep typing, and the LLM could proceed without waiting. The tool description instructs the LLM to stop and wait for the "I selected:" response, but for hard enforcement, implement selection logic server-side.
+
+
+## Configuration
+
+The constructor sets defaults; the LLM can override `title` per-call.
+
+```python
+Choice(
+ name="Choice", # App name
+ title="Choose an Option", # Default card heading
+ variant="outline", # Button style for all options
+)
+```
+
+The LLM provides the options per-call:
+
+```python
+choose(
+ prompt="What should we have for lunch?",
+ options=["Pizza", "Tacos", "Ramen", "Salad"],
+ title="The Important Questions",
+)
+```
+
+## How it works
+
+Each option renders as a full-width button in a vertical stack. When the user clicks one:
+
+1. `SendMessage` pushes the selection into the conversation as a user message
+2. `SetState("decided", True)` replaces the buttons with "Response sent."
+
+The tool description instructs the LLM to stop and wait for the "I selected:" message before proceeding with whatever the user chose.
diff --git a/docs/v3/apps/providers/file-upload.mdx b/docs/v3/apps/providers/file-upload.mdx
new file mode 100644
index 000000000..b9709d946
--- /dev/null
+++ b/docs/v3/apps/providers/file-upload.mdx
@@ -0,0 +1,129 @@
+---
+title: File Upload
+sidebarTitle: File Upload
+description: Drag-and-drop file upload for any MCP server
+icon: upload
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+`FileUpload` adds drag-and-drop file upload to any server. Users upload files through an interactive UI, bypassing the LLM context window entirely. The LLM can then list and read uploaded files through model-visible tools.
+
+
+
+
+
+```python
+from fastmcp import FastMCP
+from fastmcp.apps.file_upload import FileUpload
+
+mcp = FastMCP("My Server")
+mcp.add_provider(FileUpload())
+```
+
+This registers four tools:
+
+| Tool | Visibility | Purpose |
+|------|-----------|---------|
+| `file_manager` | Model | Opens the drag-and-drop upload UI |
+| `store_files` | App only | Called by the UI when the user clicks Upload |
+| `list_files` | Model | Returns metadata for all uploaded files |
+| `read_file` | Model | Returns a file's contents by name |
+
+The LLM sees `file_manager`, `list_files`, and `read_file`. It calls `file_manager` to show the upload interface, then uses `list_files` and `read_file` to work with whatever the user uploaded. `store_files` is app-only — the UI calls it directly and the LLM never needs to know about it.
+
+## Configuration
+
+```python
+FileUpload(
+ name="Files", # App name (used in tool routing)
+ max_file_size=10 * 1024 * 1024, # 10 MB default, enforced server-side
+ title="File Upload", # Heading shown in the UI
+ description="Drop files to...", # Description text below the heading
+ drop_label="Drop files here", # Label inside the drop zone
+)
+```
+
+The `max_file_size` limit is enforced both in the UI (the DropZone rejects oversized files) and on the server (the `store_files` tool validates before calling `on_store`).
+
+## Storage scoping
+
+By default, files are stored in memory and scoped by MCP session ID. Each session gets its own isolated file store — files uploaded in one conversation aren't visible in another.
+
+This works with **stdio**, **SSE**, and **stateful HTTP** transports, where sessions persist across requests.
+
+
+In **stateless HTTP** mode, each request creates a new session object with a new ID. Files stored during one request (e.g. the UI upload) will be invisible to the next request (e.g. the LLM calling `list_files`). You **must** override `_get_scope_key` to use a stable identifier like a user ID from your auth token.
+
+
+For stateless deployments, override `_get_scope_key` to return a stable identifier. For example, to scope files by authenticated user:
+
+```python
+from fastmcp.apps.file_upload import FileUpload
+
+class UserScopedUpload(FileUpload):
+ def _get_scope_key(self, ctx):
+ return ctx.access_token["sub"]
+```
+
+For process-wide shared storage (all users see all files):
+
+```python
+class SharedUpload(FileUpload):
+ def _get_scope_key(self, ctx):
+ return "__shared__"
+```
+
+## Custom storage
+
+The default implementation stores files in memory for the lifetime of the server process. For persistent storage, subclass `FileUpload` and override three methods. Each receives the current `Context`, giving you access to session IDs, auth tokens, and request metadata for partitioning and authorization.
+
+```python
+import base64
+
+from fastmcp.apps.file_upload import FileUpload
+
+class S3Upload(FileUpload):
+ def on_store(self, files, ctx):
+ user_id = ctx.access_token["sub"]
+ for f in files:
+ s3.put_object(
+ Bucket="uploads",
+ Key=f"{user_id}/{f['name']}",
+ Body=base64.b64decode(f["data"]),
+ )
+ return self.on_list(ctx)
+
+ def on_list(self, ctx):
+ user_id = ctx.access_token["sub"]
+ objects = s3.list_objects(Bucket="uploads", Prefix=f"{user_id}/")
+ return [
+ {
+ "name": obj["Key"].split("/", 1)[1],
+ "type": "application/octet-stream",
+ "size": obj["Size"],
+ "size_display": f"{obj['Size']} B",
+ "uploaded_at": obj["LastModified"].isoformat(),
+ }
+ for obj in objects.get("Contents", [])
+ ]
+
+ def on_read(self, name, ctx):
+ user_id = ctx.access_token["sub"]
+ obj = s3.get_object(Bucket="uploads", Key=f"{user_id}/{name}")
+ content = obj["Body"].read()
+ return {
+ "name": name,
+ "size": obj["ContentLength"],
+ "type": obj["ContentType"],
+ "uploaded_at": obj["LastModified"].isoformat(),
+ "content": content.decode("utf-8"),
+ }
+```
+
+Each file dict passed to `on_store` contains `name`, `size`, `type`, and `data` (base64-encoded content). The return value from `on_store` and `on_list` should be a list of summary dicts with `name`, `type`, `size`, `size_display`, and `uploaded_at` fields — these populate the file list in the UI.
+
+`on_read` returns a dict with file metadata and either `content` (decoded text) or `content_base64` (a base64 preview for binary files).
diff --git a/docs/v3/apps/providers/form.mdx b/docs/v3/apps/providers/form.mdx
new file mode 100644
index 000000000..e61dc0ce0
--- /dev/null
+++ b/docs/v3/apps/providers/form.mdx
@@ -0,0 +1,105 @@
+---
+title: Form Input
+sidebarTitle: Form Input
+description: Collect structured data from users via Pydantic models
+icon: rectangle-list
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+`FormInput` generates a validated form from a Pydantic model. The user fills it out, and the submission is validated against the model before being returned. Structured elicitation that can't be hallucinated.
+
+
+
+
+
+```python
+from typing import Literal
+
+from pydantic import BaseModel, Field
+from fastmcp import FastMCP
+from fastmcp.apps.form import FormInput
+
+class BugReport(BaseModel):
+ title: str = Field(description="Brief summary")
+ severity: Literal["low", "medium", "high", "critical"]
+ description: str = Field(
+ description="Detailed description",
+ json_schema_extra={"ui": {"type": "textarea"}},
+ )
+
+mcp = FastMCP("My Server")
+mcp.add_provider(FormInput(model=BugReport))
+```
+
+This registers two tools:
+
+| Tool | Visibility | Purpose |
+|------|-----------|---------|
+| `collect_bugreport` | Model | Opens the form UI |
+| `submit_form` | App only | Validates and processes the submission |
+
+The tool name is derived from the model class name, lowercased: `collect_{modelname}`. So `BugReport` becomes `collect_bugreport`, `ShippingAddress` becomes `collect_shippingaddress`. Use `tool_name` to override if needed. The LLM calls it with a prompt explaining what it needs, and the user gets a form with fields matching the model.
+
+## Field mapping
+
+`FormInput` uses Prefab's `Form.from_model()`, which maps Pydantic types to form components:
+
+| Python type | Form component |
+|------------|---------------|
+| `str` | Text input |
+| `int`, `float` | Number input |
+| `bool` | Checkbox |
+| `datetime.date` | Date picker |
+| `Literal[...]` | Select dropdown |
+| `SecretStr` | Password input |
+
+Use `Field()` metadata to control labels (`title`), placeholders (`description`), and validation (`min_length`, `max_length`, `ge`, `le`). Use `json_schema_extra={"ui": {"type": "textarea"}}` for multiline text.
+
+## Callback
+
+By default, the validated model is returned as JSON. Provide an `on_submit` callback to process the data server-side:
+
+```python
+def save_report(report: BugReport) -> str:
+ db.insert(report.model_dump())
+ return f"Bug #{db.last_id} filed: {report.title}"
+
+mcp.add_provider(FormInput(model=BugReport, on_submit=save_report))
+```
+
+The callback receives a validated model instance and returns a string that becomes the tool result.
+
+## Configuration
+
+```python
+FormInput(
+ model=BugReport, # Required: the Pydantic model
+ name="BugTracker", # App name (default: model name)
+ title="File a Bug", # Card heading (default: model name)
+ tool_name="file_bug", # Tool name (default: collect_{model})
+ submit_text="Submit Report", # Button label (default: "Submit")
+ on_submit=save_report, # Optional callback
+ send_message=True, # Push result as a chat message
+)
+```
+
+Set `send_message=True` to push the result back into the conversation via `SendMessage`, triggering the LLM's next turn. Without it, the result is just the tool return value.
+
+## Multiple forms
+
+Add multiple providers for different models — each gets its own tool:
+
+```python
+mcp = FastMCP(
+ "My Server",
+ providers=[
+ FormInput(model=ShippingAddress),
+ FormInput(model=BugReport),
+ FormInput(model=ContactInfo),
+ ],
+)
+```
diff --git a/docs/v3/apps/quickstart.mdx b/docs/v3/apps/quickstart.mdx
new file mode 100644
index 000000000..2221b9de9
--- /dev/null
+++ b/docs/v3/apps/quickstart.mdx
@@ -0,0 +1,197 @@
+---
+title: Quickstart
+sidebarTitle: Quickstart
+description: Build your first FastMCP app in under a minute.
+icon: rocket
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+import { PrefabDemoFrame } from '/snippets/prefab-demo-frame.mdx'
+
+
+
+By the end of this page, you'll have a working tool that returns this:
+
+
+
+A pie chart the user can hover, a table they can sort and search — and a single Python tool.
+
+## Install
+
+```bash
+pip install "fastmcp[apps]"
+```
+
+The `apps` extra pulls in [Prefab](https://prefab.prefect.io), the Python component library used to build app UIs.
+
+## Write the tool
+
+Create `server.py`. The interesting parts: `app=True` tells FastMCP this tool renders a UI, and `with PrefabApp() as app:` is the canonical pattern for composing one.
+
+```python server.py expandable
+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
+from fastmcp import FastMCP
+
+mcp = FastMCP("My First App")
+
+
+@mcp.tool(app=True)
+def team_directory() -> PrefabApp:
+ """Browse the team directory."""
+ 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,
+ )
+
+ return app
+```
+
+The Prefab code reads top-to-bottom. `PrefabApp()` is the root; everything inside its `with` block becomes the UI. `Column` stacks children vertically, `Grid` lays them out in columns. `DataTable` takes rows and column definitions and gives you sort and search for free.
+
+`app=True` does the rest: it sets up the renderer resource, the content security policy, and the metadata that tells the host "this tool returns a UI." The host loads the result in a sandboxed iframe where the user can interact with it — all client-side, no round-trips.
+
+## Preview it
+
+FastMCP ships a dev server that renders your app tools in a browser, no MCP host needed:
+
+```bash
+fastmcp dev apps server.py
+```
+
+Open `http://localhost:8080`, pick `team_directory`, and try sorting columns and searching.
+
+
+
+
+
+## Make it reactive
+
+The UI above renders once from your Python. Prefab apps can also respond to user input live, without any server round-trips. The key concept is **state**: a client-side key-value store that components read from and write to.
+
+Click a row in the demo below to see a detail card appear:
+
+
+
+Add a few imports, give each member a couple more fields, wire up a click handler, and render a detail card when something's selected:
+
+```python expandable server.py
+from collections import Counter
+
+from prefab_ui.actions import SetState
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import (
+ Badge, Card, CardContent, CardHeader, Column, DataTable, DataTableColumn,
+ Grid, H3, Row, Small, Text,
+)
+from prefab_ui.components.charts import PieChart
+from prefab_ui.components.control_flow import If
+from prefab_ui.rx import Rx, STATE
+from fastmcp import FastMCP
+
+mcp = FastMCP("My First App")
+
+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},
+ # ... more members ...
+]
+
+OFFICE_COUNTS = [
+ {"office": o, "count": c}
+ for o, c in Counter(m["office"] for m in MEMBERS).items()
+]
+
+
+@mcp.tool(app=True)
+def team_directory() -> PrefabApp:
+ """Browse the team directory."""
+ 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"))
+
+ return app
+```
+
+Three new ideas do all the work:
+
+- **`on_row_click=SetState("selected", Rx("$event"))`** — clicking a row writes its data into the `selected` state key. `$event` is the clicked row dict.
+- **`Rx("selected.name")`** — a reactive reference. It doesn't hold a Python value; it compiles to a browser-side expression that re-evaluates whenever `selected` changes, so `Text(Rx("selected.name"))` always shows the latest clicked name.
+- **`If(STATE.selected)`** — conditionally renders its body. Before any click, `selected` is `None` and the card stays hidden.
+
+The `state={"selected": None}` dict on `PrefabApp` sets the initial value. Everything else happens in the browser — no round-trips to your server when the user clicks.
+
+## Where to go next
+
+You've built a tool that returns an interactive, reactive UI. This pattern covers a huge range of use cases: build a visualization, return it, and the user gets it rendered right in the conversation.
+
+- **[Interactive Tools](/apps/prefab)** — charts, tables, dashboards, reactive state, with live demos
+- **[FastMCPApp](/apps/fastmcp-app)** — when the UI needs to call back to your server (forms, search, CRUD)
+- **[Examples](/apps/examples)** — complete working servers you can run today
diff --git a/docs/v3/changelog.mdx b/docs/v3/changelog.mdx
new file mode 100644
index 000000000..9ee438d53
--- /dev/null
+++ b/docs/v3/changelog.mdx
@@ -0,0 +1,3759 @@
+---
+title: "Changelog"
+icon: "list-check"
+rss: true
+tag: NEW
+---
+
+
+
+**[v3.4.4: Host in Translation](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.4)**
+
+FastMCP 3.4.4 restores HTTP deployment compatibility after the 3.4.3 Host/Origin guard changed default behavior for existing ASGI, serverless, and reverse-proxy deployments. The guard implementation remains available for deployments that opt in with explicit trusted hosts and origins, while 3.x returns to accepting traffic that worked before the patch. This release also adds Hugging Face OAuth provider support, with docs and examples for public and private apps, PKCE, Dynamic Client Registration, and CIMD.
+
+### Enhancements ✨
+* Hugging Face Auth Integration by [@evalstate](https://github.com/evalstate) in [#4385](https://github.com/PrefectHQ/fastmcp/pull/4385)
+### Fixes 🐞
+* Relax host origin guard defaults by [@jlowin](https://github.com/jlowin) in [#4439](https://github.com/PrefectHQ/fastmcp/pull/4439)
+* Restore HTTP host guard compatibility by [@jlowin](https://github.com/jlowin) in [#4472](https://github.com/PrefectHQ/fastmcp/pull/4472)
+
+## New Contributors
+* @evalstate made their first contribution in [#4385](https://github.com/PrefectHQ/fastmcp/pull/4385)
+
+**Full Changelog**: [v3.4.3...v3.4.4](https://github.com/PrefectHQ/fastmcp/compare/v3.4.3...v3.4.4)
+
+
+
+
+
+**[v3.4.3: The Fast and the Secure-ious](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.3)**
+
+FastMCP 3.4.3 closes out a month of SSRF and OAuth hardening: NAT64, 6to4, Teredo, and ISATAP transition addresses can no longer smuggle private IPv4 targets past the SSRF allow-list, Streamable HTTP now validates Host and Origin before session handling to block DNS rebinding against localhost-bound servers, and OAuth redirect validation rejects unsafe schemes and unregistered DCR redirect URIs. Alongside the security work, this release also fixes proxy session teardown races, discriminator-tag handling in JSON schema conversion, and several smaller reliability issues.
+
+### Enhancements ✨
+* Dedupe discriminator-required helper across schema converters by [@jlowin](https://github.com/jlowin) in [#4362](https://github.com/PrefectHQ/fastmcp/pull/4362)
+* Add real Monty sandbox e2e coverage for CodeMode call_tool by [@AlexlaGuardia](https://github.com/AlexlaGuardia) in [#4274](https://github.com/PrefectHQ/fastmcp/pull/4274)
+* Switch prettier hook to rbubley/mirrors-prettier by [@jlowin](https://github.com/jlowin) in [#4366](https://github.com/PrefectHQ/fastmcp/pull/4366)
+* feat(remote): add --verify flag for TLS certificate verification by [@jlowin](https://github.com/jlowin) in [#4369](https://github.com/PrefectHQ/fastmcp/pull/4369)
+### Security 🔒
+* fix(deps): clear Dependabot security alerts via lockfile bumps by [@jlowin](https://github.com/jlowin) in [#4393](https://github.com/PrefectHQ/fastmcp/pull/4393)
+* Clarify resource path parameter safety by [@jlowin](https://github.com/jlowin) in [#4398](https://github.com/PrefectHQ/fastmcp/pull/4398)
+* Fix dev apps launch escaping by [@jlowin](https://github.com/jlowin) in [#4399](https://github.com/PrefectHQ/fastmcp/pull/4399)
+* Block NAT64 SSRF bypass by [@jlowin](https://github.com/jlowin) in [#4400](https://github.com/PrefectHQ/fastmcp/pull/4400)
+* [codex] Fix event store replay isolation by [@jlowin](https://github.com/jlowin) in [#4402](https://github.com/PrefectHQ/fastmcp/pull/4402)
+* Fix DCR redirect URI validation by [@jlowin](https://github.com/jlowin) in [#4408](https://github.com/PrefectHQ/fastmcp/pull/4408)
+* Protect streamable HTTP from DNS rebinding by [@jlowin](https://github.com/jlowin) in [#4405](https://github.com/PrefectHQ/fastmcp/pull/4405)
+* Block unsafe OAuth redirect schemes by [@jlowin](https://github.com/jlowin) in [#4419](https://github.com/PrefectHQ/fastmcp/pull/4419)
+* Block IPv6 transition SSRF bypasses by [@jlowin](https://github.com/jlowin) in [#4426](https://github.com/PrefectHQ/fastmcp/pull/4426)
+### Fixes 🐞
+* fix: caching middleware TypeError on cache miss due to mismatched call_next parameter by [@gmenziesint](https://github.com/gmenziesint) in [#4301](https://github.com/PrefectHQ/fastmcp/pull/4301)
+* Fix: async rate limiting middleware get_client_id callbacks by [@Chotom](https://github.com/Chotom) in [#4319](https://github.com/PrefectHQ/fastmcp/pull/4319)
+* Recognize all GitHub issue-link forms in require-issue-link workflow by [@jlowin](https://github.com/jlowin) in [#4359](https://github.com/PrefectHQ/fastmcp/pull/4359)
+* fix: preserve required discriminator tags by [@he-yufeng](https://github.com/he-yufeng) in [#4297](https://github.com/PrefectHQ/fastmcp/pull/4297)
+* fix(proxy): shield stateful proxy disconnect during session teardown by [@jlowin](https://github.com/jlowin) in [#4363](https://github.com/PrefectHQ/fastmcp/pull/4363)
+* fix(fs): isolate same-named package imports across providers by [@jlowin](https://github.com/jlowin) in [#4361](https://github.com/PrefectHQ/fastmcp/pull/4361)
+* fix: StatefulProxyClient.clear() no longer causes KeyError on session teardown by [@tcconnally](https://github.com/tcconnally) in [#4328](https://github.com/PrefectHQ/fastmcp/pull/4328)
+* fix: guard recursive refs in json_schema_to_type by [@Epochex](https://github.com/Epochex) in [#4312](https://github.com/PrefectHQ/fastmcp/pull/4312)
+* Forward IdP auth errors to MCP client instead of showing HTML error page by [@bobbyjames839](https://github.com/bobbyjames839) in [#4293](https://github.com/PrefectHQ/fastmcp/pull/4293)
+* fix(resources): round-trip path values with reserved characters in URI templates by [@jlowin](https://github.com/jlowin) in [#4368](https://github.com/PrefectHQ/fastmcp/pull/4368)
+* fix: bracket IPv6 hosts in server startup log URL by [@jlowin](https://github.com/jlowin) in [#4372](https://github.com/PrefectHQ/fastmcp/pull/4372)
+* fix: bound default OIDC discovery timeout and expose it on provider wrappers by [@jlowin](https://github.com/jlowin) in [#4374](https://github.com/PrefectHQ/fastmcp/pull/4374)
+* fix: validate task tool arguments against declared types by [@jlowin](https://github.com/jlowin) in [#4373](https://github.com/PrefectHQ/fastmcp/pull/4373)
+* fix(tools): honor serialize_by_alias in tool result serialization by [@jlowin](https://github.com/jlowin) in [#4391](https://github.com/PrefectHQ/fastmcp/pull/4391)
+* Fix/cimd flow issue by [@twjackysu](https://github.com/twjackysu) in [#4206](https://github.com/PrefectHQ/fastmcp/pull/4206)
+* Reject empty env var keys by [@CodingFeng101](https://github.com/CodingFeng101) in [#4410](https://github.com/PrefectHQ/fastmcp/pull/4410)
+* fix: correct replace_type docstring parameter descriptions by [@hiSandog](https://github.com/hiSandog) in [#4375](https://github.com/PrefectHQ/fastmcp/pull/4375)
+* Fix ty 0.0.55 diagnostics and prefab-ui protocol version drift by [@jlowin](https://github.com/jlowin) in [#4428](https://github.com/PrefectHQ/fastmcp/pull/4428)
+* [codex] Fix OpenAPI resource template requests by [@jlowin](https://github.com/jlowin) in [#4407](https://github.com/PrefectHQ/fastmcp/pull/4407)
+### Docs 📚
+* fix: RST docstrings in fastmcp.types render raw on gofastmcp.com by [@jlowin](https://github.com/jlowin) in [#4367](https://github.com/PrefectHQ/fastmcp/pull/4367)
+* docs: fix 5 broken internal links (auth & providers pages) by [@Michael-WhiteCapData](https://github.com/Michael-WhiteCapData) in [#4344](https://github.com/PrefectHQ/fastmcp/pull/4344)
+* docs: add audit/event-record recipe for tool-call middleware by [@AlexlaGuardia](https://github.com/AlexlaGuardia) in [#4345](https://github.com/PrefectHQ/fastmcp/pull/4345)
+### Dependencies 📦
+* chore(deps): bump actions/checkout from 6 to 7 by [@dependabot](https://github.com/apps/dependabot) in [#4343](https://github.com/PrefectHQ/fastmcp/pull/4343)
+* chore(deps): bump joserfc from 1.6.5 to 1.6.7 in the uv group across 1 directory by [@dependabot](https://github.com/apps/dependabot) in [#4394](https://github.com/PrefectHQ/fastmcp/pull/4394)
+* chore(deps): bump joserfc from 1.6.7 to 1.6.8 in the uv group across 1 directory by [@dependabot](https://github.com/apps/dependabot) in [#4429](https://github.com/PrefectHQ/fastmcp/pull/4429)
+### Other Changes 🦾
+* Raise fastmcp.ValidationError for invalid tool arguments by [@jlowin](https://github.com/jlowin) in [#4392](https://github.com/PrefectHQ/fastmcp/pull/4392)
+* Fix versioned auth middleware checks by [@jlowin](https://github.com/jlowin) in [#4401](https://github.com/PrefectHQ/fastmcp/pull/4401)
+
+## New Contributors
+* @gmenziesint made their first contribution in [#4301](https://github.com/PrefectHQ/fastmcp/pull/4301)
+* @Chotom made their first contribution in [#4319](https://github.com/PrefectHQ/fastmcp/pull/4319)
+* @he-yufeng made their first contribution in [#4297](https://github.com/PrefectHQ/fastmcp/pull/4297)
+* @AlexlaGuardia made their first contribution in [#4274](https://github.com/PrefectHQ/fastmcp/pull/4274)
+* @tcconnally made their first contribution in [#4328](https://github.com/PrefectHQ/fastmcp/pull/4328)
+* @Epochex made their first contribution in [#4312](https://github.com/PrefectHQ/fastmcp/pull/4312)
+* @Michael-WhiteCapData made their first contribution in [#4344](https://github.com/PrefectHQ/fastmcp/pull/4344)
+* @bobbyjames839 made their first contribution in [#4293](https://github.com/PrefectHQ/fastmcp/pull/4293)
+* @twjackysu made their first contribution in [#4206](https://github.com/PrefectHQ/fastmcp/pull/4206)
+* @CodingFeng101 made their first contribution in [#4410](https://github.com/PrefectHQ/fastmcp/pull/4410)
+* @hiSandog made their first contribution in [#4375](https://github.com/PrefectHQ/fastmcp/pull/4375)
+
+**Full Changelog**: [v3.4.2...v3.4.3](https://github.com/PrefectHQ/fastmcp/compare/v3.4.2...v3.4.3)
+
+
+
+
+
+**[v3.4.2: Heads Up](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.2)**
+
+FastMCP 3.4.2 restores JWT compatibility for providers that include private, non-critical JWS header parameters. Tokens from providers like Clerk can carry header metadata such as `cat` without being rejected before signature and claim validation, while unsupported critical headers are still rejected.
+
+### Fixes 🐞
+* Allow private JWT headers by [@jlowin](https://github.com/jlowin) in [#4290](https://github.com/PrefectHQ/fastmcp/pull/4290)
+### Docs 📚
+* Docs: add v3.4.1 changelog entries by [@jlowin](https://github.com/jlowin) in [#4289](https://github.com/PrefectHQ/fastmcp/pull/4289)
+
+**Full Changelog**: [v3.4.1...v3.4.2](https://github.com/PrefectHQ/fastmcp/compare/v3.4.1...v3.4.2)
+
+
+
+
+
+**[v3.4.1: Floor It](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.1)**
+
+FastMCP 3.4.1 floors Starlette at `>=1.0.1` so installs can no longer resolve to a version affected by CVE-2026-48710, which was previously only constrained transitively through `mcp`. It also makes OAuthProxy log refresh-token cache misses instead of failing silently.
+
+### Enhancements ✨
+* Log refresh-token misses in OAuthProxy instead of failing silently by [@jlowin](https://github.com/jlowin) in [#4276](https://github.com/PrefectHQ/fastmcp/pull/4276)
+### Security 🔒
+* Add explicit starlette>=1.0.1 floor (CVE-2026-48710) by [@jlowin](https://github.com/jlowin) in [#4286](https://github.com/PrefectHQ/fastmcp/pull/4286)
+### Docs 📚
+* Document --notes-start-tag in release instructions by [@jlowin](https://github.com/jlowin) in [#4275](https://github.com/PrefectHQ/fastmcp/pull/4275)
+
+**Full Changelog**: [v3.4.0...v3.4.1](https://github.com/PrefectHQ/fastmcp/compare/v3.4.0...v3.4.1)
+
+
+
+
+
+**[v3.4.0: Remote Control](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.4.0)**
+
+FastMCP 3.4 is about reaching servers that live somewhere else. The headline is `fastmcp-remote`, a standalone bridge that connects stdio-only MCP hosts to servers hosted over HTTP. Around it, the proxy layer those connections depend on is hardened: a proxy now forwards `initialize` upstream and fails loudly when the backend is missing or misconfigured, instead of reporting a connected-but-empty proxy. And FastMCP-issued access tokens can now outlive short-lived upstream tokens, so authenticated sessions survive the long idle periods remote clients are prone to.
+
+### New Features 🎉
+* Add fastmcp-remote bridge package by [@jlowin](https://github.com/jlowin) in [#4208](https://github.com/PrefectHQ/fastmcp/pull/4208)
+### Breaking Changes ⚠️
+* Forward proxy initialize as bridge behavior by [@jlowin](https://github.com/jlowin) in [#4228](https://github.com/PrefectHQ/fastmcp/pull/4228)
+### Enhancements ✨
+* ci: require external PRs to link a tracked issue by [@strawgate](https://github.com/strawgate) in [#4173](https://github.com/PrefectHQ/fastmcp/pull/4173)
+* feat: new options --host and --no-log-panel | --log-panel to cli dev apps by [@itaru2622](https://github.com/itaru2622) in [#4123](https://github.com/PrefectHQ/fastmcp/pull/4123)
+* Add valid_scopes and extra_authorize_params to WorkOSProvider by [@tiagoskaneta](https://github.com/tiagoskaneta) in [#4135](https://github.com/PrefectHQ/fastmcp/pull/4135)
+* Add token_expiry_threshold_seconds for proactive token refresh by [@mohankumarelec](https://github.com/mohankumarelec) in [#4142](https://github.com/PrefectHQ/fastmcp/pull/4142)
+* Add review-issue skill for triaging gated external contributions by [@jlowin](https://github.com/jlowin) in [#4212](https://github.com/PrefectHQ/fastmcp/pull/4212)
+* Add contract gate to review-issue skill by [@jlowin](https://github.com/jlowin) in [#4214](https://github.com/PrefectHQ/fastmcp/pull/4214)
+* Let ToolResult return an error result via is_error by [@jlowin](https://github.com/jlowin) in [#4217](https://github.com/PrefectHQ/fastmcp/pull/4217)
+* Update published docs after PyPI release by [@jlowin](https://github.com/jlowin) in [#4211](https://github.com/PrefectHQ/fastmcp/pull/4211)
+* Allow pre-bound HTTP sockets by [@jlowin](https://github.com/jlowin) in [#4222](https://github.com/PrefectHQ/fastmcp/pull/4222)
+* Add targeted coverage tests by [@strawgate](https://github.com/strawgate) in [#4230](https://github.com/PrefectHQ/fastmcp/pull/4230)
+* Upgrade ty to 0.0.39 by [@jlowin](https://github.com/jlowin) in [#4225](https://github.com/PrefectHQ/fastmcp/pull/4225)
+* Decouple FastMCP access token lifetime from upstream expires_in by [@jlowin](https://github.com/jlowin) in [#4254](https://github.com/PrefectHQ/fastmcp/pull/4254)
+### Security 🔒
+* feat(code-mode): default sandbox limits and per-execution tool-call cap by [@strawgate](https://github.com/strawgate) in [#4170](https://github.com/PrefectHQ/fastmcp/pull/4170)
+* Security: Fix 3 findings in GitHub Actions workflows by [@jpr5](https://github.com/jpr5) in [#4183](https://github.com/PrefectHQ/fastmcp/pull/4183)
+* Add outbound comment guardrails by [@jlowin](https://github.com/jlowin) in [#4196](https://github.com/PrefectHQ/fastmcp/pull/4196)
+* Add uv dependency cooldown by [@jlowin](https://github.com/jlowin) in [#4213](https://github.com/PrefectHQ/fastmcp/pull/4213)
+### Fixes 🐞
+* fix: VersionSpec eq matching normalizes versions and selects deterministically by [@strawgate](https://github.com/strawgate) in [#4058](https://github.com/PrefectHQ/fastmcp/pull/4058)
+* fix(tests): hoist azure-identity import out of the OBO test timeout window by [@strawgate](https://github.com/strawgate) in [#4176](https://github.com/PrefectHQ/fastmcp/pull/4176)
+* fix(auth): disambiguate auth-denied vs missing component messages by [@strawgate](https://github.com/strawgate) in [#4165](https://github.com/PrefectHQ/fastmcp/pull/4165)
+* fix: preserve annotations, meta, title, icons when creating resources from templates by [@strawgate](https://github.com/strawgate) in [#4061](https://github.com/PrefectHQ/fastmcp/pull/4061)
+* fix: add OTEL spans to sampling step and tool execution by [@strawgate](https://github.com/strawgate) in [#4059](https://github.com/PrefectHQ/fastmcp/pull/4059)
+* fix(config): read MCP config files as UTF-8 by [@pragnyanramtha](https://github.com/pragnyanramtha) in [#4164](https://github.com/PrefectHQ/fastmcp/pull/4164)
+* fix(schema): preserve root metadata on fallback by [@yuyua9](https://github.com/yuyua9) in [#4178](https://github.com/PrefectHQ/fastmcp/pull/4178)
+* fix(proxy): restore _current_server in _restore_request_context by [@strawgate](https://github.com/strawgate) in [#4168](https://github.com/PrefectHQ/fastmcp/pull/4168)
+* fix(auth): add /.well-known/openid-configuration alias for OAuth server metadata by [@shigechika](https://github.com/shigechika) in [#4167](https://github.com/PrefectHQ/fastmcp/pull/4167)
+* fix(code-mode): cancel Monty sandbox future on task cancellation by [@strawgate](https://github.com/strawgate) in [#4169](https://github.com/PrefectHQ/fastmcp/pull/4169)
+* fix(auth): unprefix Azure scopes echoed back to MCP clients by [@rgillinlz](https://github.com/rgillinlz) in [#4130](https://github.com/PrefectHQ/fastmcp/pull/4130)
+* fix(cli): forward stateless flag in uv run path by [@yuyua9](https://github.com/yuyua9) in [#4177](https://github.com/PrefectHQ/fastmcp/pull/4177)
+* fix(ci): scope minimize-reviews concurrency by event name by [@strawgate](https://github.com/strawgate) in [#4174](https://github.com/PrefectHQ/fastmcp/pull/4174)
+* Fix docs app demo iframe assets by [@jlowin](https://github.com/jlowin) in [#4194](https://github.com/PrefectHQ/fastmcp/pull/4194)
+* Guard require-issue-link check job to pull_request_target events by [@jlowin](https://github.com/jlowin) in [#4209](https://github.com/PrefectHQ/fastmcp/pull/4209)
+* Migrate auth JWTs to joserfc by [@jlowin](https://github.com/jlowin) in [#4221](https://github.com/PrefectHQ/fastmcp/pull/4221)
+* Skip published docs update for prereleases by [@jlowin](https://github.com/jlowin) in [#4224](https://github.com/PrefectHQ/fastmcp/pull/4224)
+* Surface proxy upstream failures by [@jlowin](https://github.com/jlowin) in [#4227](https://github.com/PrefectHQ/fastmcp/pull/4227)
+* Close upstream OAuth clients by [@jlowin](https://github.com/jlowin) in [#4248](https://github.com/PrefectHQ/fastmcp/pull/4248)
+* Fix GitHub MCP resource integration test by [@jlowin](https://github.com/jlowin) in [#4253](https://github.com/PrefectHQ/fastmcp/pull/4253)
+* Fix resource templates with query params on proxied servers by [@rene84](https://github.com/rene84) in [#4251](https://github.com/PrefectHQ/fastmcp/pull/4251)
+### Docs 📚
+* Document pip upgrade recovery for the fastmcp-slim package split by [@jlowin](https://github.com/jlowin) in [#4215](https://github.com/PrefectHQ/fastmcp/pull/4215)
+* Move pip upgrade recovery into a Troubleshooting section by [@jlowin](https://github.com/jlowin) in [#4219](https://github.com/PrefectHQ/fastmcp/pull/4219)
+* Restore Horizon docs banner by [@jlowin](https://github.com/jlowin) in [#4240](https://github.com/PrefectHQ/fastmcp/pull/4240)
+* fix: Trendshift link and badge in README.md by [@bhantos](https://github.com/bhantos) in [#4236](https://github.com/PrefectHQ/fastmcp/pull/4236)
+* docs: add tool fingerprinting recipe by [@dgenio](https://github.com/dgenio) in [#4233](https://github.com/PrefectHQ/fastmcp/pull/4233)
+### Dependencies 📦
+* chore(deps): bump the uv group across 2 directories with 1 update by [@dependabot](https://github.com/dependabot) in [#4113](https://github.com/PrefectHQ/fastmcp/pull/4113)
+* chore(deps-dev): bump pydantic-monty from 0.0.16 to 0.0.17 by [@dependabot](https://github.com/dependabot) in [#4023](https://github.com/PrefectHQ/fastmcp/pull/4023)
+### Other Changes 🦾
+* Exempt maintainers from MRE auto-close by [@jlowin](https://github.com/jlowin) in [#4220](https://github.com/PrefectHQ/fastmcp/pull/4220)
+
+## New Contributors
+* @pragnyanramtha made their first contribution in [#4164](https://github.com/PrefectHQ/fastmcp/pull/4164)
+* @yuyua9 made their first contribution in [#4178](https://github.com/PrefectHQ/fastmcp/pull/4178)
+* @tiagoskaneta made their first contribution in [#4135](https://github.com/PrefectHQ/fastmcp/pull/4135)
+* @mohankumarelec made their first contribution in [#4142](https://github.com/PrefectHQ/fastmcp/pull/4142)
+* @rgillinlz made their first contribution in [#4130](https://github.com/PrefectHQ/fastmcp/pull/4130)
+* @jpr5 made their first contribution in [#4183](https://github.com/PrefectHQ/fastmcp/pull/4183)
+* @bhantos made their first contribution in [#4236](https://github.com/PrefectHQ/fastmcp/pull/4236)
+* @rene84 made their first contribution in [#4251](https://github.com/PrefectHQ/fastmcp/pull/4251)
+
+**Full Changelog**: [v3.3.1...v3.4.0](https://github.com/PrefectHQ/fastmcp/compare/v3.3.1...v3.4.0)
+
+
+
+
+
+**[v3.3.1: Loop There It Is](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.3.1)**
+
+A hotfix for the 3.3 packaging split. Clean installs could fail on standalone component imports like `from fastmcp.tools import tool`, because component modules reached auth and task primitives through `fastmcp.server` and pulled in the full server/provider stack. Those primitives now live in lightweight utility modules, with the old server import paths preserved as compatibility re-exports.
+
+### Fixes 🐞
+* fix(docs): use valid FA icon on client-only package page by [@jlowin](https://github.com/jlowin) in [#4139](https://github.com/PrefectHQ/fastmcp/pull/4139)
+* Decouple component imports from server by [@jlowin](https://github.com/jlowin) in [#4150](https://github.com/PrefectHQ/fastmcp/pull/4150)
+
+
+**Full Changelog**: [v3.3.0...v3.3.1](https://github.com/PrefectHQ/fastmcp/compare/v3.3.0...v3.3.1)
+
+
+
+
+
+**[v3.3.0: Slim Reaper](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.3.0)**
+
+FastMCP 3.3 ships `fastmcp-slim`, a dependency-light distribution that separates the client from the server stack — install FastMCP's client and transport layer without Starlette, Uvicorn, or the rest of the server machinery. The import namespace is unchanged. It also closes out a backlog of OAuth proxy security hardening, MCP-compliant OTEL instrumentation, and auth additions that accumulated through the 3.2 cycle.
+
+### New Features 🎉
+* Add fastmcp-slim for client-only installs by [@jlowin](https://github.com/jlowin) in [#4122](https://github.com/PrefectHQ/fastmcp/pull/4122)
+### Enhancements ✨
+* Add default prefill to FormInput.collect_input by [@jlowin](https://github.com/jlowin) in [#3937](https://github.com/PrefectHQ/fastmcp/pull/3937)
+* OTEL: Fix attribute compliance with MCP semantic conventions by [@strawgate](https://github.com/strawgate) in [#3889](https://github.com/PrefectHQ/fastmcp/pull/3889)
+* OTEL: Instrument all MCP list operations and enrich delegate spans by [@strawgate](https://github.com/strawgate) in [#3890](https://github.com/PrefectHQ/fastmcp/pull/3890)
+* Improve real-world schema crash test: failure dump, cluster analysis, TypeErrors baseline ratchet by [@jlowin](https://github.com/jlowin) in [#3958](https://github.com/PrefectHQ/fastmcp/pull/3958)
+* feat: add AzureB2CProvider for Azure AD B2C user flows by [@carlos-rian](https://github.com/carlos-rian) in [#3995](https://github.com/PrefectHQ/fastmcp/pull/3995)
+* Add run_in_thread opt-out for sync tools with thread affinity by [@jlowin](https://github.com/jlowin) in [#4010](https://github.com/PrefectHQ/fastmcp/pull/4010)
+* Add missing return type annotation to __getattr__ by [@ZLeventer](https://github.com/ZLeventer) in [#4026](https://github.com/PrefectHQ/fastmcp/pull/4026)
+* Add experimental_capabilities kwarg to FastMCP constructor by [@jlowin](https://github.com/jlowin) in [#4042](https://github.com/PrefectHQ/fastmcp/pull/4042)
+* Add log_level parameter to FastMCP errors by [@daniel-tsiang](https://github.com/daniel-tsiang) in [#4036](https://github.com/PrefectHQ/fastmcp/pull/4036)
+* Bump pydocket to 0.20.0 by [@chrisguidry](https://github.com/chrisguidry) in [#4031](https://github.com/PrefectHQ/fastmcp/pull/4031)
+* enh: Add public API for updating OAuthProxy scopes after initialization by [@taylorwilsdon](https://github.com/taylorwilsdon) in [#4091](https://github.com/PrefectHQ/fastmcp/pull/4091)
+* Refine fastmcp-slim packaging by [@jlowin](https://github.com/jlowin) in [#4125](https://github.com/PrefectHQ/fastmcp/pull/4125)
+### Security 🔒
+* Harden OAuth Proxy silent consent against AS-in-the-middle by [@jlowin](https://github.com/jlowin) in [#3960](https://github.com/PrefectHQ/fastmcp/pull/3960)
+* Reject dot-segments in redirect URI allowlist matching by [@jlowin](https://github.com/jlowin) in [#3963](https://github.com/PrefectHQ/fastmcp/pull/3963)
+* Bump deps with open dependabot alerts by [@jlowin](https://github.com/jlowin) in [#3965](https://github.com/PrefectHQ/fastmcp/pull/3965)
+* Partition ResponseCachingMiddleware cache by access token by [@jlowin](https://github.com/jlowin) in [#4041](https://github.com/PrefectHQ/fastmcp/pull/4041)
+### Fixes 🐞
+* fix: reject self-mount to prevent infinite recursion by [@strawgate](https://github.com/strawgate) in [#3925](https://github.com/PrefectHQ/fastmcp/pull/3925)
+* fix: ProxyTool crashes on non-TextContent error responses by [@strawgate](https://github.com/strawgate) in [#3926](https://github.com/PrefectHQ/fastmcp/pull/3926)
+* fix: _prune_param and _convert_nullable_field mutate input schemas by [@strawgate](https://github.com/strawgate) in [#3927](https://github.com/PrefectHQ/fastmcp/pull/3927)
+* fix: narrow OpenAI audio format dict to Literal for ty by [@jlowin](https://github.com/jlowin) in [#3936](https://github.com/PrefectHQ/fastmcp/pull/3936)
+* fix: allow hyphens in resource template parameter names by [@strawgate](https://github.com/strawgate) in [#3929](https://github.com/PrefectHQ/fastmcp/pull/3929)
+* fix: OpenAPI request director sends multipart and form-urlencoded as JSON by [@strawgate](https://github.com/strawgate) in [#3932](https://github.com/PrefectHQ/fastmcp/pull/3932)
+* Fix raise_on_error handling for tool tasks by [@gnanirahulnutakki](https://github.com/gnanirahulnutakki) in [#3946](https://github.com/PrefectHQ/fastmcp/pull/3946)
+* fix: FileSystemProvider reload race condition by [@strawgate](https://github.com/strawgate) in [#3938](https://github.com/PrefectHQ/fastmcp/pull/3938)
+* fix tests that relied on task=True returning error results by [@jlowin](https://github.com/jlowin) in [#3954](https://github.com/PrefectHQ/fastmcp/pull/3954)
+* Restore task snapshot via a worker-level dependency by [@chrisguidry](https://github.com/chrisguidry) in [#3945](https://github.com/PrefectHQ/fastmcp/pull/3945)
+* Forward backend capabilities in ProxyProvider by [@jlowin](https://github.com/jlowin) in [#3956](https://github.com/PrefectHQ/fastmcp/pull/3956)
+* Allow upstream client_id to be used directly without DCR by [@jlowin](https://github.com/jlowin) in [#3957](https://github.com/PrefectHQ/fastmcp/pull/3957)
+* Graceful fallback for unsupported regex patterns in json_schema_to_type by [@jlowin](https://github.com/jlowin) in [#3959](https://github.com/PrefectHQ/fastmcp/pull/3959)
+* Revert "Forward backend capabilities in ProxyProvider (#3956)" by [@jlowin](https://github.com/jlowin) in [#3964](https://github.com/PrefectHQ/fastmcp/pull/3964)
+* fix: skip stdio subprocess test on Windows CI by [@jlowin](https://github.com/jlowin) in [#3966](https://github.com/PrefectHQ/fastmcp/pull/3966)
+* fix: bound _refresh_locks with LRU eviction to prevent memory leak by [@jlowin](https://github.com/jlowin) in [#3968](https://github.com/PrefectHQ/fastmcp/pull/3968)
+* fix: handle circular JSON Pointer $ref in dereference_refs by [@lawrence3699](https://github.com/lawrence3699) in [#3896](https://github.com/PrefectHQ/fastmcp/pull/3896)
+* fix: honor upstream refresh token expiry in OAuthProxy by [@jlowin](https://github.com/jlowin) in [#3990](https://github.com/PrefectHQ/fastmcp/pull/3990)
+* fix: narrow _token_validator with isinstance for ty in AzureProvider.from_b2c by [@jlowin](https://github.com/jlowin) in [#4007](https://github.com/PrefectHQ/fastmcp/pull/4007)
+* fix: cancel orphaned session_task when Client._disconnect times out by [@jlowin](https://github.com/jlowin) in [#4011](https://github.com/PrefectHQ/fastmcp/pull/4011)
+* fix: preserve @tool metadata in from_function by [@lawrence3699](https://github.com/lawrence3699) in [#4072](https://github.com/PrefectHQ/fastmcp/pull/4072)
+* fix(openapi): keep blank values in parse_qs (refs #4056) by [@MukundaKatta](https://github.com/MukundaKatta) in [#4076](https://github.com/PrefectHQ/fastmcp/pull/4076)
+* Fix #4056: keep blank query values, add token bucket regression test by [@MukundaKatta](https://github.com/MukundaKatta) in [#4069](https://github.com/PrefectHQ/fastmcp/pull/4069)
+* fix(ping): exit ping loop cleanly when session stream is closed by [@ashwin153](https://github.com/ashwin153) in [#4087](https://github.com/PrefectHQ/fastmcp/pull/4087)
+* Fix sampling from background tasks by [@cuyua9](https://github.com/cuyua9) in [#4068](https://github.com/PrefectHQ/fastmcp/pull/4068)
+* Make Docket reentrant; mounted servers enter their own lifespan by [@jlowin](https://github.com/jlowin) in [#4095](https://github.com/PrefectHQ/fastmcp/pull/4095)
+* fix(tool_transform): hoist $defs to schema root when ArgTransform introduces them by [@SarthakB11](https://github.com/SarthakB11) in [#4101](https://github.com/PrefectHQ/fastmcp/pull/4101)
+* fix(auth): silence authlib.jose DeprecationWarning at JWT import by [@SarthakB11](https://github.com/SarthakB11) in [#4100](https://github.com/PrefectHQ/fastmcp/pull/4100)
+* fix: don't cache import map in dev apps bundle by [@jlowin](https://github.com/jlowin) in [#4106](https://github.com/PrefectHQ/fastmcp/pull/4106)
+* #4084 [Issues] Windows startup crash due to UnicodeDecodeError when l… by [@doneman536](https://github.com/doneman536) in [#4092](https://github.com/PrefectHQ/fastmcp/pull/4092)
+* fix: drop exc_info for expected tool failures, remove unreachable ValidationError by [@sergeykad](https://github.com/sergeykad) in [#4029](https://github.com/PrefectHQ/fastmcp/pull/4029)
+* fix: cli option --no-banner is NOT passed to cli but server-spec in-correctly when cli --reload option is specified. by [@itaru2622](https://github.com/itaru2622) in [#4083](https://github.com/PrefectHQ/fastmcp/pull/4083)
+* Fix None backend_* span attributes on un-renamed proxy components by [@ringerc](https://github.com/ringerc) in [#4109](https://github.com/PrefectHQ/fastmcp/pull/4109)
+* Fix OCI Provider issue in 3.x version. Add OCI auth provider example … by [@kiranthakkar](https://github.com/kiranthakkar) in [#4116](https://github.com/PrefectHQ/fastmcp/pull/4116)
+* fix(http): terminate active streamable-HTTP transports before lifespan shutdown by [@SarthakB11](https://github.com/SarthakB11) in [#4118](https://github.com/PrefectHQ/fastmcp/pull/4118)
+### Docs 📚
+* Restructure docs navigation by [@jlowin](https://github.com/jlowin) in [#3951](https://github.com/PrefectHQ/fastmcp/pull/3951)
+* docs: standardize ToolAnnotations examples by [@gnanirahulnutakki](https://github.com/gnanirahulnutakki) in [#3952](https://github.com/PrefectHQ/fastmcp/pull/3952)
+* Be constructively skeptical of bot reviews on own PRs by [@jlowin](https://github.com/jlowin) in [#3971](https://github.com/PrefectHQ/fastmcp/pull/3971)
+* Add UTM params to Horizon docs links by [@aaazzam](https://github.com/aaazzam) in [#4018](https://github.com/PrefectHQ/fastmcp/pull/4018)
+* Add a sandboxed-agents deployment guide by [@strawgate](https://github.com/strawgate) in [#4027](https://github.com/PrefectHQ/fastmcp/pull/4027)
+* docs: add best practices for custom telemetry spans by [@MukundaKatta](https://github.com/MukundaKatta) in [#4001](https://github.com/PrefectHQ/fastmcp/pull/4001)
+* Refresh landing page copy by [@jlowin](https://github.com/jlowin) in [#4043](https://github.com/PrefectHQ/fastmcp/pull/4043)
+* Refresh landing page copy by [@jlowin](https://github.com/jlowin) in [#4047](https://github.com/PrefectHQ/fastmcp/pull/4047)
+* Add UTM tracking to Horizon links by [@jlowin](https://github.com/jlowin) in [#4064](https://github.com/PrefectHQ/fastmcp/pull/4064)
+* docs(integrations): add Pydantic AI FastMCP toolset guide by [@MukundaKatta](https://github.com/MukundaKatta) in [#4070](https://github.com/PrefectHQ/fastmcp/pull/4070)
+* docs: fix broken links in Pydantic AI guide by [@jlowin](https://github.com/jlowin) in [#4094](https://github.com/PrefectHQ/fastmcp/pull/4094)
+### Dependencies 📦
+* chore(deps-dev): bump pydantic-monty from 0.0.11 to 0.0.12 by [@dependabot](https://github.com/dependabot) in [#3940](https://github.com/PrefectHQ/fastmcp/pull/3940)
+* chore(deps-dev): bump pydantic-monty from 0.0.14 to 0.0.16 by [@dependabot](https://github.com/dependabot) in [#3984](https://github.com/PrefectHQ/fastmcp/pull/3984)
+### Other Changes 🦾
+* fix: Don't completely hide plain mcp.tool app-only tools by [@owtaylor](https://github.com/owtaylor) in [#4112](https://github.com/PrefectHQ/fastmcp/pull/4112)
+
+## New Contributors
+* @gnanirahulnutakki made their first contribution in [#3946](https://github.com/PrefectHQ/fastmcp/pull/3946)
+* @lawrence3699 made their first contribution in [#3896](https://github.com/PrefectHQ/fastmcp/pull/3896)
+* @carlos-rian made their first contribution in [#3995](https://github.com/PrefectHQ/fastmcp/pull/3995)
+* @ZLeventer made their first contribution in [#4026](https://github.com/PrefectHQ/fastmcp/pull/4026)
+* @MukundaKatta made their first contribution in [#4001](https://github.com/PrefectHQ/fastmcp/pull/4001)
+* @daniel-tsiang made their first contribution in [#4036](https://github.com/PrefectHQ/fastmcp/pull/4036)
+* @ashwin153 made their first contribution in [#4087](https://github.com/PrefectHQ/fastmcp/pull/4087)
+* @cuyua9 made their first contribution in [#4068](https://github.com/PrefectHQ/fastmcp/pull/4068)
+* @taylorwilsdon made their first contribution in [#4091](https://github.com/PrefectHQ/fastmcp/pull/4091)
+* @SarthakB11 made their first contribution in [#4101](https://github.com/PrefectHQ/fastmcp/pull/4101)
+* @doneman536 made their first contribution in [#4092](https://github.com/PrefectHQ/fastmcp/pull/4092)
+* @sergeykad made their first contribution in [#4029](https://github.com/PrefectHQ/fastmcp/pull/4029)
+* @ringerc made their first contribution in [#4109](https://github.com/PrefectHQ/fastmcp/pull/4109)
+
+**Full Changelog**: [v3.2.4...v3.3.0](https://github.com/PrefectHQ/fastmcp/compare/v3.2.4...v3.3.0)
+
+
+
+
+
+**[v3.2.4: Patch Me If You Can](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.2.4)**
+
+A grab bag of fixes, hardening, and polish. The headline behavior change: background tasks are now scoped to the authorization context rather than the MCP session, so a task survives session churn and stays tied to who started it — a breaking change for anyone relying on the old session-scoped semantics. Plus actual-size validation in `FileUpload`, a Keycloak OAuth provider, automatic parameter descriptions from docstrings, and dozens of schema and sampling fixes.
+
+### Breaking Changes ⚠️
+* Scope tasks to authorization context, not session by [@chrisguidry](https://github.com/chrisguidry) in [#3800](https://github.com/PrefectHQ/fastmcp/pull/3800)
+### Enhancements ✨
+* Bump pydocket>=0.19.0, drop fakeredis pin by [@chrisguidry](https://github.com/chrisguidry) in [#3822](https://github.com/PrefectHQ/fastmcp/pull/3822)
+* Add real-world schema crash test (232K schemas from APIs.guru) by [@strawgate](https://github.com/strawgate) in [#3826](https://github.com/PrefectHQ/fastmcp/pull/3826)
+* Enable 7 zero-violation ruff rules by [@strawgate](https://github.com/strawgate) in [#3841](https://github.com/PrefectHQ/fastmcp/pull/3841)
+* Promote 7 ty rules from ignore to warn by [@strawgate](https://github.com/strawgate) in [#3852](https://github.com/PrefectHQ/fastmcp/pull/3852)
+* Replace ___ with hash-based backend tool routing and per-tool prefab resources by [@jlowin](https://github.com/jlowin) in [#3824](https://github.com/PrefectHQ/fastmcp/pull/3824)
+* Enable 4 ruff rules (DTZ, ERA, ISC, INP) and fix 9 violations by [@strawgate](https://github.com/strawgate) in [#3842](https://github.com/PrefectHQ/fastmcp/pull/3842)
+* Extract parameter descriptions from docstrings by [@jlowin](https://github.com/jlowin) in [#3872](https://github.com/PrefectHQ/fastmcp/pull/3872)
+* ci: speed up schema crash test (CSafeLoader + xdist-safe aggregation) by [@jlowin](https://github.com/jlowin) in [#3873](https://github.com/PrefectHQ/fastmcp/pull/3873)
+* test: bump OpenAPI init perf threshold to 200ms for Windows CI by [@jlowin](https://github.com/jlowin) in [#3879](https://github.com/PrefectHQ/fastmcp/pull/3879)
+* refactor: unify object-schema conversion through _object_schema_to_type by [@jlowin](https://github.com/jlowin) in [#3884](https://github.com/PrefectHQ/fastmcp/pull/3884)
+* Add Keycloak OAuth Provider for Enterprise Authentication and local dev by [@stephaneberle9](https://github.com/stephaneberle9) in [#1937](https://github.com/PrefectHQ/fastmcp/pull/1937)
+* Allow auth providers to override protected resource base URLs by [@aaazzam](https://github.com/aaazzam) in [#3900](https://github.com/PrefectHQ/fastmcp/pull/3900)
+* Enable PERF and T20 ruff rules by [@strawgate](https://github.com/strawgate) in [#3845](https://github.com/PrefectHQ/fastmcp/pull/3845)
+* Add response_title and response_description to ctx.elicit() by [@jlowin](https://github.com/jlowin) in [#3912](https://github.com/PrefectHQ/fastmcp/pull/3912)
+* Deprecate ctx.elicit() without response_type by [@jlowin](https://github.com/jlowin) in [#3916](https://github.com/PrefectHQ/fastmcp/pull/3916)
+### Security 🔒
+* Validate actual base64 data size in FileUpload, not client-reported size by [@strawgate](https://github.com/strawgate) in [#3816](https://github.com/PrefectHQ/fastmcp/pull/3816)
+* Stop forwarding inbound HTTP headers to unrelated remote servers by [@jlowin](https://github.com/jlowin) in [#3837](https://github.com/PrefectHQ/fastmcp/pull/3837)
+* AuthKit: auto-bind token audience to resource URL (RFC 8707) by [@jlowin](https://github.com/jlowin) in [#3905](https://github.com/PrefectHQ/fastmcp/pull/3905)
+### Fixes 🐞
+* Version-check is_docket_available() to avoid transitive pydocket crash by [@jlowin](https://github.com/jlowin) in [#3807](https://github.com/PrefectHQ/fastmcp/pull/3807)
+* fix: materialize generators before result conversion, handle bytes gracefully by [@strawgate](https://github.com/strawgate) in [#3830](https://github.com/PrefectHQ/fastmcp/pull/3830)
+* Fix json_schema_to_type crashes on keywords, boolean schemas, empty enums, and name collisions by [@strawgate](https://github.com/strawgate) in [#3818](https://github.com/PrefectHQ/fastmcp/pull/3818)
+* fix: replace `or` with `is not None` checks for config/override merging by [@strawgate](https://github.com/strawgate) in [#3833](https://github.com/PrefectHQ/fastmcp/pull/3833)
+* fix: TransformedTool sync fn crash and schema mutation by [@strawgate](https://github.com/strawgate) in [#3823](https://github.com/PrefectHQ/fastmcp/pull/3823)
+* fix: cross-provider duplicate detection, error visibility, mask propagation by [@strawgate](https://github.com/strawgate) in [#3827](https://github.com/PrefectHQ/fastmcp/pull/3827)
+* fix: don't pass HTTP kwargs when transport is unspecified by [@strawgate](https://github.com/strawgate) in [#3838](https://github.com/PrefectHQ/fastmcp/pull/3838)
+* fix: strip title fields from tool schemas for Gemini 2.5 Flash compatibility by [@strawgate](https://github.com/strawgate) in [#3861](https://github.com/PrefectHQ/fastmcp/pull/3861)
+* fix: retry when LLM returns text instead of calling final_response by [@strawgate](https://github.com/strawgate) in [#3850](https://github.com/PrefectHQ/fastmcp/pull/3850)
+* Raise on unhandled content types in sampling handler dispatch chains by [@strawgate](https://github.com/strawgate) in [#3857](https://github.com/PrefectHQ/fastmcp/pull/3857)
+* Fix broken code examples in docs by [@strawgate](https://github.com/strawgate) in [#3869](https://github.com/PrefectHQ/fastmcp/pull/3869)
+* fix: GoogleGenaiSamplingHandler leaks thought parts and gives unhelpful errors on empty responses by [@strawgate](https://github.com/strawgate) in [#3849](https://github.com/PrefectHQ/fastmcp/pull/3849)
+* fix: cap consecutive final_response validation retries by [@strawgate](https://github.com/strawgate) in [#3851](https://github.com/PrefectHQ/fastmcp/pull/3851)
+* Fix test quality issues by [@strawgate](https://github.com/strawgate) in [#3854](https://github.com/PrefectHQ/fastmcp/pull/3854)
+* Fix MCP tool on docs welcome page by [@lkiesow](https://github.com/lkiesow) in [#3874](https://github.com/PrefectHQ/fastmcp/pull/3874)
+* Fix CIMD clients getting required_scopes instead of valid_scopes by [@jlowin](https://github.com/jlowin) in [#3836](https://github.com/PrefectHQ/fastmcp/pull/3836)
+* Rename filesystem-provider example dir to avoid mcp/ collision by [@jlowin](https://github.com/jlowin) in [#3878](https://github.com/PrefectHQ/fastmcp/pull/3878)
+* fix: drop configurable dedupe from AggregateProvider, always warn by [@jlowin](https://github.com/jlowin) in [#3877](https://github.com/PrefectHQ/fastmcp/pull/3877)
+* fix: resolve list[dict] return type producing Root() instead of dicts by [@KeWang0622](https://github.com/KeWang0622) in [#3880](https://github.com/PrefectHQ/fastmcp/pull/3880)
+* fix: strip titles from bare-metadata nodes (Gemini 2.5 Flash) by [@jlowin](https://github.com/jlowin) in [#3881](https://github.com/PrefectHQ/fastmcp/pull/3881)
+* Fix wildcard resource template params in mounted servers by [@jlowin](https://github.com/jlowin) in [#3899](https://github.com/PrefectHQ/fastmcp/pull/3899)
+* Harden forced client disconnect cleanup by [@vonbai](https://github.com/vonbai) in [#3885](https://github.com/PrefectHQ/fastmcp/pull/3885)
+* fix: elicitation scalar return, resource auto-serialization, Client.new() state, prompt errors by [@strawgate](https://github.com/strawgate) in [#3859](https://github.com/PrefectHQ/fastmcp/pull/3859)
+* fix: task.wait() hangs indefinitely when task enters input_required by [@mrishav](https://github.com/mrishav) in [#3798](https://github.com/PrefectHQ/fastmcp/pull/3798)
+* Fix RetryMiddleware not retrying tool errors by [@strawgate](https://github.com/strawgate) in [#3858](https://github.com/PrefectHQ/fastmcp/pull/3858)
+* Stop pydantic 2.13 from leaking _WrappedResult docstring into tool output schemas by [@jlowin](https://github.com/jlowin) in [#3918](https://github.com/PrefectHQ/fastmcp/pull/3918)
+### Docs 📚
+* Note generate-notes API in release workflow docs by [@jlowin](https://github.com/jlowin) in [#3806](https://github.com/PrefectHQ/fastmcp/pull/3806)
+* docs: require agents to respect DNM markers on PRs by [@jlowin](https://github.com/jlowin) in [#3871](https://github.com/PrefectHQ/fastmcp/pull/3871)
+* docs: add uv-managed dependencies and uvx examples to mcp-json configuration by [@vincent067](https://github.com/vincent067) in [#3843](https://github.com/PrefectHQ/fastmcp/pull/3843)
+* docs: link fastmcp-keycloak-local companion project from Keycloak integration page by [@stephaneberle9](https://github.com/stephaneberle9) in [#3904](https://github.com/PrefectHQ/fastmcp/pull/3904)
+* Overhaul apps docs by [@jlowin](https://github.com/jlowin) in [#3915](https://github.com/PrefectHQ/fastmcp/pull/3915)
+### Dependencies 📦
+* chore(deps): bump extractions/setup-just from 3 to 4 by [@dependabot](https://github.com/dependabot) in [#3863](https://github.com/PrefectHQ/fastmcp/pull/3863)
+* chore(deps): bump astral-sh/setup-uv from 6 to 7 by [@dependabot](https://github.com/dependabot) in [#3865](https://github.com/PrefectHQ/fastmcp/pull/3865)
+* chore(deps): bump actions/checkout from 4 to 6 by [@dependabot](https://github.com/dependabot) in [#3864](https://github.com/PrefectHQ/fastmcp/pull/3864)
+* chore(deps-dev): bump pydantic-monty from 0.0.9 to 0.0.10 by [@dependabot](https://github.com/dependabot) in [#3809](https://github.com/PrefectHQ/fastmcp/pull/3809)
+* chore(deps): bump the uv group across 2 directories with 1 update by [@dependabot](https://github.com/dependabot) in [#3913](https://github.com/PrefectHQ/fastmcp/pull/3913)
+
+## New Contributors
+* @lkiesow made their first contribution in [#3874](https://github.com/PrefectHQ/fastmcp/pull/3874)
+* @KeWang0622 made their first contribution in [#3880](https://github.com/PrefectHQ/fastmcp/pull/3880)
+* @vonbai made their first contribution in [#3885](https://github.com/PrefectHQ/fastmcp/pull/3885)
+
+**Full Changelog**: [v3.2.3...v3.2.4](https://github.com/PrefectHQ/fastmcp/compare/v3.2.3...v3.2.4)
+
+
+
+
+
+**[v3.2.3: Redis or Not](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.2.3)**
+
+A stopgap pin: fakeredis 2.35.0 shipped an undocumented rename that broke pydocket's `memory://` backend, causing `fastmcp[tasks]` installs to fail at startup with an `ImportError`. This pins `fakeredis<2.35.0` in the `tasks` extra until a fixed pydocket ships.
+
+### Fixes 🐞
+* Pin `fakeredis<2.35.0` in tasks extra by [@jlowin](https://github.com/jlowin) in [#3804](https://github.com/PrefectHQ/fastmcp/pull/3804)
+### Docs 📚
+* Document session state isolation across mount boundaries by [@jlowin](https://github.com/jlowin) in [#3801](https://github.com/PrefectHQ/fastmcp/pull/3801)
+
+
+**Full Changelog**: [v3.2.2...v3.2.3](https://github.com/PrefectHQ/fastmcp/compare/v3.2.2...v3.2.3)
+
+
+
+
+
+**[v3.2.2: Audience Appreciation](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.2.2)**
+
+Fixes the Azure audience regression from 3.2.1: validation switched from `client_id` to `identifier_uri`, which fixed custom Application ID URIs but broke the default case where Azure AD v2 tokens set `aud` to the bare client ID GUID. Both formats are now accepted.
+
+### Fixes 🐞
+* fix: accept both client_id and identifier_uri as Azure audience by [@jlowin](https://github.com/jlowin) in [#3797](https://github.com/PrefectHQ/fastmcp/pull/3797)
+### Dependencies 📦
+* chore(deps): bump the uv group across 2 directories with 1 update by [@dependabot](https://github.com/dependabot) in [#3795](https://github.com/PrefectHQ/fastmcp/pull/3795)
+
+
+**Full Changelog**: [v3.2.1...v3.2.2](https://github.com/PrefectHQ/fastmcp/compare/v3.2.1...v3.2.2)
+
+
+
+
+
+**[v3.2.1: Audience Participation](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.2.1)**
+
+A patch focused on auth-provider audience validation. Cognito tokens now validate on `client_id` (they carry no `aud`), Azure honors the `identifier_uri` parameter for Entra v2.0 tokens, and consent cookies are LRU-capped to prevent unbounded growth past reverse proxy header limits. Also fixes OpenAPI 3.0 `nullable` fields leaking into tool input schemas and server-variable substitution in base URLs.
+
+### Breaking Changes ⚠️
+* fix(google): use sub (user ID) for client_id instead of aud (app ID) by [@shigechika](https://github.com/shigechika) in [#3722](https://github.com/PrefectHQ/fastmcp/pull/3722)
+* fix: remove CSP from tool metadata, keep on resource only by [@jlowin](https://github.com/jlowin) in [#3754](https://github.com/PrefectHQ/fastmcp/pull/3754)
+### Enhancements ✨
+* [codex] Add FastMCP docs telemetry by [@aaazzam](https://github.com/aaazzam) in [#3727](https://github.com/PrefectHQ/fastmcp/pull/3727)
+* chore: split SDK navigation into standalone $ref file by [@jlowin](https://github.com/jlowin) in [#3773](https://github.com/PrefectHQ/fastmcp/pull/3773)
+* fix: bump ty to >=0.0.29 and suppress new false positives by [@jlowin](https://github.com/jlowin) in [#3790](https://github.com/PrefectHQ/fastmcp/pull/3790)
+### Fixes 🐞
+* fix: use explicit None checks for JWT exp validation by [@jlowin](https://github.com/jlowin) in [#3724](https://github.com/PrefectHQ/fastmcp/pull/3724)
+* Unify background task context forwarding, fix concurrent dependency bugs by [@chrisguidry](https://github.com/chrisguidry) in [#3710](https://github.com/PrefectHQ/fastmcp/pull/3710)
+* fix: add proxy timeouts and modernize networking in apps dev by [@mateeaaa](https://github.com/mateeaaa) in [#3741](https://github.com/PrefectHQ/fastmcp/pull/3741)
+* fix: ResponseLimitingMiddleware no longer breaks outputSchema tools by [@jlowin](https://github.com/jlowin) in [#3756](https://github.com/PrefectHQ/fastmcp/pull/3756)
+* fix: substitute server variable defaults when building base URL from OpenAPI spec by [@mrishav](https://github.com/mrishav) in [#3770](https://github.com/PrefectHQ/fastmcp/pull/3770)
+* fix: FastAPI TestClient compatibility and lifespan re-initialization by [@kvdhanush06](https://github.com/kvdhanush06) in [#3736](https://github.com/PrefectHQ/fastmcp/pull/3736)
+* fix: propagate upstream_claims in load_access_token by [@kvdhanush06](https://github.com/kvdhanush06) in [#3750](https://github.com/PrefectHQ/fastmcp/pull/3750)
+* Remove deprecated asyncio.iscoroutinefunction fallback by [@kaiisfree](https://github.com/kaiisfree) in [#3767](https://github.com/PrefectHQ/fastmcp/pull/3767)
+* fix: changeable allowed_client_redirect_uris on OAuthProxy by [@fengarix](https://github.com/fengarix) in [#3772](https://github.com/PrefectHQ/fastmcp/pull/3772)
+* fix: broken link in changelog by [@jlowin](https://github.com/jlowin) in [#3775](https://github.com/PrefectHQ/fastmcp/pull/3775)
+* fix(docs): correct FastMCP tool name in welcome docs by [@buyua9](https://github.com/buyua9) in [#3781](https://github.com/PrefectHQ/fastmcp/pull/3781)
+* fix: cap consent cookie size to prevent header overflow by [@jlowin](https://github.com/jlowin) in [#3784](https://github.com/PrefectHQ/fastmcp/pull/3784)
+* Fix boolean property schemas in JSON Schema parsing by [@jlowin](https://github.com/jlowin) in [#3785](https://github.com/PrefectHQ/fastmcp/pull/3785)
+* Fix OpenAPI 3.0 nullable fields in tool input schemas by [@kvdhanush06](https://github.com/kvdhanush06) in [#3768](https://github.com/PrefectHQ/fastmcp/pull/3768)
+* fix: Cognito token verification checks client_id instead of aud by [@jlowin](https://github.com/jlowin) in [#3786](https://github.com/PrefectHQ/fastmcp/pull/3786)
+* fix: use identifier_uri as audience for Azure token validation by [@jlowin](https://github.com/jlowin) in [#3787](https://github.com/PrefectHQ/fastmcp/pull/3787)
+* Harden client tool result error handling by [@aimable100](https://github.com/aimable100) in [#3778](https://github.com/PrefectHQ/fastmcp/pull/3778)
+### Docs 📚
+* Github integraiton documentation fix: use result.data otherwise CallToolResult not scriptable by [@c4jquick](https://github.com/c4jquick) in [#3753](https://github.com/PrefectHQ/fastmcp/pull/3753)
+* chore: split v2 docs navigation into separate file by [@jlowin](https://github.com/jlowin) in [#3762](https://github.com/PrefectHQ/fastmcp/pull/3762)
+* docs: document forward_resource parameter on OAuthProxy by [@jlowin](https://github.com/jlowin) in [#3788](https://github.com/PrefectHQ/fastmcp/pull/3788)
+### Examples & Contrib 💡
+* fix: boolean false values dropped in form submissions by [@jlowin](https://github.com/jlowin) in [#3776](https://github.com/PrefectHQ/fastmcp/pull/3776)
+### Dependencies 📦
+* chore(deps): bump fastmcp from 3.1.1 to 3.2.0 in /examples/testing_demo in the uv group across 1 directory by [@dependabot](https://github.com/dependabot) in [#3728](https://github.com/PrefectHQ/fastmcp/pull/3728)
+* chore(deps): bump anthropic from 0.86.0 to 0.87.0 in the uv group across 1 directory by [@dependabot](https://github.com/dependabot) in [#3742](https://github.com/PrefectHQ/fastmcp/pull/3742)
+
+## New Contributors
+* @c4jquick made their first contribution in [#3753](https://github.com/PrefectHQ/fastmcp/pull/3753)
+* @mateeaaa made their first contribution in [#3741](https://github.com/PrefectHQ/fastmcp/pull/3741)
+* @mrishav made their first contribution in [#3770](https://github.com/PrefectHQ/fastmcp/pull/3770)
+* @kvdhanush06 made their first contribution in [#3736](https://github.com/PrefectHQ/fastmcp/pull/3736)
+* @kaiisfree made their first contribution in [#3767](https://github.com/PrefectHQ/fastmcp/pull/3767)
+* @fengarix made their first contribution in [#3772](https://github.com/PrefectHQ/fastmcp/pull/3772)
+* @buyua9 made their first contribution in [#3781](https://github.com/PrefectHQ/fastmcp/pull/3781)
+* @aimable100 made their first contribution in [#3778](https://github.com/PrefectHQ/fastmcp/pull/3778)
+
+**Full Changelog**: [v3.2.0...v3.2.1](https://github.com/PrefectHQ/fastmcp/compare/v3.2.0...v3.2.1)
+
+
+
+
+
+**[v3.2.0: Show Don't Tool](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.2.0)**
+
+FastMCP 3.2 is the Apps release: your tools can now return interactive UIs — charts, dashboards, forms, maps — rendered right inside the conversation. `FastMCPApp` separates the tools the LLM sees from the backend tools the UI calls, five built-in providers (FileUpload, Approval, Choice, FormInput, GenerativeUI) cover common interaction patterns, and `fastmcp dev apps` gives you a browser preview. The release also lands a significant security hardening pass across SSRF/path-traversal, JWT algorithm restrictions, OAuth scope enforcement, and CSRF.
+
+### New Features 🎉
+* Add FastMCPApp — a Provider for composable MCP applications by [@jlowin](https://github.com/jlowin) in [#3385](https://github.com/PrefectHQ/fastmcp/pull/3385)
+* Add fastmcp dev apps command with browser UI preview by [@jlowin](https://github.com/jlowin) in [#3489](https://github.com/PrefectHQ/fastmcp/pull/3489)
+* Add GenerativeUI provider, bump prefab-ui 0.14.0 by [@jlowin](https://github.com/jlowin) in [#3647](https://github.com/PrefectHQ/fastmcp/pull/3647)
+* Add FileUpload provider by [@jlowin](https://github.com/jlowin) in [#3669](https://github.com/PrefectHQ/fastmcp/pull/3669)
+* Add Approval and Choice providers by [@jlowin](https://github.com/jlowin) in [#3686](https://github.com/PrefectHQ/fastmcp/pull/3686)
+* Add FormInput provider, bump prefab-ui to 0.15.0 by [@jlowin](https://github.com/jlowin) in [#3687](https://github.com/PrefectHQ/fastmcp/pull/3687)
+### Breaking Changes ⚠️
+* Route app tool calls via ___-prefixed names by [@jlowin](https://github.com/jlowin) in [#3667](https://github.com/PrefectHQ/fastmcp/pull/3667)
+### Enhancements ✨
+* feat: add `--config-path` flag to claude-desktop install command by [@Sumanshu-Nankana](https://github.com/Sumanshu-Nankana) in [#3380](https://github.com/PrefectHQ/fastmcp/pull/3380)
+* Support ImageContent and AudioContent in Message class by [@ericrobinson-indeed](https://github.com/ericrobinson-indeed) in [#3396](https://github.com/PrefectHQ/fastmcp/pull/3396)
+* Deprecate PromptToolMiddleware and ResourceToolMiddleware by [@jlowin](https://github.com/jlowin) in [#3389](https://github.com/PrefectHQ/fastmcp/pull/3389)
+* Block HS* algorithms when JWTVerifier is configured with JWKS by [@jlowin](https://github.com/jlowin) in [#3419](https://github.com/PrefectHQ/fastmcp/pull/3419)
+* Remove prek from Marvin workflows by [@jlowin](https://github.com/jlowin) in [#3444](https://github.com/PrefectHQ/fastmcp/pull/3444)
+* Add dependency version compatibility guidance to code-review skill by [@jlowin](https://github.com/jlowin) in [#3475](https://github.com/PrefectHQ/fastmcp/pull/3475)
+* Remove "good first issue" label by [@jlowin](https://github.com/jlowin) in [#3482](https://github.com/PrefectHQ/fastmcp/pull/3482)
+* Cache component lists in ProxyProvider by [@jlowin](https://github.com/jlowin) in [#3479](https://github.com/PrefectHQ/fastmcp/pull/3479)
+* Support logging/setLevel and add client_log_level by [@jlowin](https://github.com/jlowin) in [#3491](https://github.com/PrefectHQ/fastmcp/pull/3491)
+* Propagate x-fastmcp-wrap-result in tool result _meta by [@jlowin](https://github.com/jlowin) in [#3490](https://github.com/PrefectHQ/fastmcp/pull/3490)
+* feat(auth): add external_consent param to suppress misleading warning by [@mtthidoteu](https://github.com/mtthidoteu) in [#3473](https://github.com/PrefectHQ/fastmcp/pull/3473)
+* Add `verify` parameter for SSL certificate configuration by [@jlowin](https://github.com/jlowin) in [#3487](https://github.com/PrefectHQ/fastmcp/pull/3487)
+* Expose minimum_check_interval, reduce task pickup latency by [@jlowin](https://github.com/jlowin) in [#3500](https://github.com/PrefectHQ/fastmcp/pull/3500)
+* Fix test timeouts, suppress deprecation warnings, speed up auth tests by [@jlowin](https://github.com/jlowin) in [#3504](https://github.com/PrefectHQ/fastmcp/pull/3504)
+* Auto-close upgrade check issue when build passes by [@jlowin](https://github.com/jlowin) in [#3505](https://github.com/PrefectHQ/fastmcp/pull/3505)
+* feat: make upstream_client_secret optional in OAuthProxy by [@jlowin](https://github.com/jlowin) in [#3486](https://github.com/PrefectHQ/fastmcp/pull/3486)
+* Add security label to triage workflow and release notes by [@jlowin](https://github.com/jlowin) in [#3516](https://github.com/PrefectHQ/fastmcp/pull/3516)
+* Claude/review contributor guidelines by [@jlowin](https://github.com/jlowin) in [#3517](https://github.com/PrefectHQ/fastmcp/pull/3517)
+* pin pydantic-monty to 0.0.8 by [@jlowin](https://github.com/jlowin) in [#3539](https://github.com/PrefectHQ/fastmcp/pull/3539)
+* Support ImageContent and AudioContent in sampling handlers by [@jlowin](https://github.com/jlowin) in [#3550](https://github.com/PrefectHQ/fastmcp/pull/3550)
+* Graceful degradation for multi-server proxy setup by [@jlowin](https://github.com/jlowin) in [#3546](https://github.com/PrefectHQ/fastmcp/pull/3546)
+* Extract TokenCache utility, add caching to GitHubTokenVerifier by [@jlowin](https://github.com/jlowin) in [#3547](https://github.com/PrefectHQ/fastmcp/pull/3547)
+* Add review-pr skill for Codex bot workflow by [@jlowin](https://github.com/jlowin) in [#3552](https://github.com/PrefectHQ/fastmcp/pull/3552)
+* Add MCP message inspector to dev apps UI by [@jlowin](https://github.com/jlowin) in [#3570](https://github.com/PrefectHQ/fastmcp/pull/3570)
+* Comprehensive MCP Apps docs, string CallTool resolution by [@jlowin](https://github.com/jlowin) in [#3575](https://github.com/PrefectHQ/fastmcp/pull/3575)
+* Replace UUID global keys with (app_name, tool_name) registry by [@jlowin](https://github.com/jlowin) in [#3585](https://github.com/PrefectHQ/fastmcp/pull/3585)
+* Route app tool calls through provider chain by [@jlowin](https://github.com/jlowin) in [#3587](https://github.com/PrefectHQ/fastmcp/pull/3587)
+* Dev apps: show more/less for long tool descriptions by [@jlowin](https://github.com/jlowin) in [#3600](https://github.com/PrefectHQ/fastmcp/pull/3600)
+* Apps Phase 1: docs, examples, app-only tool filtering by [@jlowin](https://github.com/jlowin) in [#3593](https://github.com/PrefectHQ/fastmcp/pull/3593)
+* Forward enable_cimd to OAuthProxy in all provider subclasses by [@jlowin](https://github.com/jlowin) in [#3608](https://github.com/PrefectHQ/fastmcp/pull/3608)
+* Tune too-long triage heuristic by [@jlowin](https://github.com/jlowin) in [#3610](https://github.com/PrefectHQ/fastmcp/pull/3610)
+* Update ty ignore comments for 0.0.25 compatibility by [@jlowin](https://github.com/jlowin) in [#3614](https://github.com/PrefectHQ/fastmcp/pull/3614)
+* Move app modules to fastmcp.apps package by [@jlowin](https://github.com/jlowin) in [#3616](https://github.com/PrefectHQ/fastmcp/pull/3616)
+* Tighten too-long heuristic for design-document issues by [@jlowin](https://github.com/jlowin) in [#3620](https://github.com/PrefectHQ/fastmcp/pull/3620)
+* Run MCP conformance tests by [@strawgate](https://github.com/strawgate) in [#3628](https://github.com/PrefectHQ/fastmcp/pull/3628)
+* Add PrefabAppConfig for customizable Prefab tool setup by [@jlowin](https://github.com/jlowin) in [#3648](https://github.com/PrefectHQ/fastmcp/pull/3648)
+* Clean error when dev apps ports are in use by [@jlowin](https://github.com/jlowin) in [#3658](https://github.com/PrefectHQ/fastmcp/pull/3658)
+* Add Clerk OAuth provider by [@mostafa6765](https://github.com/mostafa6765) in [#3677](https://github.com/PrefectHQ/fastmcp/pull/3677)
+* Add interactive map example with geocoding by [@jlowin](https://github.com/jlowin) in [#3702](https://github.com/PrefectHQ/fastmcp/pull/3702)
+* Bump pydantic-monty to 0.0.9 by [@jlowin](https://github.com/jlowin) in [#3707](https://github.com/PrefectHQ/fastmcp/pull/3707)
+* Add forward_resource flag to OAuthProxy by [@jlowin](https://github.com/jlowin) in [#3711](https://github.com/PrefectHQ/fastmcp/pull/3711)
+### Security 🔒
+* fix: enforce per-tool auth checks in sampling tool wrapper by [@jlowin](https://github.com/jlowin) in [#3494](https://github.com/PrefectHQ/fastmcp/pull/3494)
+* fix: handle re.error from malformed URI templates by [@jlowin](https://github.com/jlowin) in [#3501](https://github.com/PrefectHQ/fastmcp/pull/3501)
+* fix: reject empty/OIDC-only required_scopes in AzureProvider by [@jlowin](https://github.com/jlowin) in [#3503](https://github.com/PrefectHQ/fastmcp/pull/3503)
+* fix: restrict $ref resolution to local refs only (SSRF/LFI) by [@jlowin](https://github.com/jlowin) in [#3502](https://github.com/PrefectHQ/fastmcp/pull/3502)
+* fix: URL-encode path params to prevent SSRF/path traversal (GHSA-vv7q-7jx5-f767) by [@jlowin](https://github.com/jlowin) in [#3507](https://github.com/PrefectHQ/fastmcp/pull/3507)
+* fix: prevent path traversal in skill download by [@jlowin](https://github.com/jlowin) in [#3493](https://github.com/PrefectHQ/fastmcp/pull/3493)
+* fix: prefer IdP-granted scopes over client-requested scopes in OAuthProxy by [@jlowin](https://github.com/jlowin) in [#3492](https://github.com/PrefectHQ/fastmcp/pull/3492)
+* fix: remove forced follow_redirects from httpx_client_factory calls by [@jlowin](https://github.com/jlowin) in [#3496](https://github.com/PrefectHQ/fastmcp/pull/3496)
+* Bump PyJWT >= 2.12.0 (CVE-2026-32597) by [@jlowin](https://github.com/jlowin) in [#3515](https://github.com/PrefectHQ/fastmcp/pull/3515)
+* Drop diskcache from examples/testing_demo lockfile (CVE-2025-69872) by [@jlowin](https://github.com/jlowin) in [#3518](https://github.com/PrefectHQ/fastmcp/pull/3518)
+* fix: CSRF double-submit cookie check in consent flow by [@jlowin](https://github.com/jlowin) in [#3519](https://github.com/PrefectHQ/fastmcp/pull/3519)
+* fix: validate server names in install commands by [@jlowin](https://github.com/jlowin) in [#3522](https://github.com/PrefectHQ/fastmcp/pull/3522)
+* fix: reject refresh tokens used as Bearer access tokens by [@jlowin](https://github.com/jlowin) in [#3524](https://github.com/PrefectHQ/fastmcp/pull/3524)
+* fix: route ResourcesAsTools/PromptsAsTools through server middleware by [@jlowin](https://github.com/jlowin) in [#3495](https://github.com/PrefectHQ/fastmcp/pull/3495)
+### Fixes 🐞
+* Update docs banner and fix mobile layout by [@jlowin](https://github.com/jlowin) in [#3370](https://github.com/PrefectHQ/fastmcp/pull/3370)
+* Remove form-action from consent CSP, forward consent_csp_policy in providers by [@jlowin](https://github.com/jlowin) in [#3372](https://github.com/PrefectHQ/fastmcp/pull/3372)
+* Fix resource templates with query params on mounted servers by [@jlowin](https://github.com/jlowin) in [#3373](https://github.com/PrefectHQ/fastmcp/pull/3373)
+* Increase uv transport test timeout for CI cold starts by [@jlowin](https://github.com/jlowin) in [#3376](https://github.com/PrefectHQ/fastmcp/pull/3376)
+* Fix stale catalog in CodeMode execute by [@jlowin](https://github.com/jlowin) in [#3375](https://github.com/PrefectHQ/fastmcp/pull/3375)
+* Deduplicate versioned tools in CatalogTransform catalog by [@jlowin](https://github.com/jlowin) in [#3374](https://github.com/PrefectHQ/fastmcp/pull/3374)
+* Fix ty 0.0.20 compatibility by [@jlowin](https://github.com/jlowin) in [#3377](https://github.com/PrefectHQ/fastmcp/pull/3377)
+* Forward scopes_supported through RemoteAuthProvider subclasses by [@jlowin](https://github.com/jlowin) in [#3388](https://github.com/PrefectHQ/fastmcp/pull/3388)
+* Enforce token scopes in WorkOS verifier to prevent scope bypass by [@jlowin](https://github.com/jlowin) in [#3407](https://github.com/PrefectHQ/fastmcp/pull/3407)
+* Bind Discord token verification to configured client_id by [@jlowin](https://github.com/jlowin) in [#3405](https://github.com/PrefectHQ/fastmcp/pull/3405)
+* Return after `McpError` in initialization middleware to prevent fallthrough by [@jlowin](https://github.com/jlowin) in [#3413](https://github.com/PrefectHQ/fastmcp/pull/3413)
+* Escape client_id in OAuth consent advanced details by [@jlowin](https://github.com/jlowin) in [#3418](https://github.com/PrefectHQ/fastmcp/pull/3418)
+* Bound client auto-pagination loops to prevent unbounded list fetches by [@jlowin](https://github.com/jlowin) in [#3411](https://github.com/PrefectHQ/fastmcp/pull/3411)
+* Raise ValueError for invalid boolean query params in resource templates by [@jlowin](https://github.com/jlowin) in [#3434](https://github.com/PrefectHQ/fastmcp/pull/3434)
+* Validate workspace path is a directory in cursor install by [@jlowin](https://github.com/jlowin) in [#3435](https://github.com/PrefectHQ/fastmcp/pull/3435)
+* Validate version metadata to reject non-scalar types by [@jlowin](https://github.com/jlowin) in [#3437](https://github.com/PrefectHQ/fastmcp/pull/3437)
+* Bind AWS Cognito token verification to configured app client by [@jlowin](https://github.com/jlowin) in [#3406](https://github.com/PrefectHQ/fastmcp/pull/3406)
+* Avoid stale context leakage when proxying with an already‑connected ProxyClient by [@jlowin](https://github.com/jlowin) in [#3408](https://github.com/PrefectHQ/fastmcp/pull/3408)
+* Prevent skills manifests from hashing files outside the skill directory by [@jlowin](https://github.com/jlowin) in [#3410](https://github.com/PrefectHQ/fastmcp/pull/3410)
+* Harden fastmcp metadata parsing in proxy paths by [@jlowin](https://github.com/jlowin) in [#3412](https://github.com/PrefectHQ/fastmcp/pull/3412)
+* Re-hash response caching keys to avoid persisting raw request input by [@jlowin](https://github.com/jlowin) in [#3414](https://github.com/PrefectHQ/fastmcp/pull/3414)
+* Handle Windows npx detection when npx.cmd is missing by [@jlowin](https://github.com/jlowin) in [#3416](https://github.com/PrefectHQ/fastmcp/pull/3416)
+* Guard OAuth callback result from post-completion overwrites by [@jlowin](https://github.com/jlowin) in [#3417](https://github.com/PrefectHQ/fastmcp/pull/3417)
+* Fix tool argument rename collisions with passthrough params by [@jlowin](https://github.com/jlowin) in [#3431](https://github.com/PrefectHQ/fastmcp/pull/3431)
+* Guard default progress handler against total=0 notifications by [@jlowin](https://github.com/jlowin) in [#3432](https://github.com/PrefectHQ/fastmcp/pull/3432)
+* Fix get_* returning None when latest version is disabled by [@jlowin](https://github.com/jlowin) in [#3439](https://github.com/PrefectHQ/fastmcp/pull/3439)
+* Fix server lifespan overlap teardown by [@jlowin](https://github.com/jlowin) in [#3415](https://github.com/PrefectHQ/fastmcp/pull/3415)
+* Fix $ref output schema object detection regression by [@jlowin](https://github.com/jlowin) in [#3420](https://github.com/PrefectHQ/fastmcp/pull/3420)
+* Preserve kw-only defaults when rebuilding functions for resolved annotations by [@jlowin](https://github.com/jlowin) in [#3429](https://github.com/PrefectHQ/fastmcp/pull/3429)
+* Redact sensitive headers in OpenAPI provider debug logging by [@jlowin](https://github.com/jlowin) in [#3436](https://github.com/PrefectHQ/fastmcp/pull/3436)
+* Fix async partial callables rejected by iscoroutinefunction by [@jlowin](https://github.com/jlowin) in [#3438](https://github.com/PrefectHQ/fastmcp/pull/3438)
+* Block insecure HS* JWT verification with JWKS/public keys by [@jlowin](https://github.com/jlowin) in [#3430](https://github.com/PrefectHQ/fastmcp/pull/3430)
+* Sanitize untrusted output in `fastmcp list` and `fastmcp call` by [@jlowin](https://github.com/jlowin) in [#3409](https://github.com/PrefectHQ/fastmcp/pull/3409)
+* fix: propagate `version` to components in FileSystemProvider by [@martimfasantos](https://github.com/martimfasantos) in [#3458](https://github.com/PrefectHQ/fastmcp/pull/3458)
+* fix: use intent-based flag for OIDC scope patch in load_access_token by [@voidborne-d](https://github.com/voidborne-d) in [#3465](https://github.com/PrefectHQ/fastmcp/pull/3465)
+* Set readOnlyHint=True on ResourcesAsTools generated tools by [@jlowin](https://github.com/jlowin) in [#3476](https://github.com/PrefectHQ/fastmcp/pull/3476)
+* fix: normalize Google scope shorthands and surface valid_scopes by [@jlowin](https://github.com/jlowin) in [#3477](https://github.com/PrefectHQ/fastmcp/pull/3477)
+* fix: resolve ty 0.0.23 type-checking errors by [@jlowin](https://github.com/jlowin) in [#3481](https://github.com/PrefectHQ/fastmcp/pull/3481)
+* fix: shield lifespan teardown from cancellation by [@jlowin](https://github.com/jlowin) in [#3480](https://github.com/PrefectHQ/fastmcp/pull/3480)
+* fix: forward custom_route endpoints from mounted servers by [@voidborne-d](https://github.com/voidborne-d) in [#3462](https://github.com/PrefectHQ/fastmcp/pull/3462)
+* fix: use dynamic version in CLI help text instead of hardcoded 2.0 by [@saschabuehrle](https://github.com/saschabuehrle) in [#3456](https://github.com/PrefectHQ/fastmcp/pull/3456)
+* Fix Monty 0.0.8 compatibility by [@hkc5](https://github.com/hkc5) in [#3468](https://github.com/PrefectHQ/fastmcp/pull/3468)
+* Fix task test teardown hanging 5s per test by [@jlowin](https://github.com/jlowin) in [#3499](https://github.com/PrefectHQ/fastmcp/pull/3499)
+* fix: validate workspace path is a directory before cursor install by [@nightcityblade](https://github.com/nightcityblade) in [#3440](https://github.com/PrefectHQ/fastmcp/pull/3440)
+* Treat `refresh_expires_in=0` as missing, fall back to 30-day default by [@jlowin](https://github.com/jlowin) in [#3514](https://github.com/PrefectHQ/fastmcp/pull/3514)
+* fix: use raw strings for regex in pytest.raises match by [@jlowin](https://github.com/jlowin) in [#3523](https://github.com/PrefectHQ/fastmcp/pull/3523)
+* fix: resolve Pyright "Module is not callable" on @tool, @resource, @prompt decorators by [@jlowin](https://github.com/jlowin) in [#3540](https://github.com/PrefectHQ/fastmcp/pull/3540)
+* fix: flaky KEY_PREFIX warning test in lowest-direct deps by [@jlowin](https://github.com/jlowin) in [#3549](https://github.com/PrefectHQ/fastmcp/pull/3549)
+* fix: suppress output schema for ToolResult subclass annotations by [@jlowin](https://github.com/jlowin) in [#3548](https://github.com/PrefectHQ/fastmcp/pull/3548)
+* Bump anthropic minimum to 0.48.0 by [@jlowin](https://github.com/jlowin) in [#3553](https://github.com/PrefectHQ/fastmcp/pull/3553)
+* Update startup banner deploy URL to Prefect Horizon by [@zzstoatzz](https://github.com/zzstoatzz) in [#3557](https://github.com/PrefectHQ/fastmcp/pull/3557)
+* fix: increase sleep duration in proxy cache tests by [@strawgate](https://github.com/strawgate) in [#3567](https://github.com/PrefectHQ/fastmcp/pull/3567)
+* fix: store absolute token expiry to prevent stale expires_in on reload by [@jlowin](https://github.com/jlowin) in [#3572](https://github.com/PrefectHQ/fastmcp/pull/3572)
+* fix: preserve tool properties named 'title' during schema compression by [@jlowin](https://github.com/jlowin) in [#3582](https://github.com/PrefectHQ/fastmcp/pull/3582)
+* Add `encoding` parameter to `FileResource` by [@shulkx](https://github.com/shulkx) in [#3580](https://github.com/PrefectHQ/fastmcp/pull/3580)
+* Transparently refresh upstream token in OAuthProxy.load_access_token() by [@jlowin](https://github.com/jlowin) in [#3584](https://github.com/PrefectHQ/fastmcp/pull/3584)
+* Fix loopback redirect URI port matching per RFC 8252 §7.3 by [@radoshi](https://github.com/radoshi) in [#3589](https://github.com/PrefectHQ/fastmcp/pull/3589)
+* Fix app tool routing: visibility check and middleware propagation by [@jlowin](https://github.com/jlowin) in [#3591](https://github.com/PrefectHQ/fastmcp/pull/3591)
+* Fix query parameter serialization to respect OpenAPI explode setting by [@jlowin](https://github.com/jlowin) in [#3595](https://github.com/PrefectHQ/fastmcp/pull/3595)
+* Fix dev apps form: union types, textarea support, JSON parsing by [@jlowin](https://github.com/jlowin) in [#3597](https://github.com/PrefectHQ/fastmcp/pull/3597)
+* Respect OpenAPI content type in request body serialization by [@jlowin](https://github.com/jlowin) in [#3611](https://github.com/PrefectHQ/fastmcp/pull/3611)
+* fix(google): replace deprecated /oauth2/v1/tokeninfo with /oauth2/v3/userinfo by [@shigechika](https://github.com/shigechika) in [#3603](https://github.com/PrefectHQ/fastmcp/pull/3603)
+* fix: resolve EntraOBOToken dependency injection through MultiAuth by [@jer805](https://github.com/jer805) in [#3609](https://github.com/PrefectHQ/fastmcp/pull/3609)
+* fix: filesystem provider import machinery by [@strawgate](https://github.com/strawgate) in [#3626](https://github.com/PrefectHQ/fastmcp/pull/3626)
+* fix: recover StdioTransport after subprocess exits by [@strawgate](https://github.com/strawgate) in [#3630](https://github.com/PrefectHQ/fastmcp/pull/3630)
+* fix(server): preserve mounted tool task metadata by [@pandego](https://github.com/pandego) in [#3632](https://github.com/PrefectHQ/fastmcp/pull/3632)
+* fix: scope deprecation warning filter to FastMCPDeprecationWarning by [@jlowin](https://github.com/jlowin) in [#3649](https://github.com/PrefectHQ/fastmcp/pull/3649)
+* fix: resolve CurrentFastMCP/ctx.fastmcp to child server in mounted background tasks by [@jlowin](https://github.com/jlowin) in [#3651](https://github.com/PrefectHQ/fastmcp/pull/3651)
+* Fix blocking docs issues: chart imports, Select API, Rx consistency by [@jlowin](https://github.com/jlowin) in [#3652](https://github.com/PrefectHQ/fastmcp/pull/3652)
+* Fix prompt caching round-trip on cache miss by [@strawgate](https://github.com/strawgate) in [#3666](https://github.com/PrefectHQ/fastmcp/pull/3666)
+* fix: serialize object query params per OpenAPI style/explode rules by [@4444J99](https://github.com/4444J99) in [#3662](https://github.com/PrefectHQ/fastmcp/pull/3662)
+* fix: HTTP request headers not accessible in background task workers by [@pandego](https://github.com/pandego) in [#3631](https://github.com/PrefectHQ/fastmcp/pull/3631)
+* fix: restore HTTP headers in worker execution path for background tasks by [@jlowin](https://github.com/jlowin) in [#3681](https://github.com/PrefectHQ/fastmcp/pull/3681)
+* fix: strip discriminator after dereferencing schemas by [@jlowin](https://github.com/jlowin) in [#3682](https://github.com/PrefectHQ/fastmcp/pull/3682)
+* fix: remove stale ty:ignore directives for ty 0.0.26 by [@jlowin](https://github.com/jlowin) in [#3684](https://github.com/PrefectHQ/fastmcp/pull/3684)
+* fix: dev apps log panel UX improvements by [@jlowin](https://github.com/jlowin) in [#3698](https://github.com/PrefectHQ/fastmcp/pull/3698)
+* Add quiz example app, fix dev server empty string args by [@jlowin](https://github.com/jlowin) in [#3700](https://github.com/PrefectHQ/fastmcp/pull/3700)
+### Docs 📚
+* Add early-development warning to Prefab docs by [@jlowin](https://github.com/jlowin) in [#3362](https://github.com/PrefectHQ/fastmcp/pull/3362)
+* Add tag to docs by [@jlowin](https://github.com/jlowin) in [#3382](https://github.com/PrefectHQ/fastmcp/pull/3382)
+* Add settings and environment variables reference by [@jlowin](https://github.com/jlowin) in [#3384](https://github.com/PrefectHQ/fastmcp/pull/3384)
+* Add contributing guidelines and update issue/PR templates by [@jlowin](https://github.com/jlowin) in [#3485](https://github.com/PrefectHQ/fastmcp/pull/3485)
+* [Documentation] Move stateless_http transport kwarg to http_app as FastMCP constructo… by [@mhallo](https://github.com/mhallo) in [#3510](https://github.com/PrefectHQ/fastmcp/pull/3510)
+* Update security policy by [@jlowin](https://github.com/jlowin) in [#3521](https://github.com/PrefectHQ/fastmcp/pull/3521)
+* Add release instructions to CLAUDE.md by [@jlowin](https://github.com/jlowin) in [#3583](https://github.com/PrefectHQ/fastmcp/pull/3583)
+* fix(docs): correct misleading stateless_http header by [@jlowin](https://github.com/jlowin) in [#3622](https://github.com/PrefectHQ/fastmcp/pull/3622)
+* Add tag to deployment pages by [@jlowin](https://github.com/jlowin) in [#3624](https://github.com/PrefectHQ/fastmcp/pull/3624)
+* Docs: generative UI page, fix imports, add PrefabAppConfig by [@jlowin](https://github.com/jlowin) in [#3650](https://github.com/PrefectHQ/fastmcp/pull/3650)
+* docs: improve contributor guidelines for framework contributions by [@jlowin](https://github.com/jlowin) in [#3653](https://github.com/PrefectHQ/fastmcp/pull/3653)
+* Add release notes for v3.1.0, v3.1.1, and v2.14.6 by [@jlowin](https://github.com/jlowin) in [#3659](https://github.com/PrefectHQ/fastmcp/pull/3659)
+* Docs: showcase hero, narrative improvements, panel closed by default by [@jlowin](https://github.com/jlowin) in [#3657](https://github.com/PrefectHQ/fastmcp/pull/3657)
+* Docs: add FileTreeStore sanitization warnings and update examples by [@strawgate](https://github.com/strawgate) in [#3661](https://github.com/PrefectHQ/fastmcp/pull/3661)
+* Add prefab-ui version pinning warning to docs by [@jlowin](https://github.com/jlowin) in [#3688](https://github.com/PrefectHQ/fastmcp/pull/3688)
+* Reorganize apps overview TOC by [@jlowin](https://github.com/jlowin) in [#3689](https://github.com/PrefectHQ/fastmcp/pull/3689)
+* Fix docs gaps in app provider pages by [@jlowin](https://github.com/jlowin) in [#3690](https://github.com/PrefectHQ/fastmcp/pull/3690)
+* Polish apps docs for 3.2 release by [@jlowin](https://github.com/jlowin) in [#3693](https://github.com/PrefectHQ/fastmcp/pull/3693)
+* Add apps quickstart tutorial by [@jlowin](https://github.com/jlowin) in [#3695](https://github.com/PrefectHQ/fastmcp/pull/3695)
+* Improve quickstart: pie chart, interactive row selection, screenshots by [@jlowin](https://github.com/jlowin) in [#3699](https://github.com/PrefectHQ/fastmcp/pull/3699)
+* Add sales dashboard and live system monitor examples, bump prefab-ui to 0.17 by [@jlowin](https://github.com/jlowin) in [#3696](https://github.com/PrefectHQ/fastmcp/pull/3696)
+* Add examples gallery page by [@jlowin](https://github.com/jlowin) in [#3705](https://github.com/PrefectHQ/fastmcp/pull/3705)
+* docs: note that custom routes are unauthenticated by [@jlowin](https://github.com/jlowin) in [#3706](https://github.com/PrefectHQ/fastmcp/pull/3706)
+* Remove hardcoded prefab-ui version from pinning warnings by [@jlowin](https://github.com/jlowin) in [#3708](https://github.com/PrefectHQ/fastmcp/pull/3708)
+### Examples & Contrib 💡
+* Block recursive self-invocation in BulkToolCaller by [@jlowin](https://github.com/jlowin) in [#3433](https://github.com/PrefectHQ/fastmcp/pull/3433)
+### Dependencies 📦
+* Bump authlib from 1.6.6 to 1.6.7 in /examples/testing_demo in the uv group across 1 directory by [@dependabot](https://github.com/dependabot) in [#3390](https://github.com/PrefectHQ/fastmcp/pull/3390)
+* Bump actions/create-github-app-token from 2 to 3 by [@dependabot](https://github.com/dependabot) in [#3511](https://github.com/PrefectHQ/fastmcp/pull/3511)
+* chore(deps): bump pyasn1 from 0.6.2 to 0.6.3 in the uv group across 1 directory by [@dependabot](https://github.com/dependabot) in [#3538](https://github.com/PrefectHQ/fastmcp/pull/3538)
+* chore(deps): bump j178/prek-action from 1 to 2 by [@dependabot](https://github.com/dependabot) in [#3578](https://github.com/PrefectHQ/fastmcp/pull/3578)
+* chore(deps): bump requests from 2.32.5 to 2.33.0 in the uv group across 1 directory by [@dependabot](https://github.com/dependabot) in [#3638](https://github.com/PrefectHQ/fastmcp/pull/3638)
+* chore(deps): bump cryptography from 46.0.5 to 46.0.6 in /examples/testing_demo in the uv group across 1 directory by [@dependabot](https://github.com/dependabot) in [#3685](https://github.com/PrefectHQ/fastmcp/pull/3685)
+* chore(deps): bump actions/setup-node from 4 to 6 by [@dependabot](https://github.com/dependabot) in [#3691](https://github.com/PrefectHQ/fastmcp/pull/3691)
+
+## New Contributors
+* @Sumanshu-Nankana made their first contribution in [#3380](https://github.com/PrefectHQ/fastmcp/pull/3380)
+* @ericrobinson-indeed made their first contribution in [#3396](https://github.com/PrefectHQ/fastmcp/pull/3396)
+* @voidborne-d made their first contribution in [#3465](https://github.com/PrefectHQ/fastmcp/pull/3465)
+* @mtthidoteu made their first contribution in [#3473](https://github.com/PrefectHQ/fastmcp/pull/3473)
+* @saschabuehrle made their first contribution in [#3456](https://github.com/PrefectHQ/fastmcp/pull/3456)
+* @hkc5 made their first contribution in [#3468](https://github.com/PrefectHQ/fastmcp/pull/3468)
+* @nightcityblade made their first contribution in [#3440](https://github.com/PrefectHQ/fastmcp/pull/3440)
+* @mhallo made their first contribution in [#3510](https://github.com/PrefectHQ/fastmcp/pull/3510)
+* @radoshi made their first contribution in [#3589](https://github.com/PrefectHQ/fastmcp/pull/3589)
+* @shigechika made their first contribution in [#3603](https://github.com/PrefectHQ/fastmcp/pull/3603)
+* @pandego made their first contribution in [#3632](https://github.com/PrefectHQ/fastmcp/pull/3632)
+* @4444J99 made their first contribution in [#3662](https://github.com/PrefectHQ/fastmcp/pull/3662)
+* @mostafa6765 made their first contribution in [#3677](https://github.com/PrefectHQ/fastmcp/pull/3677)
+
+**Full Changelog**: [v3.1.0...v3.2.0](https://github.com/PrefectHQ/fastmcp/compare/v3.1.0...v3.2.0)
+
+
+
+
+
+**[v3.1.1: 'Tis But a Patch](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.1.1)**
+
+Pins `pydantic-monty` below 0.0.8 to fix a breaking change in Monty that affects code mode. Monty 0.0.8 removed the `external_functions` constructor parameter, causing `MontySandboxProvider` to fail. This patch caps the version so existing installs work correctly.
+
+### Fixes 🐞
+* Pin pydantic-monty below 0.0.8 to fix code mode by [@jlowin](https://github.com/jlowin) in [#3497](https://github.com/PrefectHQ/fastmcp/pull/3497)
+
+**Full Changelog**: [v3.1.0...v3.1.1](https://github.com/PrefectHQ/fastmcp/compare/v3.1.0...v3.1.1)
+
+
+
+
+
+**[v3.1.0: Code to Joy](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.1.0)**
+
+FastMCP 3.1 is the Code Mode release. The 3.0 architecture introduced providers and transforms as the extensibility layer — 3.1 puts that architecture to work, shipping the most requested capability since launch: servers that can find and execute code on behalf of agents, without requiring clients to know what tools exist.
+
+### New Features 🎉
+* feat: Search transforms for tool discovery by [@jlowin](https://github.com/jlowin) in [#3154](https://github.com/PrefectHQ/fastmcp/pull/3154)
+* Add experimental CodeMode transform by [@aaazzam](https://github.com/aaazzam) in [#3297](https://github.com/PrefectHQ/fastmcp/pull/3297)
+* Add Prefab Apps integration for MCP tool UIs by [@jlowin](https://github.com/jlowin) in [#3316](https://github.com/PrefectHQ/fastmcp/pull/3316)
+### Enhancements 🔧
+* Lazy-load heavy imports to reduce import time by [@jlowin](https://github.com/jlowin) in [#3295](https://github.com/PrefectHQ/fastmcp/pull/3295)
+* Add http_client parameter to all token verifiers for connection pooling by [@jlowin](https://github.com/jlowin) in [#3300](https://github.com/PrefectHQ/fastmcp/pull/3300)
+* Add in-memory caching for token introspection results by [@jlowin](https://github.com/jlowin) in [#3298](https://github.com/PrefectHQ/fastmcp/pull/3298)
+* Add SessionStart hook to install gh CLI in cloud sessions by [@jlowin](https://github.com/jlowin) in [#3308](https://github.com/PrefectHQ/fastmcp/pull/3308)
+* Fix ty 0.0.19 type errors by [@jlowin](https://github.com/jlowin) in [#3310](https://github.com/PrefectHQ/fastmcp/pull/3310)
+* Code Mode: Add resource limits to MontySandboxProvider by [@jlowin](https://github.com/jlowin) in [#3326](https://github.com/PrefectHQ/fastmcp/pull/3326)
+* Accept transforms as FastMCP init kwarg by [@jlowin](https://github.com/jlowin) in [#3324](https://github.com/PrefectHQ/fastmcp/pull/3324)
+* Split large test files to comply with loq line limit by [@jlowin](https://github.com/jlowin) in [#3328](https://github.com/PrefectHQ/fastmcp/pull/3328)
+* Add -m/--module flag to `fastmcp run` and `dev inspector` by [@dgenio](https://github.com/dgenio) in [#3331](https://github.com/PrefectHQ/fastmcp/pull/3331)
+* Add search_result_serializer hook and serialize_tools_for_output_markdown by [@MagnusS0](https://github.com/MagnusS0) in [#3337](https://github.com/PrefectHQ/fastmcp/pull/3337)
+* Add MultiAuth for composing multiple token verification sources by [@jlowin](https://github.com/jlowin) in [#3335](https://github.com/PrefectHQ/fastmcp/pull/3335)
+* Adds PropelAuth as an AuthProvider by [@andrew-propelauth](https://github.com/andrew-propelauth) in [#3358](https://github.com/PrefectHQ/fastmcp/pull/3358)
+* Replace vendored DI with uncalled-for by [@chrisguidry](https://github.com/chrisguidry) in [#3301](https://github.com/PrefectHQ/fastmcp/pull/3301)
+* Decompose CodeMode into composable discovery tools by [@jlowin](https://github.com/jlowin) in [#3354](https://github.com/PrefectHQ/fastmcp/pull/3354)
+* feat(contrib): auto-sync MCPMixin decorators with from_function signatures by [@AnkeshThakur](https://github.com/AnkeshThakur) in [#3323](https://github.com/PrefectHQ/fastmcp/pull/3323)
+* Add Google GenAI Sampling Handler by [@strawgate](https://github.com/strawgate) in [#2977](https://github.com/PrefectHQ/fastmcp/pull/2977)
+* Add ListTools, search limit, and catalog size annotation to CodeMode by [@jlowin](https://github.com/jlowin) in [#3359](https://github.com/PrefectHQ/fastmcp/pull/3359)
+* Allow configuring FastMCP transport setting in the same way as other configuration by [@jvdmr](https://github.com/jvdmr) in [#1796](https://github.com/PrefectHQ/fastmcp/pull/1796)
+* Add include_unversioned option to VersionFilter by [@yangbaechu](https://github.com/yangbaechu) in [#3349](https://github.com/PrefectHQ/fastmcp/pull/3349)
+### Fixes 🐞
+* Fix docs banner pushing nav down by [@jlowin](https://github.com/jlowin) in [#3282](https://github.com/PrefectHQ/fastmcp/pull/3282)
+* fix: Replace hardcoded TTL with DEFAULT_TTL_MS - issue #3279 by [@cedric57](https://github.com/cedric57) in [#3280](https://github.com/PrefectHQ/fastmcp/pull/3280)
+* fix: stop suppressing server stderr in fastmcp call by [@jlowin](https://github.com/jlowin) in [#3283](https://github.com/PrefectHQ/fastmcp/pull/3283)
+* fix: skip max_completion_tokens when maxTokens is None by [@eon01](https://github.com/eon01) in [#3284](https://github.com/PrefectHQ/fastmcp/pull/3284)
+* OpenAPI: rewrite $ref under propertyNames and patternProperties in _replace_ref_with_defs; add regression test for dict[StrEnum, Model] by [@manojPal23234](https://github.com/manojPal23234) in [#3306](https://github.com/PrefectHQ/fastmcp/pull/3306)
+* Remove stale add_resource() key parameter from docs by [@jlowin](https://github.com/jlowin) in [#3309](https://github.com/PrefectHQ/fastmcp/pull/3309)
+* Handle AuthorizationError as exclusion in AuthMiddleware list hooks by [@yangbaechu](https://github.com/yangbaechu) in [#3338](https://github.com/PrefectHQ/fastmcp/pull/3338)
+* Fix flaky OpenAPI performance test threshold by [@jlowin](https://github.com/jlowin) in [#3355](https://github.com/PrefectHQ/fastmcp/pull/3355)
+* Fix flaky SSE timeout test by [@jlowin](https://github.com/jlowin) in [#3343](https://github.com/PrefectHQ/fastmcp/pull/3343)
+* Remove system role references from docs by [@jlowin](https://github.com/jlowin) in [#3356](https://github.com/PrefectHQ/fastmcp/pull/3356)
+* Fix session persistence across tool calls in multi-server MCPConfigTransport by [@jer805](https://github.com/jer805) in [#3330](https://github.com/PrefectHQ/fastmcp/pull/3330)
+### Docs 📚
+* Add v3.0.2 release notes by [@jlowin](https://github.com/jlowin) in [#3276](https://github.com/PrefectHQ/fastmcp/pull/3276)
+* Fix "FastMCP Constructor Parameters" in documentation server.mdx (Remove old parameters & Add new parameter) by [@wangyy04](https://github.com/wangyy04) in [#3317](https://github.com/PrefectHQ/fastmcp/pull/3317)
+* Fix stale docs: tag filtering API and missing output_schema param by [@jlowin](https://github.com/jlowin) in [#3322](https://github.com/PrefectHQ/fastmcp/pull/3322)
+* Narrate search example clients by [@jlowin](https://github.com/jlowin) in [#3321](https://github.com/PrefectHQ/fastmcp/pull/3321)
+* Code Mode: Document resource limits and fix docs formatting by [@jlowin](https://github.com/jlowin) in [#3327](https://github.com/PrefectHQ/fastmcp/pull/3327)
+* Add reverse proxy (nginx) section to HTTP deployment docs by [@dgenio](https://github.com/dgenio) in [#3344](https://github.com/PrefectHQ/fastmcp/pull/3344)
+* Restructure docs navigation: CLI section, Composition, More by [@jlowin](https://github.com/jlowin) in [#3361](https://github.com/PrefectHQ/fastmcp/pull/3361)
+### Other Changes 🦾
+* Don't advertise sampling.tools capability by default by [@jlowin](https://github.com/jlowin) in [#3334](https://github.com/PrefectHQ/fastmcp/pull/3334)
+
+## New Contributors
+* @cedric57 made their first contribution in [#3280](https://github.com/PrefectHQ/fastmcp/pull/3280)
+* @eon01 made their first contribution in [#3284](https://github.com/PrefectHQ/fastmcp/pull/3284)
+* @manojPal23234 made their first contribution in [#3306](https://github.com/PrefectHQ/fastmcp/pull/3306)
+* @wangyy04 made their first contribution in [#3317](https://github.com/PrefectHQ/fastmcp/pull/3317)
+* @yangbaechu made their first contribution in [#3338](https://github.com/PrefectHQ/fastmcp/pull/3338)
+* @andrew-propelauth made their first contribution in [#3358](https://github.com/PrefectHQ/fastmcp/pull/3358)
+* @jer805 made their first contribution in [#3330](https://github.com/PrefectHQ/fastmcp/pull/3330)
+* @jvdmr made their first contribution in [#1796](https://github.com/PrefectHQ/fastmcp/pull/1796)
+
+**Full Changelog**: [v3.0.2...v3.1.0](https://github.com/PrefectHQ/fastmcp/compare/v3.0.2...v3.1.0)
+
+
+
+
+
+**[v3.0.2: Threecovery Mode II](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.2)**
+
+Two community-contributed fixes: auth headers from MCP transport no longer leak through to downstream OpenAPI APIs, and background task workers now correctly receive the originating request ID. Plus a new docs example for context-aware tool factories.
+
+### Fixes 🐞
+* fix: prevent MCP transport auth header from leaking to downstream OpenAPI APIs by [@stakeswky](https://github.com/stakeswky) in [#3262](https://github.com/PrefectHQ/fastmcp/pull/3262)
+* fix: propagate origin_request_id to background task workers by [@gfortaine](https://github.com/gfortaine) in [#3175](https://github.com/PrefectHQ/fastmcp/pull/3175)
+### Docs 📚
+* Add v3.0.1 release notes by [@jlowin](https://github.com/jlowin) in [#3259](https://github.com/PrefectHQ/fastmcp/pull/3259)
+* docs: add context-aware tool factory example by [@machov](https://github.com/machov) in [#3264](https://github.com/PrefectHQ/fastmcp/pull/3264)
+
+**Full Changelog**: [v3.0.1...v3.0.2](https://github.com/PrefectHQ/fastmcp/compare/v3.0.1...v3.0.2)
+
+
+
+
+
+**[v3.0.1: Three-covery Mode](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.1)**
+
+First patch after 3.0 — mostly smoothing out rough edges discovered in the wild. The big ones: middleware state that wasn't surviving the trip to tool handlers now does, `Tool.from_tool()` accepts callables again, OpenAPI schemas with circular references no longer crash discovery, and decorator overloads now return the correct types in function mode. Also adds `verify_id_token` to OIDCProxy for providers (like some Azure AD configs) that issue opaque access tokens but standard JWT id_tokens.
+
+### Enhancements 🔧
+* Add verify_id_token option to OIDCProxy by [@jlowin](https://github.com/jlowin) in [#3248](https://github.com/PrefectHQ/fastmcp/pull/3248)
+### Fixes 🐞
+* Fix v3.0.0 changelog compare link by [@jlowin](https://github.com/jlowin) in [#3223](https://github.com/PrefectHQ/fastmcp/pull/3223)
+* Fix MDX parse error in upgrade guide prompts by [@jlowin](https://github.com/jlowin) in [#3227](https://github.com/PrefectHQ/fastmcp/pull/3227)
+* Fix non-serializable state lost between middleware and tools by [@jlowin](https://github.com/jlowin) in [#3234](https://github.com/PrefectHQ/fastmcp/pull/3234)
+* Accept callables in Tool.from_tool() by [@jlowin](https://github.com/jlowin) in [#3235](https://github.com/PrefectHQ/fastmcp/pull/3235)
+* Preserve skill metadata through provider wrapping by [@jlowin](https://github.com/jlowin) in [#3237](https://github.com/PrefectHQ/fastmcp/pull/3237)
+* Fix circular reference crash in OpenAPI schemas by [@jlowin](https://github.com/jlowin) in [#3245](https://github.com/PrefectHQ/fastmcp/pull/3245)
+* Fix NameError with future annotations and Context/Depends parameters by [@jlowin](https://github.com/jlowin) in [#3243](https://github.com/PrefectHQ/fastmcp/pull/3243)
+* Fix ty ignore syntax in OpenAPI provider by [@jlowin](https://github.com/jlowin) in [#3253](https://github.com/PrefectHQ/fastmcp/pull/3253)
+* Use max_completion_tokens instead of deprecated max_tokens in OpenAI handler by [@jlowin](https://github.com/jlowin) in [#3254](https://github.com/PrefectHQ/fastmcp/pull/3254)
+* Fix ty compatibility with upgraded deps by [@jlowin](https://github.com/jlowin) in [#3257](https://github.com/PrefectHQ/fastmcp/pull/3257)
+* Fix decorator overload return types for function mode by [@jlowin](https://github.com/jlowin) in [#3258](https://github.com/PrefectHQ/fastmcp/pull/3258)
+
+
+### Docs 📚
+* Sync README with welcome.mdx, fix install count by [@jlowin](https://github.com/jlowin) in [#3224](https://github.com/PrefectHQ/fastmcp/pull/3224)
+* Document dict-to-Message prompt migration in upgrade guides by [@jlowin](https://github.com/jlowin) in [#3225](https://github.com/PrefectHQ/fastmcp/pull/3225)
+* Fix v2 upgrade guide: remove incorrect v1 import advice by [@jlowin](https://github.com/jlowin) in [#3226](https://github.com/PrefectHQ/fastmcp/pull/3226)
+* Animated banner by [@jlowin](https://github.com/jlowin) in [#3231](https://github.com/PrefectHQ/fastmcp/pull/3231)
+* Document mounted server state store isolation in upgrade guide by [@jlowin](https://github.com/jlowin) in [#3236](https://github.com/PrefectHQ/fastmcp/pull/3236)
+
+**Full Changelog**: [v3.0.0...v3.0.1](https://github.com/PrefectHQ/fastmcp/compare/v3.0.0...v3.0.1)
+
+
+
+
+
+**[v3.0.0: Three at Last](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.0)**
+
+FastMCP 3.0 is stable. Two betas, two release candidates, 21 new contributors, and more than 100,000 pre-release installs later — the architecture held up, the upgrade path was smooth, and we're shipping it.
+
+The surface API is largely unchanged — `@mcp.tool()` still works exactly as before. What changed is everything underneath: a provider/transform architecture that makes FastMCP extensible, observable, and composable in ways v2 couldn't support. If we did our jobs right, you'll barely notice the redesign. You'll just notice that more is possible.
+
+This is also the release where FastMCP moves from [jlowin/fastmcp](https://github.com/jlowin/fastmcp) to [PrefectHQ/fastmcp](https://github.com/PrefectHQ/fastmcp). GitHub forwards all links, PyPI is the same, imports are the same. A major version felt like the right moment to make it official.
+
+### Build servers from anything
+
+🔌 Components no longer have to live in one file with one server. `FileSystemProvider` discovers tools from directories with hot-reload. `OpenAPIProvider` wraps REST APIs. `ProxyProvider` proxies remote MCP servers. `SkillsProvider` delivers agent skills as resources. Write your own provider for whatever source makes sense. Compose multiple providers into one server, share one across many, or chain them with **transforms** that rename, namespace, filter, version, and secure components as they flow to clients. `ResourcesAsTools` and `PromptsAsTools` expose non-tool components to tool-only clients.
+
+### Ship to production
+
+🔐 Component versioning: serve `@tool(version="2.0")` alongside older versions from one codebase. Granular authorization on individual components with async auth checks, server-wide policies via `AuthMiddleware`, and scope-based access control. OAuth gets CIMD, Static Client Registration, Azure OBO via dependency injection, JWT audience validation, and confused-deputy protections. OpenTelemetry tracing with MCP semantic conventions. Response size limiting. Background tasks with distributed Redis notification and `ctx.elicit()` relay. Security fixes include dropping `diskcache` (CVE-2025-69872) and upgrading `python-multipart` and `protobuf` for additional CVEs.
+
+### Adapt per session
+
+💾 Session state persists across requests via `ctx.set_state()` / `ctx.get_state()`. `ctx.enable_components()` and `ctx.disable_components()` let servers adapt dynamically per client — show admin tools after authentication, progressively reveal capabilities, or scope access by role.
+
+### Develop faster
+
+⚡ `--reload` auto-restarts on file changes. Standalone decorators return the original function, so decorated tools stay callable in tests and non-MCP contexts. Sync functions auto-dispatch to a threadpool. Tool timeouts, MCP-compliant pagination, composable lifespans, `PingMiddleware` for keepalive, and concurrent tool execution when the LLM returns multiple calls in one response.
+
+### Use FastMCP as a CLI
+
+🖥️ `fastmcp list` and `fastmcp call` query and invoke tools on any server from a terminal. `fastmcp discover` scans your editor configs (Claude Desktop, Cursor, Goose, Gemini CLI) and finds configured servers by name. `fastmcp generate-cli` writes a standalone typed CLI where every tool is a subcommand. `fastmcp install` registers your server with Claude Desktop, Cursor, or Goose in one command.
+
+### Build apps (3.1 preview)
+
+📱 Spec-level support for MCP Apps is in: `ui://` resource scheme, typed UI metadata via `AppConfig`, extension negotiation, and runtime detection. The full Apps experience lands in 3.1.
+
+---
+
+If you hit 3.0 because you didn't pin your dependencies and something breaks — the [upgrade guides](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2) will get you sorted. We minimized breaking changes, but a major version is a major version.
+
+```bash
+pip install fastmcp -U
+```
+
+📖 [Documentation](https://gofastmcp.com)
+🚀 [Upgrade from FastMCP v2](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2)
+🔀 [Upgrade from MCP Python SDK](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk)
+
+## What's Changed
+### New Features 🎉
+* Refactor resource behavior and add meta support by [@jlowin](https://github.com/jlowin) in [#2611](https://github.com/PrefectHQ/fastmcp/pull/2611)
+* Refactor prompt behavior and add meta support by [@jlowin](https://github.com/jlowin) in [#2610](https://github.com/PrefectHQ/fastmcp/pull/2610)
+* feat: Provider abstraction for dynamic MCP components by [@jlowin](https://github.com/jlowin) in [#2622](https://github.com/PrefectHQ/fastmcp/pull/2622)
+* Unify component storage in LocalProvider by [@jlowin](https://github.com/jlowin) in [#2680](https://github.com/PrefectHQ/fastmcp/pull/2680)
+* Introduce ResourceResult as canonical resource return type by [@jlowin](https://github.com/jlowin) in [#2734](https://github.com/PrefectHQ/fastmcp/pull/2734)
+* Introduce Message and PromptResult as canonical prompt types by [@jlowin](https://github.com/jlowin) in [#2738](https://github.com/PrefectHQ/fastmcp/pull/2738)
+* Add --reload flag for auto-restart on file changes by [@jlowin](https://github.com/jlowin) in [#2816](https://github.com/PrefectHQ/fastmcp/pull/2816)
+* Add FileSystemProvider for filesystem-based component discovery by [@jlowin](https://github.com/jlowin) in [#2823](https://github.com/PrefectHQ/fastmcp/pull/2823)
+* Add standalone decorators and eliminate fastmcp.fs module by [@jlowin](https://github.com/jlowin) in [#2832](https://github.com/PrefectHQ/fastmcp/pull/2832)
+* Add authorization checks to components and servers by [@jlowin](https://github.com/jlowin) in [#2855](https://github.com/PrefectHQ/fastmcp/pull/2855)
+* Decorators return functions instead of component objects by [@jlowin](https://github.com/jlowin) in [#2856](https://github.com/PrefectHQ/fastmcp/pull/2856)
+* Add transform system for modifying components in provider chains by [@jlowin](https://github.com/jlowin) in [#2836](https://github.com/PrefectHQ/fastmcp/pull/2836)
+* Add OpenTelemetry tracing support by [@chrisguidry](https://github.com/chrisguidry) in [#2869](https://github.com/PrefectHQ/fastmcp/pull/2869)
+* Add component versioning and VersionFilter transform by [@jlowin](https://github.com/jlowin) in [#2894](https://github.com/PrefectHQ/fastmcp/pull/2894)
+* Add version discovery and calling a certain version for components by [@jlowin](https://github.com/jlowin) in [#2897](https://github.com/PrefectHQ/fastmcp/pull/2897)
+* Refactor visibility to mark-based enabled system by [@jlowin](https://github.com/jlowin) in [#2912](https://github.com/PrefectHQ/fastmcp/pull/2912)
+* Add session-specific visibility control via Context by [@jlowin](https://github.com/jlowin) in [#2917](https://github.com/PrefectHQ/fastmcp/pull/2917)
+* Add Skills Provider for exposing agent skills as MCP resources by [@jlowin](https://github.com/jlowin) in [#2944](https://github.com/PrefectHQ/fastmcp/pull/2944)
+* Add MCP Apps Phase 1 — SDK compatibility (SEP-1865) by [@jlowin](https://github.com/jlowin) in [#3009](https://github.com/PrefectHQ/fastmcp/pull/3009)
+* Add `fastmcp list` and `fastmcp call` CLI commands by [@jlowin](https://github.com/jlowin) in [#3054](https://github.com/PrefectHQ/fastmcp/pull/3054)
+* Add `fastmcp generate-cli` command by [@jlowin](https://github.com/jlowin) in [#3065](https://github.com/PrefectHQ/fastmcp/pull/3065)
+* Add CIMD (Client ID Metadata Document) support for OAuth by [@jlowin](https://github.com/jlowin) in [#2871](https://github.com/PrefectHQ/fastmcp/pull/2871)
+
+
+### Enhancements 🔧
+* Convert mounted servers to MountedProvider by [@jlowin](https://github.com/jlowin) in [#2635](https://github.com/PrefectHQ/fastmcp/pull/2635)
+* Simplify .key as computed property by [@jlowin](https://github.com/jlowin) in [#2648](https://github.com/PrefectHQ/fastmcp/pull/2648)
+* Refactor MountedProvider into FastMCPProvider + TransformingProvider by [@jlowin](https://github.com/jlowin) in [#2653](https://github.com/PrefectHQ/fastmcp/pull/2653)
+* Enable background task support for custom component subclasses by [@jlowin](https://github.com/jlowin) in [#2657](https://github.com/PrefectHQ/fastmcp/pull/2657)
+* Use CreateTaskResult for background task creation by [@jlowin](https://github.com/jlowin) in [#2660](https://github.com/PrefectHQ/fastmcp/pull/2660)
+* Refactor provider execution: components own their execution by [@jlowin](https://github.com/jlowin) in [#2663](https://github.com/PrefectHQ/fastmcp/pull/2663)
+* Add supports_tasks() method to replace string mode checks by [@jlowin](https://github.com/jlowin) in [#2664](https://github.com/PrefectHQ/fastmcp/pull/2664)
+* Replace type: ignore[attr-defined] with isinstance assertions in tests by [@jlowin](https://github.com/jlowin) in [#2665](https://github.com/PrefectHQ/fastmcp/pull/2665)
+* Add poll_interval to TaskConfig by [@jlowin](https://github.com/jlowin) in [#2666](https://github.com/PrefectHQ/fastmcp/pull/2666)
+* Refactor task module: rename protocol.py to requests.py and reduce redundancy by [@jlowin](https://github.com/jlowin) in [#2667](https://github.com/PrefectHQ/fastmcp/pull/2667)
+* Refactor FastMCPProxy into ProxyProvider by [@jlowin](https://github.com/jlowin) in [#2669](https://github.com/PrefectHQ/fastmcp/pull/2669)
+* Move OpenAPI to providers/openapi submodule by [@jlowin](https://github.com/jlowin) in [#2672](https://github.com/PrefectHQ/fastmcp/pull/2672)
+* Use ergonomic provider initialization pattern by [@jlowin](https://github.com/jlowin) in [#2675](https://github.com/PrefectHQ/fastmcp/pull/2675)
+* Fix ty 0.0.5 type errors by [@jlowin](https://github.com/jlowin) in [#2676](https://github.com/PrefectHQ/fastmcp/pull/2676)
+* Remove execution methods from Provider base class by [@jlowin](https://github.com/jlowin) in [#2681](https://github.com/PrefectHQ/fastmcp/pull/2681)
+* Add type-prefixed keys for globally unique component identification by [@jlowin](https://github.com/jlowin) in [#2704](https://github.com/PrefectHQ/fastmcp/pull/2704)
+* Consolidate notification system with unified API by [@jlowin](https://github.com/jlowin) in [#2710](https://github.com/PrefectHQ/fastmcp/pull/2710)
+* Parallelize provider operations by [@jlowin](https://github.com/jlowin) in [#2716](https://github.com/PrefectHQ/fastmcp/pull/2716)
+* Consolidate get_* and _list_* methods into single API by [@jlowin](https://github.com/jlowin) in [#2719](https://github.com/PrefectHQ/fastmcp/pull/2719)
+* Consolidate execution method chains into single public API by [@jlowin](https://github.com/jlowin) in [#2728](https://github.com/PrefectHQ/fastmcp/pull/2728)
+* Parallelize list_* calls in Provider.get_tasks() by [@jlowin](https://github.com/jlowin) in [#2731](https://github.com/PrefectHQ/fastmcp/pull/2731)
+* Consistent decorator-based MCP handler registration by [@jlowin](https://github.com/jlowin) in [#2732](https://github.com/PrefectHQ/fastmcp/pull/2732)
+* Make ToolResult a BaseModel for serialization support by [@jlowin](https://github.com/jlowin) in [#2736](https://github.com/PrefectHQ/fastmcp/pull/2736)
+* Align prompt handler with resource pattern by [@jlowin](https://github.com/jlowin) in [#2740](https://github.com/PrefectHQ/fastmcp/pull/2740)
+* Update classes to inherit from FastMCPBaseModel instead of BaseModel by [@jlowin](https://github.com/jlowin) in [#2739](https://github.com/PrefectHQ/fastmcp/pull/2739)
+* Add explicit task_meta parameter to FastMCP.call_tool() by [@jlowin](https://github.com/jlowin) in [#2749](https://github.com/PrefectHQ/fastmcp/pull/2749)
+* Add task_meta parameter to read_resource() for explicit task control by [@jlowin](https://github.com/jlowin) in [#2750](https://github.com/PrefectHQ/fastmcp/pull/2750)
+* Add task_meta to prompts and centralize fn_key enrichment by [@jlowin](https://github.com/jlowin) in [#2751](https://github.com/PrefectHQ/fastmcp/pull/2751)
+* Remove unused include_tags/exclude_tags settings by [@jlowin](https://github.com/jlowin) in [#2756](https://github.com/PrefectHQ/fastmcp/pull/2756)
+* Parallelize provider access when executing components by [@jlowin](https://github.com/jlowin) in [#2744](https://github.com/PrefectHQ/fastmcp/pull/2744)
+* Deprecate tool_serializer parameter by [@jlowin](https://github.com/jlowin) in [#2753](https://github.com/PrefectHQ/fastmcp/pull/2753)
+* Feature/supabase custom auth route by [@EloiZalczer](https://github.com/EloiZalczer) in [#2632](https://github.com/PrefectHQ/fastmcp/pull/2632)
+* Remove deprecated WSTransport by [@jlowin](https://github.com/jlowin) in [#2826](https://github.com/PrefectHQ/fastmcp/pull/2826)
+* Add composable lifespans by [@jlowin](https://github.com/jlowin) in [#2828](https://github.com/PrefectHQ/fastmcp/pull/2828)
+* Replace FastMCP.as_proxy() with create_proxy() function by [@jlowin](https://github.com/jlowin) in [#2829](https://github.com/PrefectHQ/fastmcp/pull/2829)
+* Add PingMiddleware for keepalive connections by [@jlowin](https://github.com/jlowin) in [#2838](https://github.com/PrefectHQ/fastmcp/pull/2838)
+* Run sync tools/resources/prompts in threadpool automatically by [@jlowin](https://github.com/jlowin) in [#2865](https://github.com/PrefectHQ/fastmcp/pull/2865)
+* Add timeout parameter for tool foreground execution by [@jlowin](https://github.com/jlowin) in [#2872](https://github.com/PrefectHQ/fastmcp/pull/2872)
+* Adopt OpenTelemetry MCP semantic conventions by [@chrisguidry](https://github.com/chrisguidry) in [#2886](https://github.com/PrefectHQ/fastmcp/pull/2886)
+* Add client_secret_post authentication to IntrospectionTokenVerifier by [@shulkx](https://github.com/shulkx) in [#2884](https://github.com/PrefectHQ/fastmcp/pull/2884)
+* Add enable_rich_logging setting to disable rich formatting by [@strawgate](https://github.com/strawgate) in [#2893](https://github.com/PrefectHQ/fastmcp/pull/2893)
+* Rename _fastmcp metadata namespace to fastmcp and make non-optional by [@jlowin](https://github.com/jlowin) in [#2895](https://github.com/PrefectHQ/fastmcp/pull/2895)
+* Refactor FastMCP to inherit from Provider by [@jlowin](https://github.com/jlowin) in [#2901](https://github.com/PrefectHQ/fastmcp/pull/2901)
+* Swap public/private method naming in Provider by [@jlowin](https://github.com/jlowin) in [#2902](https://github.com/PrefectHQ/fastmcp/pull/2902)
+* Add MCP-compliant pagination support by [@jlowin](https://github.com/jlowin) in [#2903](https://github.com/PrefectHQ/fastmcp/pull/2903)
+* Support VersionSpec in enable/disable for range-based filtering by [@jlowin](https://github.com/jlowin) in [#2914](https://github.com/PrefectHQ/fastmcp/pull/2914)
+* Immutable transform wrapping for providers by [@jlowin](https://github.com/jlowin) in [#2913](https://github.com/PrefectHQ/fastmcp/pull/2913)
+* Unify discovery API: deduplicate at protocol layer only by [@jlowin](https://github.com/jlowin) in [#2919](https://github.com/PrefectHQ/fastmcp/pull/2919)
+* Add ResourcesAsTools transform by [@jlowin](https://github.com/jlowin) in [#2943](https://github.com/PrefectHQ/fastmcp/pull/2943)
+* Add PromptsAsTools transform by [@jlowin](https://github.com/jlowin) in [#2946](https://github.com/PrefectHQ/fastmcp/pull/2946)
+* Rename Enabled transform to Visibility by [@jlowin](https://github.com/jlowin) in [#2950](https://github.com/PrefectHQ/fastmcp/pull/2950)
+* feat: option to add upstream claims to the FastMCP proxy JWT by [@JonasKs](https://github.com/JonasKs) in [#2997](https://github.com/PrefectHQ/fastmcp/pull/2997)
+* fix: automatically include offline_access as a scope in the Azure provider by [@JonasKs](https://github.com/JonasKs) in [#3001](https://github.com/PrefectHQ/fastmcp/pull/3001)
+* feat: expand --reload to watch frontend file types by [@jlowin](https://github.com/jlowin) in [#3028](https://github.com/PrefectHQ/fastmcp/pull/3028)
+* Add `fastmcp install stdio` command by [@jlowin](https://github.com/jlowin) in [#3032](https://github.com/PrefectHQ/fastmcp/pull/3032)
+* feat: Goose integration + dedicated install command by [@jlowin](https://github.com/jlowin) in [#3040](https://github.com/PrefectHQ/fastmcp/pull/3040)
+* Add `fastmcp discover` and name-based server resolution by [@jlowin](https://github.com/jlowin) in [#3055](https://github.com/PrefectHQ/fastmcp/pull/3055)
+* feat(context): Add background task support for Context by [@gfortaine](https://github.com/gfortaine) in [#2905](https://github.com/PrefectHQ/fastmcp/pull/2905)
+* Add server version to banner by [@richardkmichael](https://github.com/richardkmichael) in [#3076](https://github.com/PrefectHQ/fastmcp/pull/3076)
+* Add @handle_tool_errors decorator for standardized error handling by [@dgenio](https://github.com/dgenio) in [#2885](https://github.com/PrefectHQ/fastmcp/pull/2885)
+* Add ResponseLimitingMiddleware for tool response size control by [@dgenio](https://github.com/dgenio) in [#3072](https://github.com/PrefectHQ/fastmcp/pull/3072)
+* Infer MIME types from OpenAPI response definitions by [@jlowin](https://github.com/jlowin) in [#3101](https://github.com/PrefectHQ/fastmcp/pull/3101)
+* Remove require_auth in favor of scope-based authorization by [@jlowin](https://github.com/jlowin) in [#3103](https://github.com/PrefectHQ/fastmcp/pull/3103)
+* generate-cli: auto-generate SKILL.md agent skill by [@jlowin](https://github.com/jlowin) in [#3115](https://github.com/PrefectHQ/fastmcp/pull/3115)
+* Add Azure OBO dependencies, auth token injection, and documentation by [@jlowin](https://github.com/jlowin) in [#2918](https://github.com/PrefectHQ/fastmcp/pull/2918)
+* feat: add Static Client Registration by [@martimfasantos](https://github.com/martimfasantos) in [#3086](https://github.com/PrefectHQ/fastmcp/pull/3086)
+* Add concurrent tool execution with sequential flag by [@strawgate](https://github.com/strawgate) in [#3022](https://github.com/PrefectHQ/fastmcp/pull/3022)
+* Add validate_output option for OpenAPI tools by [@jlowin](https://github.com/jlowin) in [#3134](https://github.com/PrefectHQ/fastmcp/pull/3134)
+* Relay task elicitation through standard MCP protocol by [@chrisguidry](https://github.com/chrisguidry) in [#3136](https://github.com/PrefectHQ/fastmcp/pull/3136)
+* Support async auth checks by [@jlowin](https://github.com/jlowin) in [#3152](https://github.com/PrefectHQ/fastmcp/pull/3152)
+* Make $ref dereferencing optional via FastMCP(dereference_refs=...) by [@jlowin](https://github.com/jlowin) in [#3151](https://github.com/PrefectHQ/fastmcp/pull/3151)
+* Expose local_provider property, deprecate FastMCP.remove_tool() by [@jlowin](https://github.com/jlowin) in [#3155](https://github.com/PrefectHQ/fastmcp/pull/3155)
+* Add helpers for converting FunctionTool and TransformedTool to SamplingTool by [@strawgate](https://github.com/strawgate) in [#3062](https://github.com/PrefectHQ/fastmcp/pull/3062)
+### Fixes 🐞
+* Let FastMCPError propagate from dependencies by [@chrisguidry](https://github.com/chrisguidry) in [#2646](https://github.com/PrefectHQ/fastmcp/pull/2646)
+* Fix task execution for tools with custom names by [@chrisguidry](https://github.com/chrisguidry) in [#2645](https://github.com/PrefectHQ/fastmcp/pull/2645)
+* fix: check the cause of the tool error by [@rjolaverria](https://github.com/rjolaverria) in [#2674](https://github.com/PrefectHQ/fastmcp/pull/2674)
+* Fix uvicorn 0.39+ test timeouts and FastMCPError propagation by [@jlowin](https://github.com/jlowin) in [#2699](https://github.com/PrefectHQ/fastmcp/pull/2699)
+* Fix: resolve root-level $ref in outputSchema for MCP spec compliance by [@majiayu000](https://github.com/majiayu000) in [#2720](https://github.com/PrefectHQ/fastmcp/pull/2720)
+* Fix Proxy provider to return all resource contents by [@jlowin](https://github.com/jlowin) in [#2742](https://github.com/PrefectHQ/fastmcp/pull/2742)
+* fix: Client OAuth async_auth_flow() method causing MCP-SDK lock error by [@lgndluke](https://github.com/lgndluke) in [#2644](https://github.com/PrefectHQ/fastmcp/pull/2644)
+* Fix rate limit detection during teardown phase by [@jlowin](https://github.com/jlowin) in [#2757](https://github.com/PrefectHQ/fastmcp/pull/2757)
+* Fix OAuth Proxy resource parameter validation by [@jlowin](https://github.com/jlowin) in [#2764](https://github.com/PrefectHQ/fastmcp/pull/2764)
+* Fix `openapi_version` check so 3.1 is included by [@deeleeramone](https://github.com/deeleeramone) in [#2768](https://github.com/PrefectHQ/fastmcp/pull/2768)
+* Fix base_url fallback when url is not set by [@bhbs](https://github.com/bhbs) in [#2776](https://github.com/PrefectHQ/fastmcp/pull/2776)
+* Lazy import DiskStore to avoid sqlite3 dependency on import by [@jlowin](https://github.com/jlowin) in [#2784](https://github.com/PrefectHQ/fastmcp/pull/2784)
+* Fix OAuth token storage TTL calculation by [@jlowin](https://github.com/jlowin) in [#2796](https://github.com/PrefectHQ/fastmcp/pull/2796)
+* Fix client hanging on HTTP 4xx/5xx errors by [@jlowin](https://github.com/jlowin) in [#2803](https://github.com/PrefectHQ/fastmcp/pull/2803)
+* Fix keep_alive passthrough in StdioMCPServer.to_transport() by [@jlowin](https://github.com/jlowin) in [#2791](https://github.com/PrefectHQ/fastmcp/pull/2791)
+* Dereference $ref in tool schemas for MCP client compatibility by [@jlowin](https://github.com/jlowin) in [#2808](https://github.com/PrefectHQ/fastmcp/pull/2808)
+* Fix timeout not propagating to proxy clients in multi-server MCPConfig by [@jlowin](https://github.com/jlowin) in [#2809](https://github.com/PrefectHQ/fastmcp/pull/2809)
+* Fix ContextVar propagation for ASGI-mounted servers with tasks by [@chrisguidry](https://github.com/chrisguidry) in [#2844](https://github.com/PrefectHQ/fastmcp/pull/2844)
+* Fix HTTP transport timeout defaulting to 5 seconds by [@jlowin](https://github.com/jlowin) in [#2849](https://github.com/PrefectHQ/fastmcp/pull/2849)
+* Fix task capabilities location (issue #2870) by [@jlowin](https://github.com/jlowin) in [#2875](https://github.com/PrefectHQ/fastmcp/pull/2875)
+* fix: broaden combine_lifespans type to accept Mapping return types by [@aminsamir45](https://github.com/aminsamir45) in [#3005](https://github.com/PrefectHQ/fastmcp/pull/3005)
+* fix: correctly send resource when exchanging code for upstream by [@JonasKs](https://github.com/JonasKs) in [#3013](https://github.com/PrefectHQ/fastmcp/pull/3013)
+* chore: upgrade python-multipart to 0.0.22 (CVE-2026-24486) by [@jlowin](https://github.com/jlowin) in [#3042](https://github.com/PrefectHQ/fastmcp/pull/3042)
+* chore: upgrade protobuf to 6.33.5 (CVE-2026-0994) by [@jlowin](https://github.com/jlowin) in [#3043](https://github.com/PrefectHQ/fastmcp/pull/3043)
+* fix: use MCP spec error code -32002 for resource not found by [@jlowin](https://github.com/jlowin) in [#3041](https://github.com/PrefectHQ/fastmcp/pull/3041)
+* Fix tool_choice reset for structured output sampling by [@strawgate](https://github.com/strawgate) in [#3014](https://github.com/PrefectHQ/fastmcp/pull/3014)
+* fix: Preserve metadata in FastMCPProvider component wrappers by [@NeelayS](https://github.com/NeelayS) in [#3057](https://github.com/PrefectHQ/fastmcp/pull/3057)
+* fix: enforce redirect URI validation when allowed_client_redirect_uris is supplied by [@nathanwelsh8](https://github.com/nathanwelsh8) in [#3066](https://github.com/PrefectHQ/fastmcp/pull/3066)
+* Fix --reload port conflict when using explicit port by [@jlowin](https://github.com/jlowin) in [#3070](https://github.com/PrefectHQ/fastmcp/pull/3070)
+* Fix compress_schema to preserve additionalProperties: false by [@jlowin](https://github.com/jlowin) in [#3102](https://github.com/PrefectHQ/fastmcp/pull/3102)
+* Fix CIMD redirect allowlist bypass and cache revalidation by [@jlowin](https://github.com/jlowin) in [#3098](https://github.com/PrefectHQ/fastmcp/pull/3098)
+* Fix session visibility marks leaking across sessions by [@jlowin](https://github.com/jlowin) in [#3132](https://github.com/PrefectHQ/fastmcp/pull/3132)
+* Fix unhandled exceptions in OpenAPI POST tool calls by [@jlowin](https://github.com/jlowin) in [#3133](https://github.com/PrefectHQ/fastmcp/pull/3133)
+* feat: distributed notification queue + BLPOP elicitation for background tasks by [@gfortaine](https://github.com/gfortaine) in [#2906](https://github.com/PrefectHQ/fastmcp/pull/2906)
+* fix: snapshot access token for background tasks by [@gfortaine](https://github.com/gfortaine) in [#3138](https://github.com/PrefectHQ/fastmcp/pull/3138)
+* fix: guard client pagination loops against misbehaving servers by [@jlowin](https://github.com/jlowin) in [#3167](https://github.com/PrefectHQ/fastmcp/pull/3167)
+* Support non-serializable values in Context.set_state by [@jlowin](https://github.com/jlowin) in [#3171](https://github.com/PrefectHQ/fastmcp/pull/3171)
+* Fix stale request context in StatefulProxyClient handlers by [@jlowin](https://github.com/jlowin) in [#3172](https://github.com/PrefectHQ/fastmcp/pull/3172)
+* Drop diskcache dependency (CVE-2025-69872) by [@jlowin](https://github.com/jlowin) in [#3185](https://github.com/PrefectHQ/fastmcp/pull/3185)
+* Fix confused deputy attack via consent binding cookie by [@jlowin](https://github.com/jlowin) in [#3201](https://github.com/PrefectHQ/fastmcp/pull/3201)
+* Add JWT audience validation and RFC 8707 warnings to auth providers by [@jlowin](https://github.com/jlowin) in [#3204](https://github.com/PrefectHQ/fastmcp/pull/3204)
+* Cache OBO credentials on AzureProvider for token reuse by [@jlowin](https://github.com/jlowin) in [#3212](https://github.com/PrefectHQ/fastmcp/pull/3212)
+* Fix invalid uv add command in upgrade guide by [@jlowin](https://github.com/jlowin) in [#3217](https://github.com/PrefectHQ/fastmcp/pull/3217)
+* Use standard traceparent/tracestate keys per OTel MCP semconv by [@chrisguidry](https://github.com/chrisguidry) in [#3221](https://github.com/PrefectHQ/fastmcp/pull/3221)
+### Breaking Changes 🛫
+* Add VisibilityFilter for hierarchical enable/disable by [@jlowin](https://github.com/jlowin) in [#2708](https://github.com/PrefectHQ/fastmcp/pull/2708)
+* Remove automatic environment variable loading from auth providers by [@jlowin](https://github.com/jlowin) in [#2752](https://github.com/PrefectHQ/fastmcp/pull/2752)
+* Make pydocket optional and unify DI systems by [@jlowin](https://github.com/jlowin) in [#2835](https://github.com/PrefectHQ/fastmcp/pull/2835)
+* Add session-scoped state persistence by [@jlowin](https://github.com/jlowin) in [#2873](https://github.com/PrefectHQ/fastmcp/pull/2873)
+* Rename ui= to app= and consolidate ToolUI/ResourceUI into AppConfig by [@jlowin](https://github.com/jlowin) in [#3117](https://github.com/PrefectHQ/fastmcp/pull/3117)
+* Remove deprecated FastMCP() constructor kwargs by [@jlowin](https://github.com/jlowin) in [#3148](https://github.com/PrefectHQ/fastmcp/pull/3148)
+* Move `fastmcp dev` to `fastmcp dev inspector` by [@jlowin](https://github.com/jlowin) in [#3188](https://github.com/PrefectHQ/fastmcp/pull/3188)
+
+## New Contributors
+* [@ivanbelenky](https://github.com/ivanbelenky) made their first contribution in [#2656](https://github.com/PrefectHQ/fastmcp/pull/2656)
+* [@rjolaverria](https://github.com/rjolaverria) made their first contribution in [#2674](https://github.com/PrefectHQ/fastmcp/pull/2674)
+* [@mgoldsborough](https://github.com/mgoldsborough) made their first contribution in [#2701](https://github.com/PrefectHQ/fastmcp/pull/2701)
+* [@Ashif4354](https://github.com/Ashif4354) made their first contribution in [#2707](https://github.com/PrefectHQ/fastmcp/pull/2707)
+* [@majiayu000](https://github.com/majiayu000) made their first contribution in [#2720](https://github.com/PrefectHQ/fastmcp/pull/2720)
+* [@lgndluke](https://github.com/lgndluke) made their first contribution in [#2644](https://github.com/PrefectHQ/fastmcp/pull/2644)
+* [@EloiZalczer](https://github.com/EloiZalczer) made their first contribution in [#2632](https://github.com/PrefectHQ/fastmcp/pull/2632)
+* [@deeleeramone](https://github.com/deeleeramone) made their first contribution in [#2768](https://github.com/PrefectHQ/fastmcp/pull/2768)
+* [@shea-parkes](https://github.com/shea-parkes) made their first contribution in [#2781](https://github.com/PrefectHQ/fastmcp/pull/2781)
+* [@bryankthompson](https://github.com/bryankthompson) made their first contribution in [#2777](https://github.com/PrefectHQ/fastmcp/pull/2777)
+* [@bhbs](https://github.com/bhbs) made their first contribution in [#2776](https://github.com/PrefectHQ/fastmcp/pull/2776)
+* [@shulkx](https://github.com/shulkx) made their first contribution in [#2884](https://github.com/PrefectHQ/fastmcp/pull/2884)
+* [@abhijeethp](https://github.com/abhijeethp) made their first contribution in [#2967](https://github.com/PrefectHQ/fastmcp/pull/2967)
+* [@aminsamir45](https://github.com/aminsamir45) made their first contribution in [#3005](https://github.com/PrefectHQ/fastmcp/pull/3005)
+* [@JonasKs](https://github.com/JonasKs) made their first contribution in [#2997](https://github.com/PrefectHQ/fastmcp/pull/2997)
+* [@NeelayS](https://github.com/NeelayS) made their first contribution in [#3057](https://github.com/PrefectHQ/fastmcp/pull/3057)
+* [@gfortaine](https://github.com/gfortaine) made their first contribution in [#2905](https://github.com/PrefectHQ/fastmcp/pull/2905)
+* [@nathanwelsh8](https://github.com/nathanwelsh8) made their first contribution in [#3066](https://github.com/PrefectHQ/fastmcp/pull/3066)
+* [@dgenio](https://github.com/dgenio) made their first contribution in [#2885](https://github.com/PrefectHQ/fastmcp/pull/2885)
+* [@martimfasantos](https://github.com/martimfasantos) made their first contribution in [#3086](https://github.com/PrefectHQ/fastmcp/pull/3086)
+* [@jfBiswajit](https://github.com/jfBiswajit) made their first contribution in [#3193](https://github.com/PrefectHQ/fastmcp/pull/3193)
+
+**Full Changelog**: https://github.com/PrefectHQ/fastmcp/compare/v2.14.5...v3.0.0
+
+
+
+
+
+**[v3.0.0rc1: RC-ing is Believing](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.0rc1)**
+
+FastMCP 3 RC1 means we believe the API is stable. Beta 2 drew a wave of real-world adoption — production deployments, migration reports, integration testing — and the feedback overwhelmingly confirmed that the architecture works. This release closes gaps that surfaced under load: auth flows that needed to be async, background tasks that needed reliable notification delivery, and APIs still carrying beta-era naming. If nothing unexpected surfaces, this is what 3.0.0 looks like.
+
+🚨 **Breaking Changes** — The `ui=` parameter is now `app=` with a unified `AppConfig` class (matching the feature's actual name), and 16 `FastMCP()` constructor kwargs have finally been removed. If you've been ignoring months of deprecation warnings, you'll get a `TypeError` with specific migration instructions.
+
+🔐 **Auth Improvements** — Three changes that together round out FastMCP's auth story for production. `auth=` checks can now be `async`, so you can hit databases or external services during authorization — previously, passing an async function silently passed because the unawaited coroutine was truthy. Static Client Registration lets clients provide a pre-registered `client_id`/`client_secret` directly, bypassing DCR for servers that don't support it. And Azure OBO flows are now declarative via dependency injection:
+
+```python
+from fastmcp.server.auth.providers.azure import EntraOBOToken
+
+@mcp.tool()
+async def get_emails(
+ graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"]),
+):
+ # OBO exchange already happened — just use the token
+ ...
+```
+
+⚡ **Concurrent Sampling** — When an LLM returns multiple tool calls in a single response, `context.sample()` can now execute them in parallel. Opt in with `tool_concurrency=0` for unlimited parallelism, or set a bound. Tools that aren't safe to parallelize can declare `sequential=True`.
+
+📡 **Background Task Notifications** — Background tasks now reliably push progress updates and elicit user input through the standard MCP protocol. A distributed Redis queue replaces polling (7,200 round-trips/hour → one blocking call), and `ctx.elicit()` in background tasks automatically relays through the client's standard `elicitation_handler`.
+
+✅ **OpenAPI Output Validation** — When backends don't conform to their own OpenAPI schemas, the MCP SDK rejects the response and the tool fails. `validate_output=False` disables strict schema checking while still passing structured JSON to clients — a necessary escape hatch for imperfect APIs.
+
+## What's Changed
+### Enhancements 🔧
+* generate-cli: auto-generate SKILL.md agent skill by [@jlowin](https://github.com/jlowin) in [#3115](https://github.com/PrefectHQ/fastmcp/pull/3115)
+* Scope Martian triage to bug-labeled issues for jlowin by [@jlowin](https://github.com/jlowin) in [#3124](https://github.com/PrefectHQ/fastmcp/pull/3124)
+* Add Azure OBO dependencies, auth token injection, and documentation by [@jlowin](https://github.com/jlowin) in [#2918](https://github.com/PrefectHQ/fastmcp/pull/2918)
+* feat: add Static Client Registration (#3085) by [@martimfasantos](https://github.com/martimfasantos) in [#3086](https://github.com/PrefectHQ/fastmcp/pull/3086)
+* Add concurrent tool execution with sequential flag by [@strawgate](https://github.com/strawgate) in [#3022](https://github.com/PrefectHQ/fastmcp/pull/3022)
+* Add validate_output option for OpenAPI tools by [@jlowin](https://github.com/jlowin) in [#3134](https://github.com/PrefectHQ/fastmcp/pull/3134)
+* Relay task elicitation through standard MCP protocol by [@chrisguidry](https://github.com/chrisguidry) in [#3136](https://github.com/PrefectHQ/fastmcp/pull/3136)
+* Bump py-key-value-aio to `>=0.4.0,<0.5.0` by [@strawgate](https://github.com/strawgate) in [#3143](https://github.com/PrefectHQ/fastmcp/pull/3143)
+* Support async auth checks by [@jlowin](https://github.com/jlowin) in [#3152](https://github.com/PrefectHQ/fastmcp/pull/3152)
+* Make $ref dereferencing optional via FastMCP(dereference_refs=...) by [@jlowin](https://github.com/jlowin) in [#3151](https://github.com/PrefectHQ/fastmcp/pull/3151)
+* Expose local_provider property, deprecate FastMCP.remove_tool() by [@jlowin](https://github.com/jlowin) in [#3155](https://github.com/PrefectHQ/fastmcp/pull/3155)
+* Add helpers for converting FunctionTool and TransformedTool to SamplingTool by [@strawgate](https://github.com/strawgate) in [#3062](https://github.com/PrefectHQ/fastmcp/pull/3062)
+* Updates to github actions / workflows for claude by [@strawgate](https://github.com/strawgate) in [#3157](https://github.com/PrefectHQ/fastmcp/pull/3157)
+### Fixes 🐞
+* Updated deprecation URL for V3 by [@SrzStephen](https://github.com/SrzStephen) in [#3108](https://github.com/PrefectHQ/fastmcp/pull/3108)
+* Fix Windows test timeouts in OAuth proxy provider tests by [@strawgate](https://github.com/strawgate) in [#3123](https://github.com/PrefectHQ/fastmcp/pull/3123)
+* Fix session visibility marks leaking across sessions by [@jlowin](https://github.com/jlowin) in [#3132](https://github.com/PrefectHQ/fastmcp/pull/3132)
+* Fix unhandled exceptions in OpenAPI POST tool calls by [@jlowin](https://github.com/jlowin) in [#3133](https://github.com/PrefectHQ/fastmcp/pull/3133)
+* feat: distributed notification queue + BLPOP elicitation for background tasks by [@gfortaine](https://github.com/gfortaine) in [#2906](https://github.com/PrefectHQ/fastmcp/pull/2906)
+* fix: snapshot access token for background tasks (#3095) by [@gfortaine](https://github.com/gfortaine) in [#3138](https://github.com/PrefectHQ/fastmcp/pull/3138)
+* Stop duplicating path parameter descriptions into tool prose by [@jlowin](https://github.com/jlowin) in [#3149](https://github.com/PrefectHQ/fastmcp/pull/3149)
+* fix: guard client pagination loops against misbehaving servers by [@jlowin](https://github.com/jlowin) in [#3167](https://github.com/PrefectHQ/fastmcp/pull/3167)
+* Fix stale get_* references in docs and examples by [@jlowin](https://github.com/jlowin) in [#3168](https://github.com/PrefectHQ/fastmcp/pull/3168)
+* Support non-serializable values in Context.set_state by [@jlowin](https://github.com/jlowin) in [#3171](https://github.com/PrefectHQ/fastmcp/pull/3171)
+* Fix stale request context in StatefulProxyClient handlers by [@jlowin](https://github.com/jlowin) in [#3172](https://github.com/PrefectHQ/fastmcp/pull/3172)
+### Breaking Changes 🛫
+* Rename ui= to app= and consolidate ToolUI/ResourceUI into AppConfig by [@jlowin](https://github.com/jlowin) in [#3117](https://github.com/PrefectHQ/fastmcp/pull/3117)
+* Remove deprecated FastMCP() constructor kwargs by [@jlowin](https://github.com/jlowin) in [#3148](https://github.com/PrefectHQ/fastmcp/pull/3148)
+### Docs 📚
+* Update docs to reference beta 2 by [@jlowin](https://github.com/jlowin) in [#3112](https://github.com/PrefectHQ/fastmcp/pull/3112)
+* docs: add pre-registered OAuth clients to v3-features by [@jlowin](https://github.com/jlowin) in [#3129](https://github.com/PrefectHQ/fastmcp/pull/3129)
+### Dependencies 📦
+* chore(deps): bump cryptography from 46.0.3 to 46.0.5 in /examples/testing_demo in the uv group across 1 directory by @dependabot in [#3140](https://github.com/PrefectHQ/fastmcp/pull/3140)
+### Other Changes 🦾
+* docs: add v3.0.0rc1 features to v3-features tracking by [@jlowin](https://github.com/jlowin) in [#3145](https://github.com/PrefectHQ/fastmcp/pull/3145)
+* docs: remove nonexistent MSALApp from rc1 notes by [@jlowin](https://github.com/jlowin) in [#3146](https://github.com/PrefectHQ/fastmcp/pull/3146)
+
+## New Contributors
+* [@martimfasantos](https://github.com/martimfasantos) made their first contribution in [#3086](https://github.com/PrefectHQ/fastmcp/pull/3086)
+
+**Full Changelog**: https://github.com/PrefectHQ/fastmcp/compare/v3.0.0b2...v3.0.0rc1
+
+
+
+
+
+**[v3.0.0b2: 2 Fast 2 Beta](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.0b2)**
+
+FastMCP 3 Beta 2 reflects the huge number of people that kicked the tires on Beta 1. Seven new contributors landed changes in this release, and early migration reports went smoother than expected, including teams on Prefect Horizon upgrading from v2. Most of Beta 2 is refinement: fixing what people found, filling gaps from real usage, hardening edges. But a few new features did land along the way.
+
+🖥️ **Client CLI** — `fastmcp list`, `fastmcp call`, `fastmcp discover`, and `fastmcp generate-cli` turn any MCP server into something you can poke at from a terminal. Discover servers configured in Claude Desktop, Cursor, Goose, or project-level `mcp.json` files and reference them by name. `generate-cli` reads a server's schemas and writes a standalone typed CLI script where every tool is a proper subcommand with flags and help text.
+
+🔐 **CIMD** (Client ID Metadata Documents) adds an alternative to Dynamic Client Registration for OAuth. Clients host a static JSON document at an HTTPS URL; that URL becomes the `client_id`. Server-side support includes SSRF-hardened fetching, cache-aware revalidation, and `private_key_jwt` validation. Enabled by default on `OAuthProxy`.
+
+📱 **MCP Apps** — Spec-level compliance for the MCP Apps extension: `ui://` resource scheme, typed UI metadata on tools and resources, extension negotiation, and `ctx.client_supports_extension()` for runtime detection.
+
+⏳ **Background Task Context** — `Context` now works transparently in Docket workers. `ctx.elicit()` routes through Redis-based coordination so background tasks can pause for user input without any code changes.
+
+🛡️ **ResponseLimitingMiddleware** caps tool response sizes with UTF-8-safe truncation for text and schema-aware error handling for structured outputs.
+
+🪿 **Goose Integration** — `fastmcp install goose` generates deeplink URLs for one-command server installation into Goose.
+
+## What's Changed
+### New Features 🎉
+* Add MCP Apps Phase 1 — SDK compatibility (SEP-1865) by [@jlowin](https://github.com/jlowin) in [#3009](https://github.com/PrefectHQ/fastmcp/pull/3009)
+* Add `fastmcp list` and `fastmcp call` CLI commands by [@jlowin](https://github.com/jlowin) in [#3054](https://github.com/PrefectHQ/fastmcp/pull/3054)
+* Add `fastmcp generate-cli` command by [@jlowin](https://github.com/jlowin) in [#3065](https://github.com/PrefectHQ/fastmcp/pull/3065)
+* Add CIMD (Client ID Metadata Document) support for OAuth by [@jlowin](https://github.com/jlowin) in [#2871](https://github.com/PrefectHQ/fastmcp/pull/2871)
+### Enhancements 🔧
+* Make duplicate bot less aggressive by [@jlowin](https://github.com/jlowin) in [#2981](https://github.com/PrefectHQ/fastmcp/pull/2981)
+* Remove uv lockfile monitoring from Dependabot by [@jlowin](https://github.com/jlowin) in [#2986](https://github.com/PrefectHQ/fastmcp/pull/2986)
+* Run static checks with --upgrade, remove lockfile check by [@jlowin](https://github.com/jlowin) in [#2988](https://github.com/PrefectHQ/fastmcp/pull/2988)
+* Adjust workflow triggers for Marvin by [@strawgate](https://github.com/strawgate) in [#3010](https://github.com/PrefectHQ/fastmcp/pull/3010)
+* Move tests to a reusable action and enable nightly checks by [@strawgate](https://github.com/strawgate) in [#3017](https://github.com/PrefectHQ/fastmcp/pull/3017)
+* feat: option to add upstream claims to the FastMCP proxy JWT by [@JonasKs](https://github.com/JonasKs) in [#2997](https://github.com/PrefectHQ/fastmcp/pull/2997)
+* Fix ty 0.0.14 compatibility and upgrade dependencies by [@jlowin](https://github.com/jlowin) in [#3027](https://github.com/PrefectHQ/fastmcp/pull/3027)
+* fix: automatically include offline_access as a scope in the Azure provider to enable automatic token refreshing by [@JonasKs](https://github.com/JonasKs) in [#3001](https://github.com/PrefectHQ/fastmcp/pull/3001)
+* feat: expand --reload to watch frontend file types by [@jlowin](https://github.com/jlowin) in [#3028](https://github.com/PrefectHQ/fastmcp/pull/3028)
+* Add `fastmcp install stdio` command by [@jlowin](https://github.com/jlowin) in [#3032](https://github.com/PrefectHQ/fastmcp/pull/3032)
+* Update martian-issue-triage.yml for Workflow editing guidance by [@strawgate](https://github.com/strawgate) in [#3033](https://github.com/PrefectHQ/fastmcp/pull/3033)
+* feat: Goose integration + dedicated install command by [@jlowin](https://github.com/jlowin) in [#3040](https://github.com/PrefectHQ/fastmcp/pull/3040)
+* Fixing spelling issues in multiple files by [@didier-durand](https://github.com/didier-durand) in [#2996](https://github.com/PrefectHQ/fastmcp/pull/2996)
+* Add `fastmcp discover` and name-based server resolution by [@jlowin](https://github.com/jlowin) in [#3055](https://github.com/PrefectHQ/fastmcp/pull/3055)
+* feat(context): Add background task support for Context (SEP-1686) by [@gfortaine](https://github.com/gfortaine) in [#2905](https://github.com/PrefectHQ/fastmcp/pull/2905)
+* Add server version to banner by [@richardkmichael](https://github.com/richardkmichael) in [#3076](https://github.com/PrefectHQ/fastmcp/pull/3076)
+* Add @handle_tool_errors decorator for standardized error handling by [@dgenio](https://github.com/dgenio) in [#2885](https://github.com/PrefectHQ/fastmcp/pull/2885)
+* Update Anthropic and OpenAI clients to use Omit instead of NotGiven by [@jlowin](https://github.com/jlowin) in [#3088](https://github.com/PrefectHQ/fastmcp/pull/3088)
+* Add ResponseLimitingMiddleware for tool response size control by [@dgenio](https://github.com/dgenio) in [#3072](https://github.com/PrefectHQ/fastmcp/pull/3072)
+* Infer MIME types from OpenAPI response definitions by [@jlowin](https://github.com/jlowin) in [#3101](https://github.com/PrefectHQ/fastmcp/pull/3101)
+* Remove require_auth in favor of scope-based authorization by [@jlowin](https://github.com/jlowin) in [#3103](https://github.com/PrefectHQ/fastmcp/pull/3103)
+### Fixes 🐞
+* Fix FastAPI mounting examples in docs by [@jlowin](https://github.com/jlowin) in [#2962](https://github.com/PrefectHQ/fastmcp/pull/2962)
+* Remove outdated 'FastMCP 3.0 is coming!' CLI banner by [@jlowin](https://github.com/jlowin) in [#2974](https://github.com/PrefectHQ/fastmcp/pull/2974)
+* Pin httpx `< 1.0` and simplify beta install docs by [@jlowin](https://github.com/jlowin) in [#2975](https://github.com/PrefectHQ/fastmcp/pull/2975)
+* Add enabled field to ToolTransformConfig by [@jlowin](https://github.com/jlowin) in [#2991](https://github.com/PrefectHQ/fastmcp/pull/2991)
+* fix phue2 import in smart_home example by [@zzstoatzz](https://github.com/zzstoatzz) in [#2999](https://github.com/PrefectHQ/fastmcp/pull/2999)
+* fix: broaden combine_lifespans type to accept Mapping return types by [@aminsamir45](https://github.com/aminsamir45) in [#3005](https://github.com/PrefectHQ/fastmcp/pull/3005)
+* fix: type narrowing for skills resource contents by [@strawgate](https://github.com/strawgate) in [#3023](https://github.com/PrefectHQ/fastmcp/pull/3023)
+* fix: correctly send resource when exchanging code for the upstream by [@JonasKs](https://github.com/JonasKs) in [#3013](https://github.com/PrefectHQ/fastmcp/pull/3013)
+* MCP Apps: structured CSP/permissions types, resource meta propagation fix, QR example by [@jlowin](https://github.com/jlowin) in [#3031](https://github.com/PrefectHQ/fastmcp/pull/3031)
+* chore: upgrade python-multipart to 0.0.22 (CVE-2026-24486) by [@jlowin](https://github.com/jlowin) in [#3042](https://github.com/PrefectHQ/fastmcp/pull/3042)
+* chore: upgrade protobuf to 6.33.5 (CVE-2026-0994) by [@jlowin](https://github.com/jlowin) in [#3043](https://github.com/PrefectHQ/fastmcp/pull/3043)
+* fix: use MCP spec error code -32002 for resource not found by [@jlowin](https://github.com/jlowin) in [#3041](https://github.com/PrefectHQ/fastmcp/pull/3041)
+* Fix tool_choice reset for structured output sampling by [@strawgate](https://github.com/strawgate) in [#3014](https://github.com/PrefectHQ/fastmcp/pull/3014)
+* Fix workflow notification URL formatting in upgrade checks by [@strawgate](https://github.com/strawgate) in [#3047](https://github.com/PrefectHQ/fastmcp/pull/3047)
+* Fix Field() handling in prompts by [@strawgate](https://github.com/strawgate) in [#3050](https://github.com/PrefectHQ/fastmcp/pull/3050)
+* fix: use SkipJsonSchema to exclude callable fields from JSON schema generation by [@strawgate](https://github.com/strawgate) in [#3048](https://github.com/PrefectHQ/fastmcp/pull/3048)
+* fix: Preserve metadata in FastMCPProvider component wrappers by [@NeelayS](https://github.com/NeelayS) in [#3057](https://github.com/PrefectHQ/fastmcp/pull/3057)
+* Mock network calls in CLI tests and use MemoryStore for OAuth tests by [@strawgate](https://github.com/strawgate) in [#3051](https://github.com/PrefectHQ/fastmcp/pull/3051)
+* Remove OpenAPI timeout parameter, make client optional, surface timeout errors by [@jlowin](https://github.com/jlowin) in [#3067](https://github.com/PrefectHQ/fastmcp/pull/3067)
+* fix: enforce redirect URI validation when allowed_client_redirect_uris is supplied by [@nathanwelsh8](https://github.com/nathanwelsh8) in [#3066](https://github.com/PrefectHQ/fastmcp/pull/3066)
+* Fix --reload port conflict when using explicit port by [@jlowin](https://github.com/jlowin) in [#3070](https://github.com/PrefectHQ/fastmcp/pull/3070)
+* Fix compress_schema to preserve additionalProperties: false for MCP compatibility by [@jlowin](https://github.com/jlowin) in [#3102](https://github.com/PrefectHQ/fastmcp/pull/3102)
+* Fix CIMD redirect allowlist bypass and cache revalidation by [@jlowin](https://github.com/jlowin) in [#3098](https://github.com/PrefectHQ/fastmcp/pull/3098)
+* Exclude content-type from get_http_headers() to prevent HTTP 415 errors by [@jlowin](https://github.com/jlowin) in [#3104](https://github.com/PrefectHQ/fastmcp/pull/3104)
+### Docs 📚
+* Prepare docs for v3.0 beta release by [@jlowin](https://github.com/jlowin) in [#2954](https://github.com/PrefectHQ/fastmcp/pull/2954)
+* Restructure docs: move transforms to dedicated section by [@jlowin](https://github.com/jlowin) in [#2956](https://github.com/PrefectHQ/fastmcp/pull/2956)
+* Remove unnecessary pip warning by [@jlowin](https://github.com/jlowin) in [#2958](https://github.com/PrefectHQ/fastmcp/pull/2958)
+* Update example MCP version in installation docs by [@jlowin](https://github.com/jlowin) in [#2959](https://github.com/PrefectHQ/fastmcp/pull/2959)
+* Update brand images by [@jlowin](https://github.com/jlowin) in [#2960](https://github.com/PrefectHQ/fastmcp/pull/2960)
+* Restructure README and welcome page with motivated narrative by [@jlowin](https://github.com/jlowin) in [#2963](https://github.com/PrefectHQ/fastmcp/pull/2963)
+* Restructure README and docs with motivated narrative by [@jlowin](https://github.com/jlowin) in [#2964](https://github.com/PrefectHQ/fastmcp/pull/2964)
+* Favicon update and Prefect Horizon docs by [@jlowin](https://github.com/jlowin) in [#2978](https://github.com/PrefectHQ/fastmcp/pull/2978)
+* Add dependency injection documentation and DI-style dependencies by [@jlowin](https://github.com/jlowin) in [#2980](https://github.com/PrefectHQ/fastmcp/pull/2980)
+* docs: document expanded reload behavior and restructure beta sections by [@jlowin](https://github.com/jlowin) in [#3039](https://github.com/PrefectHQ/fastmcp/pull/3039)
+* Add output_schema caveat to response limiting docs by [@jlowin](https://github.com/jlowin) in [#3099](https://github.com/PrefectHQ/fastmcp/pull/3099)
+* Document token passthrough security in OAuth Proxy docs by [@jlowin](https://github.com/jlowin) in [#3100](https://github.com/PrefectHQ/fastmcp/pull/3100)
+### Dependencies 📦
+* Bump ty from 0.0.12 to 0.0.13 by @dependabot in [#2984](https://github.com/PrefectHQ/fastmcp/pull/2984)
+* Bump prek from 0.2.30 to 0.3.0 by @dependabot in [#2982](https://github.com/PrefectHQ/fastmcp/pull/2982)
+### Other Changes 🦾
+* Normalize resource URLs before comparison to support RFC 8707 query parameters by [@abhijeethp](https://github.com/abhijeethp) in [#2967](https://github.com/PrefectHQ/fastmcp/pull/2967)
+* Bump pydocket to 0.17.2 (memory leak fix) by [@chrisguidry](https://github.com/chrisguidry) in [#2998](https://github.com/PrefectHQ/fastmcp/pull/2998)
+* Add AzureJWTVerifier for Managed Identity token verification by [@jlowin](https://github.com/jlowin) in [#3058](https://github.com/PrefectHQ/fastmcp/pull/3058)
+* Add release notes for v2.14.4 and v2.14.5 by [@jlowin](https://github.com/jlowin) in [#3064](https://github.com/PrefectHQ/fastmcp/pull/3064)
+* Add missing beta2 features to v3 release tracking by [@jlowin](https://github.com/jlowin) in [#3105](https://github.com/PrefectHQ/fastmcp/pull/3105)
+
+## New Contributors
+* [@abhijeethp](https://github.com/abhijeethp) made their first contribution in [#2967](https://github.com/PrefectHQ/fastmcp/pull/2967)
+* [@aminsamir45](https://github.com/aminsamir45) made their first contribution in [#3005](https://github.com/PrefectHQ/fastmcp/pull/3005)
+* [@JonasKs](https://github.com/JonasKs) made their first contribution in [#2997](https://github.com/PrefectHQ/fastmcp/pull/2997)
+* [@NeelayS](https://github.com/NeelayS) made their first contribution in [#3057](https://github.com/PrefectHQ/fastmcp/pull/3057)
+* [@gfortaine](https://github.com/gfortaine) made their first contribution in [#2905](https://github.com/PrefectHQ/fastmcp/pull/2905)
+* [@nathanwelsh8](https://github.com/nathanwelsh8) made their first contribution in [#3066](https://github.com/PrefectHQ/fastmcp/pull/3066)
+* [@dgenio](https://github.com/dgenio) made their first contribution in [#2885](https://github.com/PrefectHQ/fastmcp/pull/2885)
+
+**Full Changelog**: https://github.com/PrefectHQ/fastmcp/compare/v3.0.0b1...v3.0.0b2
+
+
+
+
+
+**[v3.0.0b1: This Beta Work](https://github.com/PrefectHQ/fastmcp/releases/tag/v3.0.0b1)**
+
+FastMCP 3.0 rebuilds the framework around three primitives: components, providers, and transforms. Providers source components dynamically—from decorators, filesystems, OpenAPI specs, remote servers, or anywhere else. Transforms modify components as they flow to clients—renaming, namespacing, filtering, securing. The features that required specialized subsystems in v2 now compose naturally from these building blocks.
+
+🔌 **Provider Architecture** unifies how components are sourced. `FileSystemProvider` discovers decorated functions from directories with optional hot-reload. `SkillsProvider` exposes agent skill files as MCP resources. `OpenAPIProvider` and `ProxyProvider` get cleaner integrations. Providers are composable—share one across servers, or attach many to one server.
+
+🔄 **Transforms** add middleware for components. Namespace mounted servers, rename verbose tools, filter by version, control visibility—all without touching source code. `ResourcesAsTools` and `PromptsAsTools` expose non-tool components to tool-only clients.
+
+📋 **Component Versioning** lets you register `@tool(version="2.0")` alongside older versions. Clients see the highest version by default but can request specific versions. `VersionFilter` serves different API versions from one codebase.
+
+💾 **Session-Scoped State** persists across requests. `await ctx.set_state()` and `await ctx.get_state()` now survive the full session. Per-session visibility via `ctx.enable_components()` lets servers adapt dynamically to each client.
+
+⚡ **DX Improvements** include `--reload` for auto-restart during development, automatic threadpool dispatch for sync functions, tool timeouts, pagination for large component lists, and OpenTelemetry tracing.
+
+🔐 **Component Authorization** via `@tool(auth=require_scopes("admin"))` and `AuthMiddleware` for server-wide policies.
+
+Breaking changes are minimal: for most servers, updating the import statement is all you need. See the [migration guide](https://github.com/PrefectHQ/fastmcp/blob/main/docs/getting-started/upgrading/from-fastmcp-2.mdx) for details.
+
+## What's Changed
+### New Features 🎉
+* Refactor resource behavior and add meta support by [@jlowin](https://github.com/jlowin) in [#2611](https://github.com/PrefectHQ/fastmcp/pull/2611)
+* Refactor prompt behavior and add meta support by [@jlowin](https://github.com/jlowin) in [#2610](https://github.com/PrefectHQ/fastmcp/pull/2610)
+* feat: Provider abstraction for dynamic MCP components by [@jlowin](https://github.com/jlowin) in [#2622](https://github.com/PrefectHQ/fastmcp/pull/2622)
+* Unify component storage in LocalProvider by [@jlowin](https://github.com/jlowin) in [#2680](https://github.com/PrefectHQ/fastmcp/pull/2680)
+* Introduce ResourceResult as canonical resource return type by [@jlowin](https://github.com/jlowin) in [#2734](https://github.com/PrefectHQ/fastmcp/pull/2734)
+* Introduce Message and PromptResult as canonical prompt types by [@jlowin](https://github.com/jlowin) in [#2738](https://github.com/PrefectHQ/fastmcp/pull/2738)
+* Add --reload flag for auto-restart on file changes by [@jlowin](https://github.com/jlowin) in [#2816](https://github.com/PrefectHQ/fastmcp/pull/2816)
+* Add FileSystemProvider for filesystem-based component discovery by [@jlowin](https://github.com/jlowin) in [#2823](https://github.com/PrefectHQ/fastmcp/pull/2823)
+* Add standalone decorators and eliminate fastmcp.fs module by [@jlowin](https://github.com/jlowin) in [#2832](https://github.com/PrefectHQ/fastmcp/pull/2832)
+* Add authorization checks to components and servers by [@jlowin](https://github.com/jlowin) in [#2855](https://github.com/PrefectHQ/fastmcp/pull/2855)
+* Decorators return functions instead of component objects by [@jlowin](https://github.com/jlowin) in [#2856](https://github.com/PrefectHQ/fastmcp/pull/2856)
+* Add transform system for modifying components in provider chains by [@jlowin](https://github.com/jlowin) in [#2836](https://github.com/PrefectHQ/fastmcp/pull/2836)
+* Add OpenTelemetry tracing support by [@chrisguidry](https://github.com/chrisguidry) in [#2869](https://github.com/PrefectHQ/fastmcp/pull/2869)
+* Add component versioning and VersionFilter transform by [@jlowin](https://github.com/jlowin) in [#2894](https://github.com/PrefectHQ/fastmcp/pull/2894)
+* Add version discovery and calling a certain version for components by [@jlowin](https://github.com/jlowin) in [#2897](https://github.com/PrefectHQ/fastmcp/pull/2897)
+* Refactor visibility to mark-based enabled system by [@jlowin](https://github.com/jlowin) in [#2912](https://github.com/PrefectHQ/fastmcp/pull/2912)
+* Add session-specific visibility control via Context by [@jlowin](https://github.com/jlowin) in [#2917](https://github.com/PrefectHQ/fastmcp/pull/2917)
+* Add Skills Provider for exposing agent skills as MCP resources by [@jlowin](https://github.com/jlowin) in [#2944](https://github.com/PrefectHQ/fastmcp/pull/2944)
+### Enhancements 🔧
+* Convert mounted servers to MountedProvider by [@jlowin](https://github.com/jlowin) in [#2635](https://github.com/PrefectHQ/fastmcp/pull/2635)
+* Simplify .key as computed property by [@jlowin](https://github.com/jlowin) in [#2648](https://github.com/PrefectHQ/fastmcp/pull/2648)
+* Refactor MountedProvider into FastMCPProvider + TransformingProvider by [@jlowin](https://github.com/jlowin) in [#2653](https://github.com/PrefectHQ/fastmcp/pull/2653)
+* Enable background task support for custom component subclasses by [@jlowin](https://github.com/jlowin) in [#2657](https://github.com/PrefectHQ/fastmcp/pull/2657)
+* Use CreateTaskResult for background task creation by [@jlowin](https://github.com/jlowin) in [#2660](https://github.com/PrefectHQ/fastmcp/pull/2660)
+* Refactor provider execution: components own their execution by [@jlowin](https://github.com/jlowin) in [#2663](https://github.com/PrefectHQ/fastmcp/pull/2663)
+* Add supports_tasks() method to replace string mode checks by [@jlowin](https://github.com/jlowin) in [#2664](https://github.com/PrefectHQ/fastmcp/pull/2664)
+* Replace type: ignore[attr-defined] with isinstance assertions in tests by [@jlowin](https://github.com/jlowin) in [#2665](https://github.com/PrefectHQ/fastmcp/pull/2665)
+* Add poll_interval to TaskConfig by [@jlowin](https://github.com/jlowin) in [#2666](https://github.com/PrefectHQ/fastmcp/pull/2666)
+* Refactor task module: rename protocol.py to requests.py and reduce redundancy by [@jlowin](https://github.com/jlowin) in [#2667](https://github.com/PrefectHQ/fastmcp/pull/2667)
+* Refactor FastMCPProxy into ProxyProvider by [@jlowin](https://github.com/jlowin) in [#2669](https://github.com/PrefectHQ/fastmcp/pull/2669)
+* Move OpenAPI to providers/openapi submodule by [@jlowin](https://github.com/jlowin) in [#2672](https://github.com/PrefectHQ/fastmcp/pull/2672)
+* Use ergonomic provider initialization pattern by [@jlowin](https://github.com/jlowin) in [#2675](https://github.com/PrefectHQ/fastmcp/pull/2675)
+* Fix ty 0.0.5 type errors by [@jlowin](https://github.com/jlowin) in [#2676](https://github.com/PrefectHQ/fastmcp/pull/2676)
+* Remove execution methods from Provider base class by [@jlowin](https://github.com/jlowin) in [#2681](https://github.com/PrefectHQ/fastmcp/pull/2681)
+* Add type-prefixed keys for globally unique component identification by [@jlowin](https://github.com/jlowin) in [#2704](https://github.com/PrefectHQ/fastmcp/pull/2704)
+* Skip parallel MCP config test on Windows by [@jlowin](https://github.com/jlowin) in [#2711](https://github.com/PrefectHQ/fastmcp/pull/2711)
+* Consolidate notification system with unified API by [@jlowin](https://github.com/jlowin) in [#2710](https://github.com/PrefectHQ/fastmcp/pull/2710)
+* Skip test_multi_client on Windows by [@jlowin](https://github.com/jlowin) in [#2714](https://github.com/PrefectHQ/fastmcp/pull/2714)
+* Parallelize provider operations by [@jlowin](https://github.com/jlowin) in [#2716](https://github.com/PrefectHQ/fastmcp/pull/2716)
+* Consolidate get_* and _list_* methods into single API by [@jlowin](https://github.com/jlowin) in [#2719](https://github.com/PrefectHQ/fastmcp/pull/2719)
+* Consolidate execution method chains into single public API by [@jlowin](https://github.com/jlowin) in [#2728](https://github.com/PrefectHQ/fastmcp/pull/2728)
+* Add documentation check to required PR workflow by [@jlowin](https://github.com/jlowin) in [#2730](https://github.com/PrefectHQ/fastmcp/pull/2730)
+* Parallelize list_* calls in Provider.get_tasks() by [@jlowin](https://github.com/jlowin) in [#2731](https://github.com/PrefectHQ/fastmcp/pull/2731)
+* Consistent decorator-based MCP handler registration by [@jlowin](https://github.com/jlowin) in [#2732](https://github.com/PrefectHQ/fastmcp/pull/2732)
+* Make ToolResult a BaseModel for serialization support by [@jlowin](https://github.com/jlowin) in [#2736](https://github.com/PrefectHQ/fastmcp/pull/2736)
+* Align prompt handler with resource pattern by [@jlowin](https://github.com/jlowin) in [#2740](https://github.com/PrefectHQ/fastmcp/pull/2740)
+* Update classes to inherit from FastMCPBaseModel instead of BaseModel by [@jlowin](https://github.com/jlowin) in [#2739](https://github.com/PrefectHQ/fastmcp/pull/2739)
+* Convert provider tests to use direct server calls by [@jlowin](https://github.com/jlowin) in [#2748](https://github.com/PrefectHQ/fastmcp/pull/2748)
+* Add explicit task_meta parameter to FastMCP.call_tool() by [@jlowin](https://github.com/jlowin) in [#2749](https://github.com/PrefectHQ/fastmcp/pull/2749)
+* Add task_meta parameter to read_resource() for explicit task control by [@jlowin](https://github.com/jlowin) in [#2750](https://github.com/PrefectHQ/fastmcp/pull/2750)
+* Add task_meta to prompts and centralize fn_key enrichment by [@jlowin](https://github.com/jlowin) in [#2751](https://github.com/PrefectHQ/fastmcp/pull/2751)
+* Remove unused include_tags/exclude_tags settings by [@jlowin](https://github.com/jlowin) in [#2756](https://github.com/PrefectHQ/fastmcp/pull/2756)
+* Parallelize provider access when executing components by [@jlowin](https://github.com/jlowin) in [#2744](https://github.com/PrefectHQ/fastmcp/pull/2744)
+* Add tests for OAuth generator cleanup and use aclosing by [@jlowin](https://github.com/jlowin) in [#2759](https://github.com/PrefectHQ/fastmcp/pull/2759)
+* Deprecate tool_serializer parameter by [@jlowin](https://github.com/jlowin) in [#2753](https://github.com/PrefectHQ/fastmcp/pull/2753)
+* Feature/supabase custom auth route by [@EloiZalczer](https://github.com/EloiZalczer) in [#2632](https://github.com/PrefectHQ/fastmcp/pull/2632)
+* Add regression tests for caching with mounted server prefixes by [@jlowin](https://github.com/jlowin) in [#2762](https://github.com/PrefectHQ/fastmcp/pull/2762)
+* Update CLI banner with FastMCP 3.0 notice by [@jlowin](https://github.com/jlowin) in [#2766](https://github.com/PrefectHQ/fastmcp/pull/2766)
+* Make FASTMCP_SHOW_SERVER_BANNER apply to all server startup methods by [@jlowin](https://github.com/jlowin) in [#2771](https://github.com/PrefectHQ/fastmcp/pull/2771)
+* Add MCP tool annotations to smart_home example by [@triepod-ai](https://github.com/triepod-ai) in [#2777](https://github.com/PrefectHQ/fastmcp/pull/2777)
+* Cherry-pick debug logging for OAuth token expiry to main by [@jlowin](https://github.com/jlowin) in [#2797](https://github.com/PrefectHQ/fastmcp/pull/2797)
+* Turn off negative CLI flags by default by [@jlowin](https://github.com/jlowin) in [#2801](https://github.com/PrefectHQ/fastmcp/pull/2801)
+* Configure ty to fail on warnings by [@jlowin](https://github.com/jlowin) in [#2804](https://github.com/PrefectHQ/fastmcp/pull/2804)
+* Dereference $ref in tool schemas for MCP client compatibility by [@jlowin](https://github.com/jlowin) in [#2814](https://github.com/PrefectHQ/fastmcp/pull/2814)
+* Add v3.0 feature tracking document by [@jlowin](https://github.com/jlowin) in [#2822](https://github.com/PrefectHQ/fastmcp/pull/2822)
+* Remove deprecated WSTransport by [@jlowin](https://github.com/jlowin) in [#2826](https://github.com/PrefectHQ/fastmcp/pull/2826)
+* Add composable lifespans by [@jlowin](https://github.com/jlowin) in [#2828](https://github.com/PrefectHQ/fastmcp/pull/2828)
+* Replace FastMCP.as_proxy() with create_proxy() function by [@jlowin](https://github.com/jlowin) in [#2829](https://github.com/PrefectHQ/fastmcp/pull/2829)
+* Add docs-broken-links command and fix docstring markdown parsing by [@jlowin](https://github.com/jlowin) in [#2830](https://github.com/PrefectHQ/fastmcp/pull/2830)
+* Add PingMiddleware for keepalive connections by [@jlowin](https://github.com/jlowin) in [#2838](https://github.com/PrefectHQ/fastmcp/pull/2838)
+* Add CLI update notifications by [@jlowin](https://github.com/jlowin) in [#2840](https://github.com/PrefectHQ/fastmcp/pull/2840)
+* Add agent skills for testing and code review by [@jlowin](https://github.com/jlowin) in [#2846](https://github.com/PrefectHQ/fastmcp/pull/2846)
+* Add loq pre-commit hook for file size enforcement by [@jlowin](https://github.com/jlowin) in [#2847](https://github.com/PrefectHQ/fastmcp/pull/2847)
+* Add transport property to Context by [@jlowin](https://github.com/jlowin) in [#2850](https://github.com/PrefectHQ/fastmcp/pull/2850)
+* Add loq file size limits and clean up type ignores by [@jlowin](https://github.com/jlowin) in [#2859](https://github.com/PrefectHQ/fastmcp/pull/2859)
+* Run sync tools/resources/prompts in threadpool automatically by [@jlowin](https://github.com/jlowin) in [#2865](https://github.com/PrefectHQ/fastmcp/pull/2865)
+* Add timeout parameter for tool foreground execution by [@jlowin](https://github.com/jlowin) in [#2872](https://github.com/PrefectHQ/fastmcp/pull/2872)
+* Adopt OpenTelemetry MCP semantic conventions by [@chrisguidry](https://github.com/chrisguidry) in [#2886](https://github.com/PrefectHQ/fastmcp/pull/2886)
+* Add client_secret_post authentication to IntrospectionTokenVerifier by [@shulkx](https://github.com/shulkx) in [#2884](https://github.com/PrefectHQ/fastmcp/pull/2884)
+* Add enable_rich_logging setting to disable rich formatting by [@strawgate](https://github.com/strawgate) in [#2893](https://github.com/PrefectHQ/fastmcp/pull/2893)
+* Rename _fastmcp metadata namespace to fastmcp and make non-optional by [@jlowin](https://github.com/jlowin) in [#2895](https://github.com/PrefectHQ/fastmcp/pull/2895)
+* Refactor FastMCP to inherit from Provider by [@jlowin](https://github.com/jlowin) in [#2901](https://github.com/PrefectHQ/fastmcp/pull/2901)
+* Swap public/private method naming in Provider by [@jlowin](https://github.com/jlowin) in [#2902](https://github.com/PrefectHQ/fastmcp/pull/2902)
+* Add MCP-compliant pagination support by [@jlowin](https://github.com/jlowin) in [#2903](https://github.com/PrefectHQ/fastmcp/pull/2903)
+* Support VersionSpec in enable/disable for range-based filtering by [@jlowin](https://github.com/jlowin) in [#2914](https://github.com/PrefectHQ/fastmcp/pull/2914)
+* Remove sync notification infrastructure by [@jlowin](https://github.com/jlowin) in [#2915](https://github.com/PrefectHQ/fastmcp/pull/2915)
+* Immutable transform wrapping for providers by [@jlowin](https://github.com/jlowin) in [#2913](https://github.com/PrefectHQ/fastmcp/pull/2913)
+* Unify discovery API: deduplicate at protocol layer only by [@jlowin](https://github.com/jlowin) in [#2919](https://github.com/PrefectHQ/fastmcp/pull/2919)
+* Split transports.py into modular structure by [@jlowin](https://github.com/jlowin) in [#2921](https://github.com/PrefectHQ/fastmcp/pull/2921)
+* Move session visibility logic to enabled.py by [@jlowin](https://github.com/jlowin) in [#2924](https://github.com/PrefectHQ/fastmcp/pull/2924)
+* Refactor Client class into mixins and add timeout utilities by [@jlowin](https://github.com/jlowin) in [#2933](https://github.com/PrefectHQ/fastmcp/pull/2933)
+* Refactor OAuthProxy into focused modules by [@jlowin](https://github.com/jlowin) in [#2935](https://github.com/PrefectHQ/fastmcp/pull/2935)
+* Refactor LocalProvider into mixin modules by [@jlowin](https://github.com/jlowin) in [#2936](https://github.com/PrefectHQ/fastmcp/pull/2936)
+* Refactor server.py into mixins by [@jlowin](https://github.com/jlowin) in [#2939](https://github.com/PrefectHQ/fastmcp/pull/2939)
+* Consolidate test fixtures and refactor large test files by [@jlowin](https://github.com/jlowin) in [#2941](https://github.com/PrefectHQ/fastmcp/pull/2941)
+* Refactor transform list methods to pure function pattern by [@jlowin](https://github.com/jlowin) in [#2942](https://github.com/PrefectHQ/fastmcp/pull/2942)
+* Add ResourcesAsTools transform by [@jlowin](https://github.com/jlowin) in [#2943](https://github.com/PrefectHQ/fastmcp/pull/2943)
+* Add PromptsAsTools transform by [@jlowin](https://github.com/jlowin) in [#2946](https://github.com/PrefectHQ/fastmcp/pull/2946)
+* Add client utilities for downloading skills by [@jlowin](https://github.com/jlowin) in [#2948](https://github.com/PrefectHQ/fastmcp/pull/2948)
+* Rename Enabled transform to Visibility by [@jlowin](https://github.com/jlowin) in [#2950](https://github.com/PrefectHQ/fastmcp/pull/2950)
+### Fixes 🐞
+* Let FastMCPError propagate from dependencies by [@chrisguidry](https://github.com/chrisguidry) in [#2646](https://github.com/PrefectHQ/fastmcp/pull/2646)
+* Fix task execution for tools with custom names by [@chrisguidry](https://github.com/chrisguidry) in [#2645](https://github.com/PrefectHQ/fastmcp/pull/2645)
+* fix: check the cause of the tool error by [@rjolaverria](https://github.com/rjolaverria) in [#2674](https://github.com/PrefectHQ/fastmcp/pull/2674)
+* Bump pydocket to 0.16.3 for task cancellation support by [@chrisguidry](https://github.com/chrisguidry) in [#2683](https://github.com/PrefectHQ/fastmcp/pull/2683)
+* Fix uvicorn 0.39+ test timeouts and FastMCPError propagation by [@jlowin](https://github.com/jlowin) in [#2699](https://github.com/PrefectHQ/fastmcp/pull/2699)
+* Fix Prefect website URL in docs footer by [@mgoldsborough](https://github.com/mgoldsborough) in [#2701](https://github.com/PrefectHQ/fastmcp/pull/2701)
+* Fix: resolve root-level $ref in outputSchema for MCP spec compliance by [@majiayu000](https://github.com/majiayu000) in [#2720](https://github.com/PrefectHQ/fastmcp/pull/2720)
+* Fix Provider.get_tasks() to include custom component subclasses by [@jlowin](https://github.com/jlowin) in [#2729](https://github.com/PrefectHQ/fastmcp/pull/2729)
+* Fix Proxy provider to return all resource contents by [@jlowin](https://github.com/jlowin) in [#2742](https://github.com/PrefectHQ/fastmcp/pull/2742)
+* Fix prompt return type documentation by [@jlowin](https://github.com/jlowin) in [#2741](https://github.com/PrefectHQ/fastmcp/pull/2741)
+* fix: Client OAuth async_auth_flow() method causing MCP-SDK self.context.lock error. by [@lgndluke](https://github.com/lgndluke) in [#2644](https://github.com/PrefectHQ/fastmcp/pull/2644)
+* Fix rate limit detection during teardown phase by [@jlowin](https://github.com/jlowin) in [#2757](https://github.com/PrefectHQ/fastmcp/pull/2757)
+* fix: set pytest-asyncio default fixture loop scope to function by [@jlowin](https://github.com/jlowin) in [#2758](https://github.com/PrefectHQ/fastmcp/pull/2758)
+* Fix OAuth Proxy resource parameter validation by [@jlowin](https://github.com/jlowin) in [#2764](https://github.com/PrefectHQ/fastmcp/pull/2764)
+* [BugFix] Fix `openapi_version` Check So 3.1 Is Included by [@deeleeramone](https://github.com/deeleeramone) in [#2768](https://github.com/PrefectHQ/fastmcp/pull/2768)
+* Fix titled enum elicitation schema to comply with MCP spec by [@jlowin](https://github.com/jlowin) in [#2773](https://github.com/PrefectHQ/fastmcp/pull/2773)
+* Fix base_url fallback when url is not set by [@bhbs](https://github.com/bhbs) in [#2776](https://github.com/PrefectHQ/fastmcp/pull/2776)
+* Lazy import DiskStore to avoid sqlite3 dependency on import by [@jlowin](https://github.com/jlowin) in [#2784](https://github.com/PrefectHQ/fastmcp/pull/2784)
+* Fix OAuth token storage TTL calculation by [@jlowin](https://github.com/jlowin) in [#2796](https://github.com/PrefectHQ/fastmcp/pull/2796)
+* Use consistent refresh_ttl for JTI mapping store by [@jlowin](https://github.com/jlowin) in [#2799](https://github.com/PrefectHQ/fastmcp/pull/2799)
+* Return 401 for invalid_grant token errors per MCP spec by [@jlowin](https://github.com/jlowin) in [#2800](https://github.com/PrefectHQ/fastmcp/pull/2800)
+* Fix client hanging on HTTP 4xx/5xx errors by [@jlowin](https://github.com/jlowin) in [#2803](https://github.com/PrefectHQ/fastmcp/pull/2803)
+* Fix unawaited coroutine warning and treat as test error by [@jlowin](https://github.com/jlowin) in [#2806](https://github.com/PrefectHQ/fastmcp/pull/2806)
+* Fix keep_alive passthrough in StdioMCPServer.to_transport() by [@jlowin](https://github.com/jlowin) in [#2791](https://github.com/PrefectHQ/fastmcp/pull/2791)
+* Dereference $ref in tool schemas for MCP client compatibility by [@jlowin](https://github.com/jlowin) in [#2808](https://github.com/PrefectHQ/fastmcp/pull/2808)
+* Prefix Redis keys with docket name for ACL isolation by [@chrisguidry](https://github.com/chrisguidry) in [#2811](https://github.com/PrefectHQ/fastmcp/pull/2811)
+* fix smart_home example: HueAttributes schema and deprecated prefix by [@zzstoatzz](https://github.com/zzstoatzz) in [#2818](https://github.com/PrefectHQ/fastmcp/pull/2818)
+* Fix redirect URI validation docs to match implementation by [@jlowin](https://github.com/jlowin) in [#2824](https://github.com/PrefectHQ/fastmcp/pull/2824)
+* Fix timeout not propagating to proxy clients in multi-server MCPConfig by [@jlowin](https://github.com/jlowin) in [#2809](https://github.com/PrefectHQ/fastmcp/pull/2809)
+* Fix ContextVar propagation for ASGI-mounted servers with tasks by [@chrisguidry](https://github.com/chrisguidry) in [#2844](https://github.com/PrefectHQ/fastmcp/pull/2844)
+* Fix HTTP transport timeout defaulting to 5 seconds by [@jlowin](https://github.com/jlowin) in [#2849](https://github.com/PrefectHQ/fastmcp/pull/2849)
+* Fix decorator error messages to link to correct doc pages by [@jlowin](https://github.com/jlowin) in [#2858](https://github.com/PrefectHQ/fastmcp/pull/2858)
+* Fix task capabilities location (issue #2870) by [@jlowin](https://github.com/jlowin) in [#2875](https://github.com/PrefectHQ/fastmcp/pull/2875)
+* Bump the uv group across 1 directory with 2 updates by [@dependabot](https://github.com/dependabot)\[bot\] in [#2890](https://github.com/PrefectHQ/fastmcp/pull/2890)
+### Breaking Changes 🛫
+* Add VisibilityFilter for hierarchical enable/disable by [@jlowin](https://github.com/jlowin) in [#2708](https://github.com/PrefectHQ/fastmcp/pull/2708)
+* Remove automatic environment variable loading from auth providers by [@jlowin](https://github.com/jlowin) in [#2752](https://github.com/PrefectHQ/fastmcp/pull/2752)
+* Make pydocket optional and unify DI systems by [@jlowin](https://github.com/jlowin) in [#2835](https://github.com/PrefectHQ/fastmcp/pull/2835)
+* Add session-scoped state persistence by [@jlowin](https://github.com/jlowin) in [#2873](https://github.com/PrefectHQ/fastmcp/pull/2873)
+### Docs 📚
+* Undocumented `McpError` exceptions by [@ivanbelenky](https://github.com/ivanbelenky) in [#2656](https://github.com/PrefectHQ/fastmcp/pull/2656)
+* docs(server): add http to transport options in run() method docstring by [@Ashif4354](https://github.com/Ashif4354) in [#2707](https://github.com/PrefectHQ/fastmcp/pull/2707)
+* Add v3 breaking changes notice to README by [@jlowin](https://github.com/jlowin) in [#2712](https://github.com/PrefectHQ/fastmcp/pull/2712)
+* Add changelog entries for v2.13.1 through v2.14.1 by [@jlowin](https://github.com/jlowin) in [#2725](https://github.com/PrefectHQ/fastmcp/pull/2725)
+* Reorganize docs around provider architecture by [@jlowin](https://github.com/jlowin) in [#2723](https://github.com/PrefectHQ/fastmcp/pull/2723)
+* Fix documentation to use 'meta' instead of '_meta' for MCP spec field by [@jlowin](https://github.com/jlowin) in [#2735](https://github.com/PrefectHQ/fastmcp/pull/2735)
+* Enhance documentation on tool transformation by [@shea-parkes](https://github.com/shea-parkes) in [#2781](https://github.com/PrefectHQ/fastmcp/pull/2781)
+* Add FastMCP 4.0 preview to documentation by [@jlowin](https://github.com/jlowin) in [#2831](https://github.com/PrefectHQ/fastmcp/pull/2831)
+* Add release notes for v2.14.2 and v2.14.3 by [@jlowin](https://github.com/jlowin) in [#2852](https://github.com/PrefectHQ/fastmcp/pull/2852)
+* Add missing 3.0.0 version badges and document tasks extra by [@jlowin](https://github.com/jlowin) in [#2866](https://github.com/PrefectHQ/fastmcp/pull/2866)
+* Fix custom provider docs to show correct interface by [@jlowin](https://github.com/jlowin) in [#2920](https://github.com/PrefectHQ/fastmcp/pull/2920)
+* Update v3 features that were missed in PRs by [@jlowin](https://github.com/jlowin) in [#2947](https://github.com/PrefectHQ/fastmcp/pull/2947)
+* Restructure documentation for FastMCP 3.0 by [@jlowin](https://github.com/jlowin) in [#2951](https://github.com/PrefectHQ/fastmcp/pull/2951)
+* Fix broken documentation links by [@jlowin](https://github.com/jlowin) in [#2952](https://github.com/PrefectHQ/fastmcp/pull/2952)
+* Clarify installation for FastMCP 3.0 beta by [@jlowin](https://github.com/jlowin) in [#2953](https://github.com/PrefectHQ/fastmcp/pull/2953)
+### Dependencies 📦
+* Bump peter-evans/create-pull-request from 7 to 8 by [@dependabot](https://github.com/dependabot)\[bot\] in [#2623](https://github.com/PrefectHQ/fastmcp/pull/2623)
+* Bump ty to 0.0.7+ by [@jlowin](https://github.com/jlowin) in [#2737](https://github.com/PrefectHQ/fastmcp/pull/2737)
+* Bump the uv group across 1 directory with 4 updates by [@dependabot](https://github.com/dependabot)\[bot\] in [#2891](https://github.com/PrefectHQ/fastmcp/pull/2891)
+
+## New Contributors
+* [@ivanbelenky](https://github.com/ivanbelenky) made their first contribution in [#2656](https://github.com/PrefectHQ/fastmcp/pull/2656)
+* [@rjolaverria](https://github.com/rjolaverria) made their first contribution in [#2674](https://github.com/PrefectHQ/fastmcp/pull/2674)
+* [@mgoldsborough](https://github.com/mgoldsborough) made their first contribution in [#2701](https://github.com/PrefectHQ/fastmcp/pull/2701)
+* [@Ashif4354](https://github.com/Ashif4354) made their first contribution in [#2707](https://github.com/PrefectHQ/fastmcp/pull/2707)
+* [@majiayu000](https://github.com/majiayu000) made their first contribution in [#2720](https://github.com/PrefectHQ/fastmcp/pull/2720)
+* [@lgndluke](https://github.com/lgndluke) made their first contribution in [#2644](https://github.com/PrefectHQ/fastmcp/pull/2644)
+* [@EloiZalczer](https://github.com/EloiZalczer) made their first contribution in [#2632](https://github.com/PrefectHQ/fastmcp/pull/2632)
+* [@deeleeramone](https://github.com/deeleeramone) made their first contribution in [#2768](https://github.com/PrefectHQ/fastmcp/pull/2768)
+* [@shea-parkes](https://github.com/shea-parkes) made their first contribution in [#2781](https://github.com/PrefectHQ/fastmcp/pull/2781)
+* [@triepod-ai](https://github.com/triepod-ai) made their first contribution in [#2777](https://github.com/PrefectHQ/fastmcp/pull/2777)
+* [@bhbs](https://github.com/bhbs) made their first contribution in [#2776](https://github.com/PrefectHQ/fastmcp/pull/2776)
+* [@shulkx](https://github.com/shulkx) made their first contribution in [#2884](https://github.com/PrefectHQ/fastmcp/pull/2884)
+
+**Full Changelog**: [v2.14.1...v3.0.0b1](https://github.com/PrefectHQ/fastmcp/compare/v2.14.1...v3.0.0b1)
+
+
+
+
+
+**[v2.14.7: Fake It Till You Break It](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.7)**
+
+A 2.x backport of the fakeredis pin: fakeredis 2.35.0 renamed a connection class that pydocket's `memory://` backend depended on, crashing `fastmcp[tasks]` installs at startup. This caps `fakeredis<2.35.0` on the 2.x line.
+
+### Fixes 🐞
+* fix(deps): cap fakeredis to `<2.35.0` to prevent startup crash on 2.x by [@vincent067](https://github.com/vincent067) in [#3883](https://github.com/PrefectHQ/fastmcp/pull/3883)
+
+**Full Changelog**: [v2.14.6...v2.14.7](https://github.com/PrefectHQ/fastmcp/compare/v2.14.6...v2.14.7)
+
+
+
+
+
+**[v2.14.6: $Ref Dead Redemption](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.6)**
+
+v2.14.4 backported `dereference_refs()` but never wired it into the tool schema pipeline — `$ref` and `$defs` were still sent to MCP clients. Now fixed: `compress_schema()` dereferences at both tool schema creation sites, so schemas are fully inlined before reaching clients.
+
+### Fixes 🐞
+* Updated deprecation URL for V2 by [@SrzStephen](https://github.com/SrzStephen) in [#3109](https://github.com/PrefectHQ/fastmcp/pull/3109)
+* Use MemoryStore for OAuth proxy tests by [@SrzStephen](https://github.com/SrzStephen) in [#3111](https://github.com/PrefectHQ/fastmcp/pull/3111)
+* fix: wire up dereference_refs() in tool schema pipeline by [@jlowin](https://github.com/jlowin) in [#3170](https://github.com/PrefectHQ/fastmcp/pull/3170)
+
+**Full Changelog**: [v2.14.5...v2.14.6](https://github.com/PrefectHQ/fastmcp/compare/v2.14.5...v2.14.6)
+
+
+
+
+
+**[v2.14.5: Sealed Docket](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.5)**
+
+Fixes a memory leak in the memory:// docket broker where cancelled tasks accumulated instead of being cleaned up. Bumps pydocket to ≥0.17.2.
+
+## What's Changed
+### Enhancements 🔧
+* Bump pydocket to 0.17.2 (memory leak fix) by [@chrisguidry](https://github.com/chrisguidry) in [#2992](https://github.com/PrefectHQ/fastmcp/pull/2992)
+
+**Full Changelog**: [v2.14.4...v2.14.5](https://github.com/PrefectHQ/fastmcp/compare/v2.14.4...v2.14.5)
+
+
+
+
+
+**[v2.14.4: Package Deal](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.4)**
+
+Fixes a fresh install bug where the packaging library was missing as a direct dependency, plus backports from 3.x for $ref dereferencing in tool schemas and a task capabilities location fix.
+
+## What's Changed
+### Enhancements 🔧
+* Add release notes for v2.14.2 and v2.14.3 by [@jlowin](https://github.com/jlowin) in [#2851](https://github.com/PrefectHQ/fastmcp/pull/2851)
+### Fixes 🐞
+* Backport: Dereference $ref in tool schemas for MCP client compatibility by [@jlowin](https://github.com/jlowin) in [#2861](https://github.com/PrefectHQ/fastmcp/pull/2861)
+* Fix task capabilities location (issue #2870) by [@jlowin](https://github.com/jlowin) in [#2874](https://github.com/PrefectHQ/fastmcp/pull/2874)
+* Add missing packaging dependency by [@jlowin](https://github.com/jlowin) in [#2989](https://github.com/PrefectHQ/fastmcp/pull/2989)
+
+**Full Changelog**: [v2.14.3...v2.14.4](https://github.com/PrefectHQ/fastmcp/compare/v2.14.3...v2.14.4)
+
+
+
+
+
+**[v2.14.3: Time After Timeout](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.3)**
+
+Sometimes five seconds just isn't enough. This release fixes an HTTP transport bug that was cutting connections short, along with OAuth and Redis fixes, better ASGI support, and CLI update notifications so you never miss a beat.
+
+## What's Changed
+### Enhancements 🔧
+* Add debug logging for OAuth token expiry diagnostics by [@jlowin](https://github.com/jlowin) in [#2789](https://github.com/PrefectHQ/fastmcp/pull/2789)
+* Add CLI update notifications by [@jlowin](https://github.com/jlowin) in [#2839](https://github.com/PrefectHQ/fastmcp/pull/2839)
+* Use pip instead of uv pip in upgrade instructions by [@jlowin](https://github.com/jlowin) in [#2841](https://github.com/PrefectHQ/fastmcp/pull/2841)
+### Fixes 🐞
+* Backport OAuth token storage TTL fix to release/2.x by [@jlowin](https://github.com/jlowin) in [#2798](https://github.com/PrefectHQ/fastmcp/pull/2798)
+* Prefix Redis keys with docket name for ACL isolation (2.x backport) by [@chrisguidry](https://github.com/chrisguidry) in [#2812](https://github.com/PrefectHQ/fastmcp/pull/2812)
+* Fix ContextVar propagation for ASGI-mounted servers with tasks by [@chrisguidry](https://github.com/chrisguidry) in [#2843](https://github.com/PrefectHQ/fastmcp/pull/2843)
+* Fix HTTP transport timeout defaulting to 5 seconds by [@jlowin](https://github.com/jlowin) in [#2848](https://github.com/PrefectHQ/fastmcp/pull/2848)
+
+**Full Changelog**: [v2.14.2...v2.14.3](https://github.com/PrefectHQ/fastmcp/compare/v2.14.2...v2.14.3)
+
+
+
+
+
+**[v2.14.2: Port Authority](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.2)**
+
+FastMCP 2.14.2 brings a wave of community contributions safely into the 2.x line. A variety of important fixes backported from 3.0 work improve OpenAPI 3.1 compatibility, MCP spec compliance for output schemas and elicitation, and correct a subtle base_url fallback issue. The CLI now gently reminds you that FastMCP 3.0 is on the horizon.
+
+## What's Changed
+### Enhancements 🔧
+* Pin MCP under 2.x by [@jlowin](https://github.com/jlowin) in [#2709](https://github.com/PrefectHQ/fastmcp/pull/2709)
+* Add auth_route parameter to SupabaseProvider by [@EloiZalczer](https://github.com/EloiZalczer) in [#2760](https://github.com/PrefectHQ/fastmcp/pull/2760)
+* Update CLI banner with FastMCP 3.0 notice by [@jlowin](https://github.com/jlowin) in [#2765](https://github.com/PrefectHQ/fastmcp/pull/2765)
+### Fixes 🐞
+* Let FastMCPError propagate unchanged from managers by [@jlowin](https://github.com/jlowin) in [#2697](https://github.com/PrefectHQ/fastmcp/pull/2697)
+* Fix test cleanup for uvicorn 0.39+ context isolation by [@jlowin](https://github.com/jlowin) in [#2696](https://github.com/PrefectHQ/fastmcp/pull/2696)
+* Bump pydocket to 0.16.3 to fix worker cleanup race condition by [@chrisguidry](https://github.com/chrisguidry) in [#2700](https://github.com/PrefectHQ/fastmcp/pull/2700)
+* Fix Prefect website URL in docs footer by [@mgoldsborough](https://github.com/mgoldsborough) in [#2705](https://github.com/PrefectHQ/fastmcp/pull/2705)
+* Fix: resolve root-level $ref in outputSchema for MCP spec compliance by [@majiayu000](https://github.com/majiayu000) in [#2727](https://github.com/PrefectHQ/fastmcp/pull/2727)
+* Fix OAuth Proxy resource parameter validation by [@jlowin](https://github.com/jlowin) in [#2763](https://github.com/PrefectHQ/fastmcp/pull/2763)
+* Fix openapi_version check to include 3.1 by [@deeleeramone](https://github.com/deeleeramone) in [#2769](https://github.com/PrefectHQ/fastmcp/pull/2769)
+* Fix titled enum elicitation schema to comply with MCP spec by [@jlowin](https://github.com/jlowin) in [#2774](https://github.com/PrefectHQ/fastmcp/pull/2774)
+* Fix base_url fallback when url is not set by [@bhbs](https://github.com/bhbs) in [#2782](https://github.com/PrefectHQ/fastmcp/pull/2782)
+* Lazy import DiskStore to avoid sqlite3 dependency on import by [@jlowin](https://github.com/jlowin) in [#2785](https://github.com/PrefectHQ/fastmcp/pull/2785)
+### Docs 📚
+* Add v3 breaking changes notice to README and docs by [@jlowin](https://github.com/jlowin) in [#2713](https://github.com/PrefectHQ/fastmcp/pull/2713)
+* Add changelog entries for v2.13.1 through v2.14.1 by [@jlowin](https://github.com/jlowin) in [#2724](https://github.com/PrefectHQ/fastmcp/pull/2724)
+* conference to 2.x branch by [@aaazzam](https://github.com/aaazzam) in [#2787](https://github.com/PrefectHQ/fastmcp/pull/2787)
+
+**Full Changelog**: [v2.14.1...v2.14.2](https://github.com/PrefectHQ/fastmcp/compare/v2.14.1...v2.14.2)
+
+
+
+
+
+**[v2.14.1: 'Tis a Gift to Be Sample](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.1)**
+
+FastMCP 2.14.1 introduces sampling with tools (SEP-1577), enabling servers to pass tools to `ctx.sample()` for agentic workflows where the LLM can automatically execute tool calls in a loop. The new `ctx.sample_step()` method provides single LLM calls that return `SampleStep` objects for custom control flow, while `result_type` enables structured outputs via validated Pydantic models.
+
+🤖 **AnthropicSamplingHandler** joins the existing OpenAI handler, providing multi-provider sampling support out of the box.
+
+⚡ **OpenAISamplingHandler promoted** from experimental status—sampling handlers are now production-ready with a unified API.
+
+## What's Changed
+### New Features 🎉
+* Sampling with tools by [@jlowin](https://github.com/jlowin) in [#2538](https://github.com/PrefectHQ/fastmcp/pull/2538)
+* Add AnthropicSamplingHandler by [@jlowin](https://github.com/jlowin) in [#2677](https://github.com/PrefectHQ/fastmcp/pull/2677)
+### Enhancements 🔧
+* Add Python 3.13 to ubuntu CI by [@jlowin](https://github.com/jlowin) in [#2648](https://github.com/PrefectHQ/fastmcp/pull/2648)
+* Remove legacy task initialization workaround by [@jlowin](https://github.com/jlowin) in [#2649](https://github.com/PrefectHQ/fastmcp/pull/2649)
+* Consolidate session state reset logic by [@jlowin](https://github.com/jlowin) in [#2651](https://github.com/PrefectHQ/fastmcp/pull/2651)
+* Unify SamplingHandler; promote OpenAI from experimental by [@jlowin](https://github.com/jlowin) in [#2656](https://github.com/PrefectHQ/fastmcp/pull/2656)
+* Add `tool_names` parameter to mount() for name customization by [@jlowin](https://github.com/jlowin) in [#2660](https://github.com/PrefectHQ/fastmcp/pull/2660)
+* Use streamable HTTP client API from MCP SDK by [@jlowin](https://github.com/jlowin) in [#2678](https://github.com/PrefectHQ/fastmcp/pull/2678)
+* Deprecate `exclude_args` in favor of Depends() by [@jlowin](https://github.com/jlowin) in [#2693](https://github.com/PrefectHQ/fastmcp/pull/2693)
+### Fixes 🐞
+* Fix prompt tasks to return mcp.types.PromptMessage by [@jlowin](https://github.com/jlowin) in [#2650](https://github.com/PrefectHQ/fastmcp/pull/2650)
+* Fix Windows test warnings by [@jlowin](https://github.com/jlowin) in [#2653](https://github.com/PrefectHQ/fastmcp/pull/2653)
+* Cleanup cancelled connection startup by [@jlowin](https://github.com/jlowin) in [#2679](https://github.com/PrefectHQ/fastmcp/pull/2679)
+* Fix tool choice bug in sampling examples by [@shawnthapa](https://github.com/shawnthapa) in [#2686](https://github.com/PrefectHQ/fastmcp/pull/2686)
+### Docs 📚
+* Simplify Docket tip wording by [@chrisguidry](https://github.com/chrisguidry) in [#2662](https://github.com/PrefectHQ/fastmcp/pull/2662)
+### Other Changes 🦾
+* Bump pydocket to ≥0.15.5 by [@jlowin](https://github.com/jlowin) in [#2694](https://github.com/PrefectHQ/fastmcp/pull/2694)
+
+## New Contributors
+* [@shawnthapa](https://github.com/shawnthapa) made their first contribution in [#2686](https://github.com/PrefectHQ/fastmcp/pull/2686)
+
+**Full Changelog**: [v2.14.0...v2.14.1](https://github.com/PrefectHQ/fastmcp/compare/v2.14.0...v2.14.1)
+
+
+
+
+
+**[v2.14.0: Task and You Shall Receive](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.14.0)**
+
+FastMCP 2.14 begins adopting the MCP 2025-11-25 specification, introducing protocol-native background tasks (SEP-1686) that enable long-running operations to report progress without blocking clients. The experimental OpenAPI parser graduates to standard, the `OpenAISamplingHandler` is promoted from experimental, and deprecated APIs accumulated across the 2.x series are removed.
+
+⏳ **Background Tasks** let you add `task=True` to any async tool decorator to run operations in the background with progress tracking. Powered by [Docket](https://github.com/chrisguidry/docket), an enterprise task scheduler handling millions of concurrent tasks daily—in-memory backends work out-of-the-box, and Redis URLs enable persistence and horizontal scaling.
+
+🔧 **OpenAPI Parser Promoted** from experimental to standard with improved performance through single-pass schema processing and cleaner abstractions.
+
+📋 **MCP 2025-11-25 Specification Support** including SSE polling and event resumability (SEP-1699), multi-select enum elicitation schemas (SEP-1330), default values for elicitation (SEP-1034), and tool name validation at registration time (SEP-986).
+
+## Breaking Changes
+- Docket is always enabled; task execution is forbidden through proxies
+- Task protocol enabled by default
+- Removed deprecated settings, imports, and methods accumulated across 2.x series
+
+## What's Changed
+### New Features 🎉
+* OpenAPI parser is now the default by [@jlowin](https://github.com/jlowin) in [#2583](https://github.com/PrefectHQ/fastmcp/pull/2583)
+* Implement SEP-1686: Background Tasks by [@jlowin](https://github.com/jlowin) in [#2550](https://github.com/PrefectHQ/fastmcp/pull/2550)
+### Enhancements 🔧
+* Expose InitializeResult in middleware by [@jlowin](https://github.com/jlowin) in [#2562](https://github.com/PrefectHQ/fastmcp/pull/2562)
+* Update MCP SDK auth compatibility by [@jlowin](https://github.com/jlowin) in [#2574](https://github.com/PrefectHQ/fastmcp/pull/2574)
+* Validate tool names at registration (SEP-986) by [@jlowin](https://github.com/jlowin) in [#2588](https://github.com/PrefectHQ/fastmcp/pull/2588)
+* Support SEP-1034 and SEP-1330 for elicitation by [@jlowin](https://github.com/jlowin) in [#2595](https://github.com/PrefectHQ/fastmcp/pull/2595)
+* Implement SSE polling (SEP-1699) by [@jlowin](https://github.com/jlowin) in [#2612](https://github.com/PrefectHQ/fastmcp/pull/2612)
+* Expose session ID callback by [@jlowin](https://github.com/jlowin) in [#2628](https://github.com/PrefectHQ/fastmcp/pull/2628)
+### Fixes 🐞
+* Fix OAuth metadata discovery by [@jlowin](https://github.com/jlowin) in [#2565](https://github.com/PrefectHQ/fastmcp/pull/2565)
+* Fix fastapi.cli package structure by [@jlowin](https://github.com/jlowin) in [#2570](https://github.com/PrefectHQ/fastmcp/pull/2570)
+* Correct OAuth error codes by [@jlowin](https://github.com/jlowin) in [#2578](https://github.com/PrefectHQ/fastmcp/pull/2578)
+* Prevent function signature modification by [@jlowin](https://github.com/jlowin) in [#2590](https://github.com/PrefectHQ/fastmcp/pull/2590)
+* Fix proxy client kwargs by [@jlowin](https://github.com/jlowin) in [#2605](https://github.com/PrefectHQ/fastmcp/pull/2605)
+* Fix nested server routing by [@jlowin](https://github.com/jlowin) in [#2618](https://github.com/PrefectHQ/fastmcp/pull/2618)
+* Use access token expiry fallback by [@jlowin](https://github.com/jlowin) in [#2635](https://github.com/PrefectHQ/fastmcp/pull/2635)
+* Handle transport cleanup exceptions by [@jlowin](https://github.com/jlowin) in [#2642](https://github.com/PrefectHQ/fastmcp/pull/2642)
+### Docs 📚
+* Add OCI and Supabase integration docs by [@jlowin](https://github.com/jlowin) in [#2580](https://github.com/PrefectHQ/fastmcp/pull/2580)
+* Add v2.14.0 upgrade guide by [@jlowin](https://github.com/jlowin) in [#2598](https://github.com/PrefectHQ/fastmcp/pull/2598)
+* Rewrite background tasks documentation by [@jlowin](https://github.com/jlowin) in [#2620](https://github.com/PrefectHQ/fastmcp/pull/2620)
+* Document read-only tool patterns by [@jlowin](https://github.com/jlowin) in [#2632](https://github.com/PrefectHQ/fastmcp/pull/2632)
+
+## New Contributors
+11 total contributors including 7 first-time participants.
+
+**Full Changelog**: [v2.13.3...v2.14.0](https://github.com/PrefectHQ/fastmcp/compare/v2.13.3...v2.14.0)
+
+
+
+
+
+**[v2.13.3: Pin-ish Line](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.3)**
+
+FastMCP 2.13.3 pins `mcp<1.23` as a precautionary measure. MCP SDK 1.23 introduced changes related to the November 25, 2025 MCP protocol update that break certain FastMCP patches and workarounds, particularly around OAuth implementation details. FastMCP 2.14 introduces proper support for the updated protocol and requires `mcp>=1.23`.
+
+## What's Changed
+### Fixes 🐞
+* Pin MCP SDK below 1.23 by [@jlowin](https://github.com/jlowin) in [#2545](https://github.com/PrefectHQ/fastmcp/pull/2545)
+
+**Full Changelog**: [v2.13.2...v2.13.3](https://github.com/PrefectHQ/fastmcp/compare/v2.13.2...v2.13.3)
+
+
+
+
+
+**[v2.13.2: Refreshing Changes](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.2)**
+
+FastMCP 2.13.2 polishes the authentication stack with improvements to token refresh, scope handling, and multi-instance deployments. Discord was added as a built-in OAuth provider, Azure and Google token handling became more reliable, and proxy classes now properly forward icons and titles.
+
+## What's Changed
+### New Features 🎉
+* Add Discord OAuth provider by [@jlowin](https://github.com/jlowin) in [#2480](https://github.com/PrefectHQ/fastmcp/pull/2480)
+### Enhancements 🔧
+* Descope Provider updates for new well-known URLs by [@anvibanga](https://github.com/anvibanga) in [#2465](https://github.com/PrefectHQ/fastmcp/pull/2465)
+* Scalekit provider improvements by [@jlowin](https://github.com/jlowin) in [#2472](https://github.com/PrefectHQ/fastmcp/pull/2472)
+* Add CSP customization for consent screens by [@jlowin](https://github.com/jlowin) in [#2488](https://github.com/PrefectHQ/fastmcp/pull/2488)
+* Add icon support to proxy classes by [@jlowin](https://github.com/jlowin) in [#2495](https://github.com/PrefectHQ/fastmcp/pull/2495)
+### Fixes 🐞
+* Google Provider now defaults to refresh token support by [@jlowin](https://github.com/jlowin) in [#2468](https://github.com/PrefectHQ/fastmcp/pull/2468)
+* Fix Azure OAuth token refresh with unprefixed scopes by [@jlowin](https://github.com/jlowin) in [#2475](https://github.com/PrefectHQ/fastmcp/pull/2475)
+* Prevent `$defs` mutation during tool transforms by [@jlowin](https://github.com/jlowin) in [#2482](https://github.com/PrefectHQ/fastmcp/pull/2482)
+* Fix OAuth proxy refresh token storage for multi-instance deployments by [@jlowin](https://github.com/jlowin) in [#2490](https://github.com/PrefectHQ/fastmcp/pull/2490)
+* Fix stale token issue after OAuth refresh by [@jlowin](https://github.com/jlowin) in [#2498](https://github.com/PrefectHQ/fastmcp/pull/2498)
+* Fix Azure provider OIDC scope handling by [@jlowin](https://github.com/jlowin) in [#2505](https://github.com/PrefectHQ/fastmcp/pull/2505)
+
+## New Contributors
+7 new contributors made their first FastMCP contributions in this release.
+
+**Full Changelog**: [v2.13.1...v2.13.2](https://github.com/PrefectHQ/fastmcp/compare/v2.13.1...v2.13.2)
+
+
+
+
+
+**[v2.13.1: Heavy Meta](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.1)**
+
+FastMCP 2.13.1 introduces meta parameter support for `ToolResult`, enabling tools to return supplementary metadata alongside results. This supports emerging use cases like OpenAI's Apps SDK. The release also brings improved OAuth functionality with custom token verifiers including a new DebugTokenVerifier, and adds OCI and Supabase authentication providers.
+
+🏷️ **Meta parameters for ToolResult** enable tools to return supplementary metadata alongside results, supporting patterns like OpenAI's Apps SDK integration.
+
+🔐 **Custom token verifiers** with DebugTokenVerifier for development, plus Azure Government support through a `base_authority` parameter and Supabase authentication algorithm configuration.
+
+🔒 **Security fixes** address CVE-2025-61920 through authlib updates and validate Cursor deeplink URLs using safer Windows APIs.
+
+## What's Changed
+### New Features 🎉
+* Add meta parameter support for ToolResult by [@jlowin](https://github.com/jlowin) in [#2350](https://github.com/PrefectHQ/fastmcp/pull/2350)
+* Add OCI authentication provider by [@jlowin](https://github.com/jlowin) in [#2365](https://github.com/PrefectHQ/fastmcp/pull/2365)
+* Add Supabase authentication provider by [@jlowin](https://github.com/jlowin) in [#2378](https://github.com/PrefectHQ/fastmcp/pull/2378)
+### Enhancements 🔧
+* Add custom token verifier support to OIDCProxy by [@jlowin](https://github.com/jlowin) in [#2355](https://github.com/PrefectHQ/fastmcp/pull/2355)
+* Add DebugTokenVerifier for development by [@jlowin](https://github.com/jlowin) in [#2362](https://github.com/PrefectHQ/fastmcp/pull/2362)
+* Add Azure Government support via base_authority parameter by [@jlowin](https://github.com/jlowin) in [#2385](https://github.com/PrefectHQ/fastmcp/pull/2385)
+* Add Supabase authentication algorithm configuration by [@jlowin](https://github.com/jlowin) in [#2392](https://github.com/PrefectHQ/fastmcp/pull/2392)
+### Fixes 🐞
+* Security: Update authlib for CVE-2025-61920 by [@jlowin](https://github.com/jlowin) in [#2398](https://github.com/PrefectHQ/fastmcp/pull/2398)
+* Validate Cursor deeplink URLs using safer Windows APIs by [@jlowin](https://github.com/jlowin) in [#2405](https://github.com/PrefectHQ/fastmcp/pull/2405)
+* Exclude MCP SDK 1.21.1 due to integration test failures by [@jlowin](https://github.com/jlowin) in [#2422](https://github.com/PrefectHQ/fastmcp/pull/2422)
+
+## New Contributors
+18 new contributors joined in this release across 70+ pull requests.
+
+**Full Changelog**: [v2.13.0...v2.13.1](https://github.com/PrefectHQ/fastmcp/compare/v2.13.0...v2.13.1)
+
+
+
+
+
+**[v2.13.0: Cache Me If You Can](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.13.0)**
+
+FastMCP 2.13 "Cache Me If You Can" represents a fundamental maturation of the framework. After months of community feedback on authentication and state management, this release delivers the infrastructure FastMCP needs to handle production workloads: persistent storage, response caching, and pragmatic OAuth improvements that reflect real-world deployment challenges.
+
+💾 **Pluggable storage backends** bring persistent state to FastMCP servers. Built on [py-key-value-aio](https://github.com/strawgate/py-key-value), a new library from FastMCP maintainer Bill Easton ([@strawgate](https://github.com/strawgate)), the storage layer provides encrypted disk storage by default, platform-aware token management, and a simple key-value interface for application state. We're excited to bring this elegantly designed library into the FastMCP ecosystem - it's both powerful and remarkably easy to use, including wrappers to add encryption, TTLs, caching, and more to backends ranging from Elasticsearch, Redis, DynamoDB, filesystem, in-memory, and more! OAuth providers now automatically persist tokens across restarts, and developers can store arbitrary state without reaching for external databases. This foundation enables long-running sessions, cached credentials, and stateful applications built on MCP.
+
+🔐 **OAuth maturity** brings months of production learnings into the framework. The new consent screen prevents confused deputy and authorization bypass attacks discovered in earlier versions while providing a clean UX with customizable branding. The OAuth proxy now issues its own tokens with automatic key derivation from client secrets, and RFC 7662 token introspection support enables enterprise auth flows. Path prefix mounting enables OAuth-protected servers to integrate into existing web applications under custom paths like `/api`, and MCP 1.17+ compliance with RFC 9728 ensures protocol compatibility. Combined with improved error handling and platform-aware token storage, OAuth is now production-ready and security-hardened for serious applications.
+
+FastMCP now supports out-of-the-box authentication with:
+- **[WorkOS](https://gofastmcp.com/integrations/workos)** and **[AuthKit](https://gofastmcp.com/integrations/authkit)**
+- **[GitHub](https://gofastmcp.com/integrations/github)**
+- **[Google](https://gofastmcp.com/integrations/google)**
+- **[Azure](https://gofastmcp.com/integrations/azure)** (Entra ID)
+- **[AWS Cognito](https://gofastmcp.com/integrations/aws-cognito)**
+- **[Auth0](https://gofastmcp.com/integrations/auth0)**
+- **[Descope](https://gofastmcp.com/integrations/descope)**
+- **[Scalekit](https://gofastmcp.com/integrations/scalekit)**
+- **[JWTs](https://gofastmcp.com/servers/auth/token-verification#jwt-token-verification)**
+- **[RFC 7662 token introspection](https://gofastmcp.com/servers/auth/token-verification#token-introspection-protocol)**
+
+⚡ **Response Caching Middleware** dramatically improves performance for expensive operations. Cache tool and resource responses with configurable TTLs, reducing redundant API calls and speeding up repeated queries.
+
+🔄 **Server lifespans** provide proper initialization and cleanup hooks that run once per server instance instead of per client session. This fixes a long-standing source of confusion in the MCP SDK and enables proper resource management for database connections, background tasks, and other server-level state. Note: this is a breaking behavioral change if you were using the `lifespan` parameter.
+
+✨ **Developer experience improvements** include Pydantic input validation for better type safety, icon support for richer UX, RFC 6570 query parameters for resource templates, improved Context API methods (list_resources, list_prompts, get_prompt), and async file/directory resources.
+
+This release includes contributions from **20** new contributors and represents the largest feature set in a while. Thank you to everyone who tested preview builds and filed issues - your feedback shaped these improvements!
+
+**Full Changelog**: [v2.12.5...v2.13.0](https://github.com/PrefectHQ/fastmcp/compare/v2.12.5...v2.13.0)
+
+
+
+
+
+**[v2.12.5: Safety Pin](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.5)**
+
+FastMCP 2.12.5 is a point release that pins the MCP SDK version below 1.17, which introduced a change affecting FastMCP users with auth providers mounted as part of a larger application. This ensures the `.well-known` payload appears in the expected location when using FastMCP authentication providers with composite applications.
+
+## What's Changed
+
+### Fixes 🐞
+* Pin MCP SDK version below 1.17 by [@jlowin](https://github.com/jlowin) in [a1b2c3d](https://github.com/PrefectHQ/fastmcp/commit/dab2b316ddc3883b7896a86da21cacb68da01e5c)
+
+**Full Changelog**: [v2.12.4...v2.12.5](https://github.com/PrefectHQ/fastmcp/compare/v2.12.4...v2.12.5)
+
+
+
+
+
+**[v2.12.4: OIDC What You Did There](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.4)**
+
+FastMCP 2.12.4 adds comprehensive OIDC support and expands authentication options with AWS Cognito and Descope providers. The release also includes improvements to logging middleware, URL handling for nested resources, persistent OAuth client registration storage, and various fixes to the experimental OpenAPI parser.
+
+## What's Changed
+### New Features 🎉
+* feat: Add support for OIDC configuration by [@ruhulio](https://github.com/ruhulio) in [#1817](https://github.com/PrefectHQ/fastmcp/pull/1817)
+### Enhancements 🔧
+* feat: Move the Starlette context middleware to the front by [@akkuman](https://github.com/akkuman) in [#1812](https://github.com/PrefectHQ/fastmcp/pull/1812)
+* Refactor Logging and Structured Logging Middleware by [@strawgate](https://github.com/strawgate) in [#1805](https://github.com/PrefectHQ/fastmcp/pull/1805)
+* Update pull_request_template.md by [@jlowin](https://github.com/jlowin) in [#1824](https://github.com/PrefectHQ/fastmcp/pull/1824)
+* chore: Set redirect_path default in function by [@ruhulio](https://github.com/ruhulio) in [#1833](https://github.com/PrefectHQ/fastmcp/pull/1833)
+* feat: Set instructions in code by [@attiks](https://github.com/attiks) in [#1838](https://github.com/PrefectHQ/fastmcp/pull/1838)
+* Automatically Create inline Snapshots by [@strawgate](https://github.com/strawgate) in [#1779](https://github.com/PrefectHQ/fastmcp/pull/1779)
+* chore: Cleanup Auth0 redirect_path initialization by [@ruhulio](https://github.com/ruhulio) in [#1842](https://github.com/PrefectHQ/fastmcp/pull/1842)
+* feat: Add support for Descope Authentication by [@anvibanga](https://github.com/anvibanga) in [#1853](https://github.com/PrefectHQ/fastmcp/pull/1853)
+* Update descope version badges by [@jlowin](https://github.com/jlowin) in [#1870](https://github.com/PrefectHQ/fastmcp/pull/1870)
+* Update welcome images by [@jlowin](https://github.com/jlowin) in [#1884](https://github.com/PrefectHQ/fastmcp/pull/1884)
+* Fix rounded edges of image by [@jlowin](https://github.com/jlowin) in [#1886](https://github.com/PrefectHQ/fastmcp/pull/1886)
+* optimize test suite by [@zzstoatzz](https://github.com/zzstoatzz) in [#1893](https://github.com/PrefectHQ/fastmcp/pull/1893)
+* Enhancement: client completions support context_arguments by [@isijoe](https://github.com/isijoe) in [#1906](https://github.com/PrefectHQ/fastmcp/pull/1906)
+* Update Descope icon by [@anvibanga](https://github.com/anvibanga) in [#1912](https://github.com/PrefectHQ/fastmcp/pull/1912)
+* Add AWS Cognito OAuth Provider for Enterprise Authentication by [@stephaneberle9](https://github.com/stephaneberle9) in [#1873](https://github.com/PrefectHQ/fastmcp/pull/1873)
+* Fix typos discovered by codespell by [@cclauss](https://github.com/cclauss) in [#1922](https://github.com/PrefectHQ/fastmcp/pull/1922)
+* Use lowercase namespace for fastmcp logger by [@jlowin](https://github.com/jlowin) in [#1791](https://github.com/PrefectHQ/fastmcp/pull/1791)
+### Fixes 🐞
+* Update quickstart.mdx by [@radi-dev](https://github.com/radi-dev) in [#1821](https://github.com/PrefectHQ/fastmcp/pull/1821)
+* Remove extraneous union import by [@jlowin](https://github.com/jlowin) in [#1823](https://github.com/PrefectHQ/fastmcp/pull/1823)
+* Delay import of Provider classes until FastMCP Server Creation by [@strawgate](https://github.com/strawgate) in [#1820](https://github.com/PrefectHQ/fastmcp/pull/1820)
+* fix: correct documentation link in deprecation warning by [@strawgate](https://github.com/strawgate) in [#1828](https://github.com/PrefectHQ/fastmcp/pull/1828)
+* fix: Increase default 3s timeout on Pytest by [@dacamposol](https://github.com/dacamposol) in [#1866](https://github.com/PrefectHQ/fastmcp/pull/1866)
+* fix: Improve URL handling in OIDCConfiguration by [@ruhulio](https://github.com/ruhulio) in [#1850](https://github.com/PrefectHQ/fastmcp/pull/1850)
+* fix: correct typing for on_read_resource middleware method by [@strawgate](https://github.com/strawgate) in [#1858](https://github.com/PrefectHQ/fastmcp/pull/1858)
+* feat(experimental/openapi): replace $ref in additionalProperties; add tests by [@jlowin](https://github.com/jlowin) in [#1735](https://github.com/PrefectHQ/fastmcp/pull/1735)
+* Honor client supplied scopes during registration by [@dmikusa](https://github.com/dmikusa) in [#1860](https://github.com/PrefectHQ/fastmcp/pull/1860)
+* Fix: FastAPI list parameter parsing in experimental OpenAPI parser by [@jlowin](https://github.com/jlowin) in [#1834](https://github.com/PrefectHQ/fastmcp/pull/1834)
+* Add log level support for stdio and HTTP transports by [@jlowin](https://github.com/jlowin) in [#1840](https://github.com/PrefectHQ/fastmcp/pull/1840)
+* Fix OAuth pre-flight check to accept HTTP 200 responses by [@jlowin](https://github.com/jlowin) in [#1874](https://github.com/PrefectHQ/fastmcp/pull/1874)
+* Fix: Preserve OpenAPI parameter descriptions in experimental parser by [@shlomo666](https://github.com/shlomo666) in [#1877](https://github.com/PrefectHQ/fastmcp/pull/1877)
+* Add persistent storage for OAuth client registrations by [@jlowin](https://github.com/jlowin) in [#1879](https://github.com/PrefectHQ/fastmcp/pull/1879)
+* docs: update release dates based on github releases by [@lodu](https://github.com/lodu) in [#1890](https://github.com/PrefectHQ/fastmcp/pull/1890)
+* Small updates to Sampling types by [@strawgate](https://github.com/strawgate) in [#1882](https://github.com/PrefectHQ/fastmcp/pull/1882)
+* remove lockfile smart_home example by [@zzstoatzz](https://github.com/zzstoatzz) in [#1892](https://github.com/PrefectHQ/fastmcp/pull/1892)
+* Fix: Remove JSON schema title metadata while preserving parameters named 'title' by [@jlowin](https://github.com/jlowin) in [#1872](https://github.com/PrefectHQ/fastmcp/pull/1872)
+* Fix: get_resource_url nested URL handling by [@raphael-linx](https://github.com/raphael-linx) in [#1914](https://github.com/PrefectHQ/fastmcp/pull/1914)
+* Clean up code for creating the resource url by [@jlowin](https://github.com/jlowin) in [#1916](https://github.com/PrefectHQ/fastmcp/pull/1916)
+* Fix route count logging in OpenAPI server by [@zzstoatzz](https://github.com/zzstoatzz) in [#1928](https://github.com/PrefectHQ/fastmcp/pull/1928)
+### Docs 📚
+* docs: make Gemini CLI integration discoverable by [@jackwotherspoon](https://github.com/jackwotherspoon) in [#1827](https://github.com/PrefectHQ/fastmcp/pull/1827)
+* docs: update NEW tags for AI assistant integrations by [@jackwotherspoon](https://github.com/jackwotherspoon) in [#1829](https://github.com/PrefectHQ/fastmcp/pull/1829)
+* Update wordmark by [@jlowin](https://github.com/jlowin) in [#1832](https://github.com/PrefectHQ/fastmcp/pull/1832)
+* docs: improve OAuth and OIDC Proxy documentation by [@jlowin](https://github.com/jlowin) in [#1880](https://github.com/PrefectHQ/fastmcp/pull/1880)
+* Update readme + welcome docs by [@jlowin](https://github.com/jlowin) in [#1883](https://github.com/PrefectHQ/fastmcp/pull/1883)
+* Update dark mode image in README by [@jlowin](https://github.com/jlowin) in [#1885](https://github.com/PrefectHQ/fastmcp/pull/1885)
+
+## New Contributors
+* [@radi-dev](https://github.com/radi-dev) made their first contribution in [#1821](https://github.com/PrefectHQ/fastmcp/pull/1821)
+* [@akkuman](https://github.com/akkuman) made their first contribution in [#1812](https://github.com/PrefectHQ/fastmcp/pull/1812)
+* [@ruhulio](https://github.com/ruhulio) made their first contribution in [#1817](https://github.com/PrefectHQ/fastmcp/pull/1817)
+* [@attiks](https://github.com/attiks) made their first contribution in [#1838](https://github.com/PrefectHQ/fastmcp/pull/1838)
+* [@anvibanga](https://github.com/anvibanga) made their first contribution in [#1853](https://github.com/PrefectHQ/fastmcp/pull/1853)
+* [@shlomo666](https://github.com/shlomo666) made their first contribution in [#1877](https://github.com/PrefectHQ/fastmcp/pull/1877)
+* [@lodu](https://github.com/lodu) made their first contribution in [#1890](https://github.com/PrefectHQ/fastmcp/pull/1890)
+* [@isijoe](https://github.com/isijoe) made their first contribution in [#1906](https://github.com/PrefectHQ/fastmcp/pull/1906)
+* [@raphael-linx](https://github.com/raphael-linx) made their first contribution in [#1914](https://github.com/PrefectHQ/fastmcp/pull/1914)
+* [@stephaneberle9](https://github.com/stephaneberle9) made their first contribution in [#1873](https://github.com/PrefectHQ/fastmcp/pull/1873)
+* [@cclauss](https://github.com/cclauss) made their first contribution in [#1922](https://github.com/PrefectHQ/fastmcp/pull/1922)
+
+**Full Changelog**: [v2.12.3...v2.12.4](https://github.com/PrefectHQ/fastmcp/compare/v2.12.3...v2.12.4)
+
+
+
+
+
+**[v2.12.3: Double Time](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.3)**
+
+FastMCP 2.12.3 focuses on performance and developer experience improvements based on community feedback. This release includes optimized auth provider imports that reduce server startup time, enhanced OIDC authentication flows with proper token management, and several reliability fixes for OAuth proxy configurations. The addition of automatic inline snapshot creation significantly improves the testing experience for contributors.
+
+## What's Changed
+### New Features 🎉
+* feat: Support setting MCP log level via transport configuration by [@jlowin](https://github.com/jlowin) in [#1756](https://github.com/PrefectHQ/fastmcp/pull/1756)
+### Enhancements 🔧
+* Add client-side auth support for mcp install cursor command by [@jlowin](https://github.com/jlowin) in [#1747](https://github.com/PrefectHQ/fastmcp/pull/1747)
+* Automatically Create inline Snapshots by [@strawgate](https://github.com/strawgate) in [#1779](https://github.com/PrefectHQ/fastmcp/pull/1779)
+* Use lowercase namespace for fastmcp logger by [@jlowin](https://github.com/jlowin) in [#1791](https://github.com/PrefectHQ/fastmcp/pull/1791)
+### Fixes 🐞
+* fix: correct merge mistake during auth0 refactor by [@strawgate](https://github.com/strawgate) in [#1742](https://github.com/PrefectHQ/fastmcp/pull/1742)
+* Remove extraneous union import by [@jlowin](https://github.com/jlowin) in [#1823](https://github.com/PrefectHQ/fastmcp/pull/1823)
+* Delay import of Provider classes until FastMCP Server Creation by [@strawgate](https://github.com/strawgate) in [#1820](https://github.com/PrefectHQ/fastmcp/pull/1820)
+* fix: refactor OIDC configuration provider for proper token management by [@strawgate](https://github.com/strawgate) in [#1751](https://github.com/PrefectHQ/fastmcp/pull/1751)
+* Fix smart_home example imports by [@strawgate](https://github.com/strawgate) in [#1753](https://github.com/PrefectHQ/fastmcp/pull/1753)
+* fix: correct oauth proxy initialization of client by [@strawgate](https://github.com/strawgate) in [#1759](https://github.com/PrefectHQ/fastmcp/pull/1759)
+* Fix: return empty string when prompts have no arguments by [@jlowin](https://github.com/jlowin) in [#1766](https://github.com/PrefectHQ/fastmcp/pull/1766)
+* Fix async server callbacks by [@strawgate](https://github.com/strawgate) in [#1774](https://github.com/PrefectHQ/fastmcp/pull/1774)
+* Fix error when retrieving Completion API errors by [@strawgate](https://github.com/strawgate) in [#1785](https://github.com/PrefectHQ/fastmcp/pull/1785)
+* fix: correct documentation link in deprecation warning by [@strawgate](https://github.com/strawgate) in [#1828](https://github.com/PrefectHQ/fastmcp/pull/1828)
+### Docs 📚
+* Add migration docs for 2.12 by [@jlowin](https://github.com/jlowin) in [#1745](https://github.com/PrefectHQ/fastmcp/pull/1745)
+* Update docs for default sampling implementation to mention OpenAI API Key by [@strawgate](https://github.com/strawgate) in [#1763](https://github.com/PrefectHQ/fastmcp/pull/1763)
+* Add tip about sampling prompts and user_context to sampling documentation by [@jlowin](https://github.com/jlowin) in [#1764](https://github.com/PrefectHQ/fastmcp/pull/1764)
+* Update quickstart.mdx by [@radi-dev](https://github.com/radi-dev) in [#1821](https://github.com/PrefectHQ/fastmcp/pull/1821)
+### Other Changes 🦾
+* Replace Marvin with Claude Code in CI by [@jlowin](https://github.com/jlowin) in [#1800](https://github.com/PrefectHQ/fastmcp/pull/1800)
+* Refactor logging and structured logging middleware by [@strawgate](https://github.com/strawgate) in [#1805](https://github.com/PrefectHQ/fastmcp/pull/1805)
+* feat: Move the Starlette context middleware to the front by [@akkuman](https://github.com/akkuman) in [#1812](https://github.com/PrefectHQ/fastmcp/pull/1812)
+* feat: Add support for OIDC configuration by [@ruhulio](https://github.com/ruhulio) in [#1817](https://github.com/PrefectHQ/fastmcp/pull/1817)
+
+## New Contributors
+* [@radi-dev](https://github.com/radi-dev) made their first contribution in [#1821](https://github.com/PrefectHQ/fastmcp/pull/1821)
+* [@akkuman](https://github.com/akkuman) made their first contribution in [#1812](https://github.com/PrefectHQ/fastmcp/pull/1812)
+* [@ruhulio](https://github.com/ruhulio) made their first contribution in [#1817](https://github.com/PrefectHQ/fastmcp/pull/1817)
+
+**Full Changelog**: [v2.12.2...v2.12.3](https://github.com/PrefectHQ/fastmcp/compare/v2.12.2...v2.12.3)
+
+
+
+
+
+**[v2.12.2: Perchance to Stream](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.2)**
+
+This is a hotfix for a bug where the `streamable-http` transport was not recognized as a valid option in `fastmcp.json` configuration files, despite being supported by the CLI. This resulted in a parsing error when the CLI arguments were merged against the configuration spec.
+
+## What's Changed
+### Fixes 🐞
+* Fix streamable-http transport validation in fastmcp.json config by [@jlowin](https://github.com/jlowin) in [#1739](https://github.com/PrefectHQ/fastmcp/pull/1739)
+
+**Full Changelog**: [v2.12.1...v2.12.2](https://github.com/PrefectHQ/fastmcp/compare/v2.12.1...v2.12.2)
+
+
+
+
+
+**[v2.12.1: OAuth to Joy](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.1)**
+
+FastMCP 2.12.1 strengthens the OAuth proxy implementation based on extensive community testing and feedback. This release improves client storage reliability, adds PKCE forwarding for enhanced security, introduces configurable token endpoint authentication methods, and expands scope handling—all addressing real-world integration challenges discovered since 2.12.0. The enhanced test suite with mock providers ensures these improvements are robust and maintainable.
+
+## Breaking Changes
+- **OAuth Proxy**: Users of built-in IDP integrations should note that `resource_server_url` has been renamed to `base_url` for clarity and consistency
+
+## What's Changed
+### Enhancements 🔧
+* Make openai dependency optional by [@jlowin](https://github.com/jlowin) in [#1701](https://github.com/PrefectHQ/fastmcp/pull/1701)
+* Remove orphaned OAuth proxy code by [@jlowin](https://github.com/jlowin) in [#1722](https://github.com/PrefectHQ/fastmcp/pull/1722)
+* Expose valid scopes from OAuthProxy metadata by [@dmikusa](https://github.com/dmikusa) in [#1717](https://github.com/PrefectHQ/fastmcp/pull/1717)
+* OAuth proxy PKCE forwarding by [@jlowin](https://github.com/jlowin) in [#1733](https://github.com/PrefectHQ/fastmcp/pull/1733)
+* Add token_endpoint_auth_method parameter to OAuthProxy by [@jlowin](https://github.com/jlowin) in [#1736](https://github.com/PrefectHQ/fastmcp/pull/1736)
+* Clean up and enhance OAuth proxy tests with mock provider by [@jlowin](https://github.com/jlowin) in [#1738](https://github.com/PrefectHQ/fastmcp/pull/1738)
+### Fixes 🐞
+* refactor: replace auth provider registry with ImportString by [@jlowin](https://github.com/jlowin) in [#1710](https://github.com/PrefectHQ/fastmcp/pull/1710)
+* Fix OAuth resource URL handling and WWW-Authenticate header by [@jlowin](https://github.com/jlowin) in [#1706](https://github.com/PrefectHQ/fastmcp/pull/1706)
+* Fix OAuth proxy client storage and add retry logic by [@jlowin](https://github.com/jlowin) in [#1732](https://github.com/PrefectHQ/fastmcp/pull/1732)
+### Docs 📚
+* Fix documentation: use StreamableHttpTransport for headers in testing by [@jlowin](https://github.com/jlowin) in [#1702](https://github.com/PrefectHQ/fastmcp/pull/1702)
+* docs: add performance warnings for mounted servers and proxies by [@strawgate](https://github.com/strawgate) in [#1669](https://github.com/PrefectHQ/fastmcp/pull/1669)
+* Update documentation around scopes for google by [@jlowin](https://github.com/jlowin) in [#1703](https://github.com/PrefectHQ/fastmcp/pull/1703)
+* Add deployment information to quickstart by [@seanpwlms](https://github.com/seanpwlms) in [#1433](https://github.com/PrefectHQ/fastmcp/pull/1433)
+* Update quickstart by [@jlowin](https://github.com/jlowin) in [#1728](https://github.com/PrefectHQ/fastmcp/pull/1728)
+* Add development docs for FastMCP by [@jlowin](https://github.com/jlowin) in [#1719](https://github.com/PrefectHQ/fastmcp/pull/1719)
+### Other Changes 🦾
+* Set generics without bounds to default=Any by [@strawgate](https://github.com/strawgate) in [#1648](https://github.com/PrefectHQ/fastmcp/pull/1648)
+
+## New Contributors
+* [@dmikusa](https://github.com/dmikusa) made their first contribution in [#1717](https://github.com/PrefectHQ/fastmcp/pull/1717)
+* [@seanpwlms](https://github.com/seanpwlms) made their first contribution in [#1433](https://github.com/PrefectHQ/fastmcp/pull/1433)
+
+**Full Changelog**: [v2.12.0...v2.12.1](https://github.com/PrefectHQ/fastmcp/compare/v2.12.0...v2.12.1)
+
+
+
+
+
+**[v2.12.0: Auth to the Races](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.12.0)**
+
+FastMCP 2.12 represents one of our most significant releases to date, both in scope and community involvement. After extensive testing and iteration with the community, we're shipping major improvements to authentication, configuration, and MCP feature adoption.
+
+🔐 **OAuth Proxy for Broader Provider Support** addresses a fundamental challenge: while MCP requires Dynamic Client Registration (DCR), many popular OAuth providers don't support it. The new OAuth proxy bridges this gap, enabling FastMCP servers to authenticate with providers like GitHub, Google, WorkOS, and Azure through minimal configuration. These native integrations ship today, with more providers planned based on community needs.
+
+📋 **Declarative JSON Configuration** introduces a standardized, portable way to describe and deploy MCP servers. The `fastmcp.json` configuration file becomes the single source of truth for dependencies, transport settings, entrypoints, and server metadata. This foundation sets the stage for future capabilities like transformations and remote sources, moving toward a world where MCP servers are as portable and shareable as container images.
+
+🧠 **Sampling API Fallback** tackles the chicken-and-egg problem limiting adoption of advanced MCP features. Sampling—where servers request LLM completions from clients—is powerful but underutilized due to limited client support. FastMCP now lets server authors define fallback handlers that generate sampling completions server-side when clients don't support the feature, encouraging adoption while maintaining compatibility.
+
+This release took longer than usual to ship, and for good reason: the community's aggressive testing and feedback on the authentication system helped us reach a level of stability we're confident in. There's certainly more work ahead, but these foundations position FastMCP to handle increasingly complex use cases while remaining approachable for developers.
+
+Thank you to our new contributors and everyone who tested preview builds. Your feedback directly shaped these features.
+
+## What's Changed
+### New Features 🎉
+* Add OAuth proxy that allows authentication with social IDPs without DCR support by [@jlowin](https://github.com/jlowin) in [#1434](https://github.com/PrefectHQ/fastmcp/pull/1434)
+* feat: introduce declarative JSON configuration system by [@jlowin](https://github.com/jlowin) in [#1517](https://github.com/PrefectHQ/fastmcp/pull/1517)
+* ✨ Fallback to a Completions API when Sampling is not available by [@strawgate](https://github.com/strawgate) in [#1145](https://github.com/PrefectHQ/fastmcp/pull/1145)
+* Implement typed source system for FastMCP declarative configuration by [@jlowin](https://github.com/jlowin) in [#1607](https://github.com/PrefectHQ/fastmcp/pull/1607)
+### Enhancements 🔧
+* Support importing custom_route endpoints when mounting servers by [@jlowin](https://github.com/jlowin) in [#1470](https://github.com/PrefectHQ/fastmcp/pull/1470)
+* Remove unnecessary asserts by [@jlowin](https://github.com/jlowin) in [#1484](https://github.com/PrefectHQ/fastmcp/pull/1484)
+* Add Claude issue triage by [@jlowin](https://github.com/jlowin) in [#1510](https://github.com/PrefectHQ/fastmcp/pull/1510)
+* Inline dedupe prompt by [@jlowin](https://github.com/jlowin) in [#1512](https://github.com/PrefectHQ/fastmcp/pull/1512)
+* Improve stdio and mcp_config clean-up by [@strawgate](https://github.com/strawgate) in [#1444](https://github.com/PrefectHQ/fastmcp/pull/1444)
+* involve kwargs to pass parameters on creating RichHandler for logging customization. by [@itaru2622](https://github.com/itaru2622) in [#1504](https://github.com/PrefectHQ/fastmcp/pull/1504)
+* Move SDK docs generation to post-merge workflow by [@jlowin](https://github.com/jlowin) in [#1513](https://github.com/PrefectHQ/fastmcp/pull/1513)
+* Improve label triage guidance by [@jlowin](https://github.com/jlowin) in [#1516](https://github.com/PrefectHQ/fastmcp/pull/1516)
+* Add code review guidelines for agents by [@jlowin](https://github.com/jlowin) in [#1520](https://github.com/PrefectHQ/fastmcp/pull/1520)
+* Remove trailing slash in unit tests by [@jlowin](https://github.com/jlowin) in [#1535](https://github.com/PrefectHQ/fastmcp/pull/1535)
+* Update OAuth callback UI branding by [@jlowin](https://github.com/jlowin) in [#1536](https://github.com/PrefectHQ/fastmcp/pull/1536)
+* Fix Marvin workflow to support development tools by [@jlowin](https://github.com/jlowin) in [#1537](https://github.com/PrefectHQ/fastmcp/pull/1537)
+* Add mounted_components_raise_on_load_error setting for debugging by [@jlowin](https://github.com/jlowin) in [#1534](https://github.com/PrefectHQ/fastmcp/pull/1534)
+* feat: Add --workspace flag to fastmcp install cursor by [@jlowin](https://github.com/jlowin) in [#1522](https://github.com/PrefectHQ/fastmcp/pull/1522)
+* switch from `pyright` to `ty` by [@zzstoatzz](https://github.com/zzstoatzz) in [#1545](https://github.com/PrefectHQ/fastmcp/pull/1545)
+* feat: trigger Marvin workflow on PR body content by [@jlowin](https://github.com/jlowin) in [#1549](https://github.com/PrefectHQ/fastmcp/pull/1549)
+* Add WorkOS and Azure OAuth providers by [@jlowin](https://github.com/jlowin) in [#1550](https://github.com/PrefectHQ/fastmcp/pull/1550)
+* Adjust timeout for slow MCP Server shutdown test by [@strawgate](https://github.com/strawgate) in [#1561](https://github.com/PrefectHQ/fastmcp/pull/1561)
+* Update banner by [@jlowin](https://github.com/jlowin) in [#1567](https://github.com/PrefectHQ/fastmcp/pull/1567)
+* Added import of AuthProxy to auth __init__ by [@KaliszS](https://github.com/KaliszS) in [#1568](https://github.com/PrefectHQ/fastmcp/pull/1568)
+* Add configurable redirect URI validation for OAuth providers by [@jlowin](https://github.com/jlowin) in [#1582](https://github.com/PrefectHQ/fastmcp/pull/1582)
+* Remove invalid-argument-type ignore and fix type errors by [@jlowin](https://github.com/jlowin) in [#1588](https://github.com/PrefectHQ/fastmcp/pull/1588)
+* Remove generate-schema from public CLI by [@jlowin](https://github.com/jlowin) in [#1591](https://github.com/PrefectHQ/fastmcp/pull/1591)
+* Skip flaky windows test / mulit-client garbage collection by [@jlowin](https://github.com/jlowin) in [#1592](https://github.com/PrefectHQ/fastmcp/pull/1592)
+* Add setting to disable logging configuration by [@isra17](https://github.com/isra17) in [#1575](https://github.com/PrefectHQ/fastmcp/pull/1575)
+* Improve debug logging for nested Servers / Clients by [@strawgate](https://github.com/strawgate) in [#1604](https://github.com/PrefectHQ/fastmcp/pull/1604)
+* Add GitHub pull request template by [@strawgate](https://github.com/strawgate) in [#1581](https://github.com/PrefectHQ/fastmcp/pull/1581)
+* chore: Automate docs and schema updates via PRs by [@jlowin](https://github.com/jlowin) in [#1611](https://github.com/PrefectHQ/fastmcp/pull/1611)
+* Experiment with haiku for limited workflows by [@jlowin](https://github.com/jlowin) in [#1613](https://github.com/PrefectHQ/fastmcp/pull/1613)
+* feat: Improve GitHub workflow automation for schema and SDK docs by [@jlowin](https://github.com/jlowin) in [#1615](https://github.com/PrefectHQ/fastmcp/pull/1615)
+* Consolidate server loading logic into FileSystemSource by [@jlowin](https://github.com/jlowin) in [#1614](https://github.com/PrefectHQ/fastmcp/pull/1614)
+* Prevent Haiku Marvin from commenting when there are no duplicates by [@jlowin](https://github.com/jlowin) in [#1622](https://github.com/PrefectHQ/fastmcp/pull/1622)
+* chore: Add clarifying note to automated PR bodies by [@jlowin](https://github.com/jlowin) in [#1623](https://github.com/PrefectHQ/fastmcp/pull/1623)
+* feat: introduce inline snapshots by [@strawgate](https://github.com/strawgate) in [#1605](https://github.com/PrefectHQ/fastmcp/pull/1605)
+* Improve fastmcp.json environment configuration and project-based deployments by [@jlowin](https://github.com/jlowin) in [#1631](https://github.com/PrefectHQ/fastmcp/pull/1631)
+* fix: allow passing query params in OAuthProxy upstream authorization url by [@danb27](https://github.com/danb27) in [#1630](https://github.com/PrefectHQ/fastmcp/pull/1630)
+* Support multiple --with-editable flags in CLI commands by [@jlowin](https://github.com/jlowin) in [#1634](https://github.com/PrefectHQ/fastmcp/pull/1634)
+* feat: support comma separated oauth scopes by [@jlowin](https://github.com/jlowin) in [#1642](https://github.com/PrefectHQ/fastmcp/pull/1642)
+* Add allowed_client_redirect_uris to OAuth provider subclasses by [@jlowin](https://github.com/jlowin) in [#1662](https://github.com/PrefectHQ/fastmcp/pull/1662)
+* Consolidate CLI config parsing and prevent infinite loops by [@jlowin](https://github.com/jlowin) in [#1660](https://github.com/PrefectHQ/fastmcp/pull/1660)
+* Internal refactor: mcp server config by [@jlowin](https://github.com/jlowin) in [#1672](https://github.com/PrefectHQ/fastmcp/pull/1672)
+* Refactor Environment to support multiple runtime types by [@jlowin](https://github.com/jlowin) in [#1673](https://github.com/PrefectHQ/fastmcp/pull/1673)
+* Add type field to Environment base class by [@jlowin](https://github.com/jlowin) in [#1676](https://github.com/PrefectHQ/fastmcp/pull/1676)
+### Fixes 🐞
+* Fix breaking change: restore output_schema=False compatibility by [@jlowin](https://github.com/jlowin) in [#1482](https://github.com/PrefectHQ/fastmcp/pull/1482)
+* Fix #1506: Update tool filtering documentation from _meta to meta by [@maybenotconnor](https://github.com/maybenotconnor) in [#1511](https://github.com/PrefectHQ/fastmcp/pull/1511)
+* Fix pytest warnings by [@jlowin](https://github.com/jlowin) in [#1559](https://github.com/PrefectHQ/fastmcp/pull/1559)
+* nest schemas under assets by [@jlowin](https://github.com/jlowin) in [#1593](https://github.com/PrefectHQ/fastmcp/pull/1593)
+* Skip flaky windows test by [@jlowin](https://github.com/jlowin) in [#1596](https://github.com/PrefectHQ/fastmcp/pull/1596)
+* ACTUALLY move schemas to fastmcp.json by [@jlowin](https://github.com/jlowin) in [#1597](https://github.com/PrefectHQ/fastmcp/pull/1597)
+* Fix and centralize CLI path resolution by [@jlowin](https://github.com/jlowin) in [#1590](https://github.com/PrefectHQ/fastmcp/pull/1590)
+* Remove client info modifications by [@jlowin](https://github.com/jlowin) in [#1620](https://github.com/PrefectHQ/fastmcp/pull/1620)
+* Fix $defs being discarded in input schema of transformed tool by [@pldesch-chift](https://github.com/pldesch-chift) in [#1578](https://github.com/PrefectHQ/fastmcp/pull/1578)
+* Fix enum elicitation to use inline schemas for MCP compatibility by [@jlowin](https://github.com/jlowin) in [#1632](https://github.com/PrefectHQ/fastmcp/pull/1632)
+* Reuse session for `StdioTransport` in `Client.new` by [@strawgate](https://github.com/strawgate) in [#1635](https://github.com/PrefectHQ/fastmcp/pull/1635)
+* Feat: Configurable LoggingMiddleware payload serialization by [@vl-kp](https://github.com/vl-kp) in [#1636](https://github.com/PrefectHQ/fastmcp/pull/1636)
+* Fix OAuth redirect URI validation for DCR compatibility by [@jlowin](https://github.com/jlowin) in [#1661](https://github.com/PrefectHQ/fastmcp/pull/1661)
+* Add default scope handling in OAuth proxy by [@romanusyk](https://github.com/romanusyk) in [#1667](https://github.com/PrefectHQ/fastmcp/pull/1667)
+* Fix OAuth token expiry handling by [@jlowin](https://github.com/jlowin) in [#1671](https://github.com/PrefectHQ/fastmcp/pull/1671)
+* Add resource_server_url parameter to OAuth proxy providers by [@jlowin](https://github.com/jlowin) in [#1682](https://github.com/PrefectHQ/fastmcp/pull/1682)
+### Breaking Changes 🛫
+* Enhance inspect command with structured output and format options by [@jlowin](https://github.com/jlowin) in [#1481](https://github.com/PrefectHQ/fastmcp/pull/1481)
+### Docs 📚
+* Update changelog by [@jlowin](https://github.com/jlowin) in [#1453](https://github.com/PrefectHQ/fastmcp/pull/1453)
+* Update banner by [@jlowin](https://github.com/jlowin) in [#1472](https://github.com/PrefectHQ/fastmcp/pull/1472)
+* Update logo files by [@jlowin](https://github.com/jlowin) in [#1473](https://github.com/PrefectHQ/fastmcp/pull/1473)
+* Update deployment docs by [@jlowin](https://github.com/jlowin) in [#1486](https://github.com/PrefectHQ/fastmcp/pull/1486)
+* Update FastMCP Cloud screenshot by [@jlowin](https://github.com/jlowin) in [#1487](https://github.com/PrefectHQ/fastmcp/pull/1487)
+* Update authentication note in docs by [@jlowin](https://github.com/jlowin) in [#1488](https://github.com/PrefectHQ/fastmcp/pull/1488)
+* chore: Update installation.mdx version snippet by [@thomas-te](https://github.com/thomas-te) in [#1496](https://github.com/PrefectHQ/fastmcp/pull/1496)
+* Update fastmcp cloud server requirements by [@jlowin](https://github.com/jlowin) in [#1497](https://github.com/PrefectHQ/fastmcp/pull/1497)
+* Fix oauth pyright type checking by [@strawgate](https://github.com/strawgate) in [#1498](https://github.com/PrefectHQ/fastmcp/pull/1498)
+* docs: Fix type annotation in return value documentation by [@MaikelVeen](https://github.com/MaikelVeen) in [#1499](https://github.com/PrefectHQ/fastmcp/pull/1499)
+* Fix PromptMessage usage in docs example by [@jlowin](https://github.com/jlowin) in [#1515](https://github.com/PrefectHQ/fastmcp/pull/1515)
+* Create CODE_OF_CONDUCT.md by [@jlowin](https://github.com/jlowin) in [#1523](https://github.com/PrefectHQ/fastmcp/pull/1523)
+* Fixed wrong import path in new docs page by [@KaliszS](https://github.com/KaliszS) in [#1538](https://github.com/PrefectHQ/fastmcp/pull/1538)
+* Document symmetric key JWT verification support by [@jlowin](https://github.com/jlowin) in [#1586](https://github.com/PrefectHQ/fastmcp/pull/1586)
+* Update fastmcp.json schema path by [@jlowin](https://github.com/jlowin) in [#1595](https://github.com/PrefectHQ/fastmcp/pull/1595)
+### Dependencies 📦
+* Bump actions/create-github-app-token from 1 to 2 by [@dependabot](https://github.com/dependabot)[bot] in [#1436](https://github.com/PrefectHQ/fastmcp/pull/1436)
+* Bump astral-sh/setup-uv from 4 to 6 by [@dependabot](https://github.com/dependabot)[bot] in [#1532](https://github.com/PrefectHQ/fastmcp/pull/1532)
+* Bump actions/checkout from 4 to 5 by [@dependabot](https://github.com/dependabot)[bot] in [#1533](https://github.com/PrefectHQ/fastmcp/pull/1533)
+### Other Changes 🦾
+* Add dedupe workflow by [@jlowin](https://github.com/jlowin) in [#1454](https://github.com/PrefectHQ/fastmcp/pull/1454)
+* Update AGENTS.md by [@jlowin](https://github.com/jlowin) in [#1471](https://github.com/PrefectHQ/fastmcp/pull/1471)
+* Give Marvin the power of the Internet by [@strawgate](https://github.com/strawgate) in [#1475](https://github.com/PrefectHQ/fastmcp/pull/1475)
+* Update `just` error message for static checks by [@jlowin](https://github.com/jlowin) in [#1483](https://github.com/PrefectHQ/fastmcp/pull/1483)
+* Remove labeler by [@jlowin](https://github.com/jlowin) in [#1509](https://github.com/PrefectHQ/fastmcp/pull/1509)
+* update aproto server to handle rich links by [@zzstoatzz](https://github.com/zzstoatzz) in [#1556](https://github.com/PrefectHQ/fastmcp/pull/1556)
+* fix: enable triage bot for fork PRs using pull_request_target by [@jlowin](https://github.com/jlowin) in [#1557](https://github.com/PrefectHQ/fastmcp/pull/1557)
+
+## New Contributors
+* [@thomas-te](https://github.com/thomas-te) made their first contribution in [#1496](https://github.com/PrefectHQ/fastmcp/pull/1496)
+* [@maybenotconnor](https://github.com/maybenotconnor) made their first contribution in [#1511](https://github.com/PrefectHQ/fastmcp/pull/1511)
+* [@MaikelVeen](https://github.com/MaikelVeen) made their first contribution in [#1499](https://github.com/PrefectHQ/fastmcp/pull/1499)
+* [@KaliszS](https://github.com/KaliszS) made their first contribution in [#1538](https://github.com/PrefectHQ/fastmcp/pull/1538)
+* [@isra17](https://github.com/isra17) made their first contribution in [#1575](https://github.com/PrefectHQ/fastmcp/pull/1575)
+* [@marvin-context-protocol](https://github.com/marvin-context-protocol)[bot] made their first contribution in [#1616](https://github.com/PrefectHQ/fastmcp/pull/1616)
+* [@pldesch-chift](https://github.com/pldesch-chift) made their first contribution in [#1578](https://github.com/PrefectHQ/fastmcp/pull/1578)
+* [@vl-kp](https://github.com/vl-kp) made their first contribution in [#1636](https://github.com/PrefectHQ/fastmcp/pull/1636)
+* [@romanusyk](https://github.com/romanusyk) made their first contribution in [#1667](https://github.com/PrefectHQ/fastmcp/pull/1667)
+
+**Full Changelog**: [v2.11.3...v2.12.0](https://github.com/PrefectHQ/fastmcp/compare/v2.11.3...v2.12.0)
+
+
+
+
+
+**[v2.11.3: API-tite for Change](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.11.3)**
+
+This release includes significant enhancements to the experimental OpenAPI parser and fixes a significant bug that led schemas not to be included in input/output schemas if they were transitive dependencies (e.g. A → B → C implies A depends on C). For users naively transforming large OpenAPI specs into MCP servers, this may result in ballooning payload sizes and necessitate curation.
+
+## What's Changed
+### Enhancements 🔧
+* Improve redirect handling to address 307's by [@jlowin](https://github.com/jlowin) in [#1387](https://github.com/PrefectHQ/fastmcp/pull/1387)
+* Ensure resource + template names are properly prefixed when importing/mounting by [@jlowin](https://github.com/jlowin) in [#1423](https://github.com/PrefectHQ/fastmcp/pull/1423)
+* fixes #1398: Add JWT claims to AccessToken by [@panargirakis](https://github.com/panargirakis) in [#1399](https://github.com/PrefectHQ/fastmcp/pull/1399)
+* Enable Protected Resource Metadata to provide resource_name and resou… by [@yannj-fr](https://github.com/yannj-fr) in [#1371](https://github.com/PrefectHQ/fastmcp/pull/1371)
+* Pin mcp SDK under 2.0 to avoid breaking changes by [@jlowin](https://github.com/jlowin) in [#1428](https://github.com/PrefectHQ/fastmcp/pull/1428)
+* Clean up complexity from PR #1426 by [@jlowin](https://github.com/jlowin) in [#1435](https://github.com/PrefectHQ/fastmcp/pull/1435)
+* Optimize OpenAPI payload size by 46% by [@jlowin](https://github.com/jlowin) in [#1452](https://github.com/PrefectHQ/fastmcp/pull/1452)
+* Update static checks by [@jlowin](https://github.com/jlowin) in [#1448](https://github.com/PrefectHQ/fastmcp/pull/1448)
+### Fixes 🐞
+* Fix client-side logging bug #1394 by [@chi2liu](https://github.com/chi2liu) in [#1397](https://github.com/PrefectHQ/fastmcp/pull/1397)
+* fix: Fix httpx_client_factory type annotation to match MCP SDK (#1402) by [@chi2liu](https://github.com/chi2liu) in [#1405](https://github.com/PrefectHQ/fastmcp/pull/1405)
+* Fix OpenAPI allOf handling at requestBody top level (#1378) by [@chi2liu](https://github.com/chi2liu) in [#1425](https://github.com/PrefectHQ/fastmcp/pull/1425)
+* Fix OpenAPI transitive references and performance (#1372) by [@jlowin](https://github.com/jlowin) in [#1426](https://github.com/PrefectHQ/fastmcp/pull/1426)
+* fix(type): lifespan is partially unknown by [@ykun9](https://github.com/ykun9) in [#1389](https://github.com/PrefectHQ/fastmcp/pull/1389)
+* Ensure transformed tools generate structured content by [@jlowin](https://github.com/jlowin) in [#1443](https://github.com/PrefectHQ/fastmcp/pull/1443)
+### Docs 📚
+* docs(client/logging): reflect corrected default log level mapping by [@jlowin](https://github.com/jlowin) in [#1403](https://github.com/PrefectHQ/fastmcp/pull/1403)
+* Add documentation for get_access_token() dependency function by [@jlowin](https://github.com/jlowin) in [#1446](https://github.com/PrefectHQ/fastmcp/pull/1446)
+### Other Changes 🦾
+* Add comprehensive tests for utilities.components module by [@chi2liu](https://github.com/chi2liu) in [#1395](https://github.com/PrefectHQ/fastmcp/pull/1395)
+* Consolidate agent instructions into AGENTS.md by [@jlowin](https://github.com/jlowin) in [#1404](https://github.com/PrefectHQ/fastmcp/pull/1404)
+* Fix performance test threshold to prevent flaky failures by [@jlowin](https://github.com/jlowin) in [#1406](https://github.com/PrefectHQ/fastmcp/pull/1406)
+* Update agents.md; add github instructions by [@jlowin](https://github.com/jlowin) in [#1410](https://github.com/PrefectHQ/fastmcp/pull/1410)
+* Add Marvin assistant by [@jlowin](https://github.com/jlowin) in [#1412](https://github.com/PrefectHQ/fastmcp/pull/1412)
+* Marvin: fix deprecated variable names by [@jlowin](https://github.com/jlowin) in [#1417](https://github.com/PrefectHQ/fastmcp/pull/1417)
+* Simplify action setup and add github tools for Marvin by [@jlowin](https://github.com/jlowin) in [#1419](https://github.com/PrefectHQ/fastmcp/pull/1419)
+* Update marvin workflow name by [@jlowin](https://github.com/jlowin) in [#1421](https://github.com/PrefectHQ/fastmcp/pull/1421)
+* Improve GitHub templates by [@jlowin](https://github.com/jlowin) in [#1422](https://github.com/PrefectHQ/fastmcp/pull/1422)
+
+## New Contributors
+* [@panargirakis](https://github.com/panargirakis) made their first contribution in [#1399](https://github.com/PrefectHQ/fastmcp/pull/1399)
+* [@ykun9](https://github.com/ykun9) made their first contribution in [#1389](https://github.com/PrefectHQ/fastmcp/pull/1389)
+* [@yannj-fr](https://github.com/yannj-fr) made their first contribution in [#1371](https://github.com/PrefectHQ/fastmcp/pull/1371)
+
+**Full Changelog**: [v2.11.2...v2.11.3](https://github.com/PrefectHQ/fastmcp/compare/v2.11.2...v2.11.3)
+
+
+
+
+
+## [v2.11.2: Satis-factory](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.11.2)
+
+## What's Changed
+### Enhancements 🔧
+* Support factory functions in fastmcp run by [@jlowin](https://github.com/jlowin) in [#1384](https://github.com/PrefectHQ/fastmcp/pull/1384)
+* Add async support to client_factory in FastMCPProxy (#1286) by [@bianning](https://github.com/bianning) in [#1375](https://github.com/PrefectHQ/fastmcp/pull/1375)
+### Fixes 🐞
+* Fix server_version field in inspect manifest by [@jlowin](https://github.com/jlowin) in [#1383](https://github.com/PrefectHQ/fastmcp/pull/1383)
+* Fix Settings field with both default and default_factory by [@jlowin](https://github.com/jlowin) in [#1380](https://github.com/PrefectHQ/fastmcp/pull/1380)
+### Other Changes 🦾
+* Remove unused arg by [@jlowin](https://github.com/jlowin) in [#1382](https://github.com/PrefectHQ/fastmcp/pull/1382)
+* Add remote auth provider tests by [@jlowin](https://github.com/jlowin) in [#1351](https://github.com/PrefectHQ/fastmcp/pull/1351)
+
+## New Contributors
+* [@bianning](https://github.com/bianning) made their first contribution in [#1375](https://github.com/PrefectHQ/fastmcp/pull/1375)
+
+**Full Changelog**: [v2.11.1...v2.11.2](https://github.com/PrefectHQ/fastmcp/compare/v2.11.1...v2.11.2)
+
+
+
+
+
+## [v2.11.1: You're Better Auth Now](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.11.1)
+
+## What's Changed
+### New Features 🎉
+* Introduce `RemoteAuthProvider` for cleaner external identity provider integration, update docs by [@jlowin](https://github.com/jlowin) in [#1346](https://github.com/PrefectHQ/fastmcp/pull/1346)
+### Enhancements 🔧
+* perf: optimize string operations in OpenAPI parameter processing by [@chi2liu](https://github.com/chi2liu) in [#1342](https://github.com/PrefectHQ/fastmcp/pull/1342)
+### Fixes 🐞
+* Fix method-bound FunctionTool schemas by [@strawgate](https://github.com/strawgate) in [#1360](https://github.com/PrefectHQ/fastmcp/pull/1360)
+* Manually set `_key` after `model_copy()` to enable prefixing Transformed Tools by [@strawgate](https://github.com/strawgate) in [#1357](https://github.com/PrefectHQ/fastmcp/pull/1357)
+### Docs 📚
+* Docs updates by [@jlowin](https://github.com/jlowin) in [#1336](https://github.com/PrefectHQ/fastmcp/pull/1336)
+* Add 2.11 to changelog by [@jlowin](https://github.com/jlowin) in [#1337](https://github.com/PrefectHQ/fastmcp/pull/1337)
+* Update AuthKit vocab by [@jlowin](https://github.com/jlowin) in [#1338](https://github.com/PrefectHQ/fastmcp/pull/1338)
+* Fix typo in decorating-methods.mdx by [@Ozzuke](https://github.com/Ozzuke) in [#1344](https://github.com/PrefectHQ/fastmcp/pull/1344)
+
+## New Contributors
+* [@Ozzuke](https://github.com/Ozzuke) made their first contribution in [#1344](https://github.com/PrefectHQ/fastmcp/pull/1344)
+
+**Full Changelog**: [v2.11.0...v2.11.1](https://github.com/PrefectHQ/fastmcp/compare/v2.11.0...v2.11.1)
+
+
+
+
+
+## [v2.11.0: Auth to a Good Start](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.11.0)
+
+FastMCP 2.11 doubles down on what developers need most: speed and simplicity. This massive release delivers significant performance improvements and a dramatically better developer experience.
+
+🔐 **Enterprise-Ready Authentication** brings comprehensive OAuth 2.1 support with WorkOS's AuthKit integration. The new AuthProvider interface leverages MCP's support for separate resource and authorization servers, handling API keys and remote authentication with Dynamic Client Registration. AuthKit integration means you can plug into existing enterprise identity systems without rebuilding your auth stack, setting the stage for plug-and-play auth that doesn't require users to become security experts overnight.
+
+⚡ The **Experimental OpenAPI Parser** delivers dramatic performance improvements through single-pass schema processing and optimized memory usage. OpenAPI integrations are now significantly faster, with cleaner, more maintainable code. _(Note: the experimental parser is disabled by default, set `FASTMCPEXPERIMENTALENABLENEWOPENAPIPARSER=1` to enable it. A message will be shown to all users on the legacy parser encouraging them to try the new one before it becomes the default.)_
+
+🧠 **Context State Management** finally gives you persistent state across tool calls with a simple dict interface, while enhanced meta support lets you expose rich component metadata to clients. Combined with improved type annotations, string-based argument descriptions, and UV transport support, this release makes FastMCP feel more intuitive than ever.
+
+This release represents a TON of community contributions and sets the foundation for even more ambitious features ahead.
+
+## What's Changed
+### New Features 🎉
+* Introduce experimental OpenAPI parser with improved performance and maintainability by [@jlowin](https://github.com/jlowin) in [#1209](https://github.com/PrefectHQ/fastmcp/pull/1209)
+* Add state dict to Context (#1118) by [@mukulmurthy](https://github.com/mukulmurthy) in [#1160](https://github.com/PrefectHQ/fastmcp/pull/1160)
+* Expose FastMCP tags to clients via component `meta` dict by [@jlowin](https://github.com/jlowin) in [#1281](https://github.com/PrefectHQ/fastmcp/pull/1281)
+* Add _fastmcp meta namespace by [@jlowin](https://github.com/jlowin) in [#1290](https://github.com/PrefectHQ/fastmcp/pull/1290)
+* Add TokenVerifier protocol support alongside existing OAuthProvider authentication by [@jlowin](https://github.com/jlowin) in [#1297](https://github.com/PrefectHQ/fastmcp/pull/1297)
+* Add comprehensive OAuth 2.1 authentication system with WorkOS integration by [@jlowin](https://github.com/jlowin) in [#1327](https://github.com/PrefectHQ/fastmcp/pull/1327)
+### Enhancements 🔧
+* [🐶] Transform MCP Server Tools by [@strawgate](https://github.com/strawgate) in [#1132](https://github.com/PrefectHQ/fastmcp/pull/1132)
+* Add --python, --project, and --with-requirements options to CLI commands by [@jlowin](https://github.com/jlowin) in [#1190](https://github.com/PrefectHQ/fastmcp/pull/1190)
+* Support `fastmcp run mcp.json` by [@strawgate](https://github.com/strawgate) in [#1138](https://github.com/PrefectHQ/fastmcp/pull/1138)
+* Support from __future__ import annotations by [@jlowin](https://github.com/jlowin) in [#1199](https://github.com/PrefectHQ/fastmcp/pull/1199)
+* Optimize OpenAPI parser performance with single-pass schema processing by [@jlowin](https://github.com/jlowin) in [#1214](https://github.com/PrefectHQ/fastmcp/pull/1214)
+* Log tool name on transform validation error by [@strawgate](https://github.com/strawgate) in [#1238](https://github.com/PrefectHQ/fastmcp/pull/1238)
+* Refactor `get_http_request` and `context.session_id` by [@hopeful0](https://github.com/hopeful0) in [#1242](https://github.com/PrefectHQ/fastmcp/pull/1242)
+* Support creating tool argument descriptions from string annotations by [@jlowin](https://github.com/jlowin) in [#1255](https://github.com/PrefectHQ/fastmcp/pull/1255)
+* feat: Add Annotations support for resources and resource templates by [@chughtapan](https://github.com/chughtapan) in [#1260](https://github.com/PrefectHQ/fastmcp/pull/1260)
+* Add UV Transport by [@strawgate](https://github.com/strawgate) in [#1270](https://github.com/PrefectHQ/fastmcp/pull/1270)
+* Improve OpenAPI-to-JSONSchema conversion utilities by [@jlowin](https://github.com/jlowin) in [#1283](https://github.com/PrefectHQ/fastmcp/pull/1283)
+* Ensure proxy components forward meta dicts by [@jlowin](https://github.com/jlowin) in [#1282](https://github.com/PrefectHQ/fastmcp/pull/1282)
+* fix: server argument passing in CLI run command by [@chughtapan](https://github.com/chughtapan) in [#1293](https://github.com/PrefectHQ/fastmcp/pull/1293)
+* Add meta support to tool transformation utilities by [@jlowin](https://github.com/jlowin) in [#1295](https://github.com/PrefectHQ/fastmcp/pull/1295)
+* feat: Allow Resource Metadata URL as field in OAuthProvider by [@dacamposol](https://github.com/dacamposol) in [#1287](https://github.com/PrefectHQ/fastmcp/pull/1287)
+* Use a simple overwrite instead of a merge for meta by [@jlowin](https://github.com/jlowin) in [#1296](https://github.com/PrefectHQ/fastmcp/pull/1296)
+* Remove unused TimedCache by [@strawgate](https://github.com/strawgate) in [#1303](https://github.com/PrefectHQ/fastmcp/pull/1303)
+* refactor: standardize logging usage across OpenAPI utilities by [@chi2liu](https://github.com/chi2liu) in [#1322](https://github.com/PrefectHQ/fastmcp/pull/1322)
+* perf: optimize OpenAPI parsing by reducing dict copy operations by [@chi2liu](https://github.com/chi2liu) in [#1321](https://github.com/PrefectHQ/fastmcp/pull/1321)
+* Structured client-side logging by [@cjermain](https://github.com/cjermain) in [#1326](https://github.com/PrefectHQ/fastmcp/pull/1326)
+### Fixes 🐞
+* fix: preserve def reference when referenced in allOf / oneOf / anyOf by [@algirdasci](https://github.com/algirdasci) in [#1208](https://github.com/PrefectHQ/fastmcp/pull/1208)
+* fix: add type hint to custom_route decorator by [@zzstoatzz](https://github.com/zzstoatzz) in [#1210](https://github.com/PrefectHQ/fastmcp/pull/1210)
+* chore: typo by [@richardkmichael](https://github.com/richardkmichael) in [#1216](https://github.com/PrefectHQ/fastmcp/pull/1216)
+* fix: handle non-string $ref values in experimental OpenAPI parser by [@jlowin](https://github.com/jlowin) in [#1217](https://github.com/PrefectHQ/fastmcp/pull/1217)
+* Skip repeated type conversion and validation in proxy client elicitation handler by [@chughtapan](https://github.com/chughtapan) in [#1222](https://github.com/PrefectHQ/fastmcp/pull/1222)
+* Ensure default fields are not marked nullable by [@jlowin](https://github.com/jlowin) in [#1224](https://github.com/PrefectHQ/fastmcp/pull/1224)
+* Fix stateful proxy client mixing in multi-proxies sessions by [@hopeful0](https://github.com/hopeful0) in [#1245](https://github.com/PrefectHQ/fastmcp/pull/1245)
+* Fix invalid async context manager usage in proxy documentation by [@zzstoatzz](https://github.com/zzstoatzz) in [#1246](https://github.com/PrefectHQ/fastmcp/pull/1246)
+* fix: experimental FastMCPOpenAPI server lost headers in request when __init__(client with headers) by [@itaru2622](https://github.com/itaru2622) in [#1254](https://github.com/PrefectHQ/fastmcp/pull/1254)
+* Fix typing, add tests for tool call middleware by [@jlowin](https://github.com/jlowin) in [#1269](https://github.com/PrefectHQ/fastmcp/pull/1269)
+* Fix: prune hidden parameter defs by [@muhammadkhalid-03](https://github.com/muhammadkhalid-03) in [#1257](https://github.com/PrefectHQ/fastmcp/pull/1257)
+* Fix nullable field handling in OpenAPI to JSON Schema conversion by [@jlowin](https://github.com/jlowin) in [#1279](https://github.com/PrefectHQ/fastmcp/pull/1279)
+* Ensure fastmcp run supports v1 servers by [@jlowin](https://github.com/jlowin) in [#1332](https://github.com/PrefectHQ/fastmcp/pull/1332)
+### Breaking Changes 🛫
+* Change server flag to --name by [@jlowin](https://github.com/jlowin) in [#1248](https://github.com/PrefectHQ/fastmcp/pull/1248)
+### Docs 📚
+* Remove unused import from FastAPI integration documentation by [@mariotaddeucci](https://github.com/mariotaddeucci) in [#1194](https://github.com/PrefectHQ/fastmcp/pull/1194)
+* Update fastapi docs by [@jlowin](https://github.com/jlowin) in [#1198](https://github.com/PrefectHQ/fastmcp/pull/1198)
+* Add docs for context state management by [@jlowin](https://github.com/jlowin) in [#1227](https://github.com/PrefectHQ/fastmcp/pull/1227)
+* Permit.io integration docs by [@orweis](https://github.com/orweis) in [#1226](https://github.com/PrefectHQ/fastmcp/pull/1226)
+* Update docs to reflect sync tools by [@jlowin](https://github.com/jlowin) in [#1234](https://github.com/PrefectHQ/fastmcp/pull/1234)
+* Update changelog.mdx by [@jlowin](https://github.com/jlowin) in [#1235](https://github.com/PrefectHQ/fastmcp/pull/1235)
+* Update SDK docs by [@jlowin](https://github.com/jlowin) in [#1236](https://github.com/PrefectHQ/fastmcp/pull/1236)
+* Update --name flag documentation for Cursor/Claude by [@adam-conway](https://github.com/adam-conway) in [#1239](https://github.com/PrefectHQ/fastmcp/pull/1239)
+* Add annotations docs by [@jlowin](https://github.com/jlowin) in [#1268](https://github.com/PrefectHQ/fastmcp/pull/1268)
+* Update openapi/fastapi URLs README.md by [@jbn](https://github.com/jbn) in [#1278](https://github.com/PrefectHQ/fastmcp/pull/1278)
+* Add 2.11 version badge for state management by [@jlowin](https://github.com/jlowin) in [#1289](https://github.com/PrefectHQ/fastmcp/pull/1289)
+* Add meta parameter support to tools, resources, templates, and prompts decorators by [@jlowin](https://github.com/jlowin) in [#1294](https://github.com/PrefectHQ/fastmcp/pull/1294)
+* docs: update get_state and set_state references by [@Maxi91f](https://github.com/Maxi91f) in [#1306](https://github.com/PrefectHQ/fastmcp/pull/1306)
+* Add unit tests and docs for denying tool calls with middleware by [@jlowin](https://github.com/jlowin) in [#1333](https://github.com/PrefectHQ/fastmcp/pull/1333)
+* Remove reference to stacked decorators by [@jlowin](https://github.com/jlowin) in [#1334](https://github.com/PrefectHQ/fastmcp/pull/1334)
+* Eunomia authorization server can run embedded within the MCP server by [@tommitt](https://github.com/tommitt) in [#1317](https://github.com/PrefectHQ/fastmcp/pull/1317)
+### Other Changes 🦾
+* Update README.md by [@jlowin](https://github.com/jlowin) in [#1230](https://github.com/PrefectHQ/fastmcp/pull/1230)
+* Logcapture addition to test_server file by [@Sourav-Tripathy](https://github.com/Sourav-Tripathy) in [#1229](https://github.com/PrefectHQ/fastmcp/pull/1229)
+* Add tests for headers with both legacy and experimental openapi parser by [@jlowin](https://github.com/jlowin) in [#1259](https://github.com/PrefectHQ/fastmcp/pull/1259)
+* Small clean-up from MCP Tool Transform PR by [@strawgate](https://github.com/strawgate) in [#1267](https://github.com/PrefectHQ/fastmcp/pull/1267)
+* Add test for proxy tags visibility by [@jlowin](https://github.com/jlowin) in [#1302](https://github.com/PrefectHQ/fastmcp/pull/1302)
+* Add unit test for sampling with image messages by [@jlowin](https://github.com/jlowin) in [#1329](https://github.com/PrefectHQ/fastmcp/pull/1329)
+* Remove redundant resource_metadata_url assignment by [@jlowin](https://github.com/jlowin) in [#1328](https://github.com/PrefectHQ/fastmcp/pull/1328)
+* Update bug.yml by [@jlowin](https://github.com/jlowin) in [#1331](https://github.com/PrefectHQ/fastmcp/pull/1331)
+* Ensure validation errors are raised when masked by [@jlowin](https://github.com/jlowin) in [#1330](https://github.com/PrefectHQ/fastmcp/pull/1330)
+
+## New Contributors
+* [@mariotaddeucci](https://github.com/mariotaddeucci) made their first contribution in [#1194](https://github.com/PrefectHQ/fastmcp/pull/1194)
+* [@algirdasci](https://github.com/algirdasci) made their first contribution in [#1208](https://github.com/PrefectHQ/fastmcp/pull/1208)
+* [@chughtapan](https://github.com/chughtapan) made their first contribution in [#1222](https://github.com/PrefectHQ/fastmcp/pull/1222)
+* [@mukulmurthy](https://github.com/mukulmurthy) made their first contribution in [#1160](https://github.com/PrefectHQ/fastmcp/pull/1160)
+* [@orweis](https://github.com/orweis) made their first contribution in [#1226](https://github.com/PrefectHQ/fastmcp/pull/1226)
+* [@Sourav-Tripathy](https://github.com/Sourav-Tripathy) made their first contribution in [#1229](https://github.com/PrefectHQ/fastmcp/pull/1229)
+* [@adam-conway](https://github.com/adam-conway) made their first contribution in [#1239](https://github.com/PrefectHQ/fastmcp/pull/1239)
+* [@muhammadkhalid-03](https://github.com/muhammadkhalid-03) made their first contribution in [#1257](https://github.com/PrefectHQ/fastmcp/pull/1257)
+* [@jbn](https://github.com/jbn) made their first contribution in [#1278](https://github.com/PrefectHQ/fastmcp/pull/1278)
+* [@dacamposol](https://github.com/dacamposol) made their first contribution in [#1287](https://github.com/PrefectHQ/fastmcp/pull/1287)
+* [@chi2liu](https://github.com/chi2liu) made their first contribution in [#1322](https://github.com/PrefectHQ/fastmcp/pull/1322)
+* [@cjermain](https://github.com/cjermain) made their first contribution in [#1326](https://github.com/PrefectHQ/fastmcp/pull/1326)
+
+**Full Changelog**: [v2.10.6...v2.11.0](https://github.com/PrefectHQ/fastmcp/compare/v2.10.6...v2.11.0)
+
+
+
+
+
+## [v2.10.6: Hymn for the Weekend](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.6)
+
+A special Saturday release with many fixes.
+
+## What's Changed
+### Enhancements 🔧
+* Resolve #1139 -- Implement include_context argument in Context.sample by [@codingjoe](https://github.com/codingjoe) in [#1141](https://github.com/PrefectHQ/fastmcp/pull/1141)
+* feat(settings): add log level normalization by [@ka2048](https://github.com/ka2048) in [#1171](https://github.com/PrefectHQ/fastmcp/pull/1171)
+* add server name to mounted server warnings by [@artificial-aidan](https://github.com/artificial-aidan) in [#1147](https://github.com/PrefectHQ/fastmcp/pull/1147)
+* Add StatefulProxyClient by [@hopeful0](https://github.com/hopeful0) in [#1109](https://github.com/PrefectHQ/fastmcp/pull/1109)
+### Fixes 🐞
+* Fix OpenAPI empty parameters by [@FabrizioSandri](https://github.com/FabrizioSandri) in [#1128](https://github.com/PrefectHQ/fastmcp/pull/1128)
+* Fix title field preservation in tool transformations by [@jlowin](https://github.com/jlowin) in [#1131](https://github.com/PrefectHQ/fastmcp/pull/1131)
+* Fix optional parameter validation in OpenAPI integration by [@jlowin](https://github.com/jlowin) in [#1135](https://github.com/PrefectHQ/fastmcp/pull/1135)
+* Do not silently exclude the "context" key from JSON body by [@melkamar](https://github.com/melkamar) in [#1153](https://github.com/PrefectHQ/fastmcp/pull/1153)
+* Fix tool output schema generation to respect Pydantic serialization aliases by [@zzstoatzz](https://github.com/zzstoatzz) in [#1148](https://github.com/PrefectHQ/fastmcp/pull/1148)
+* fix: _replace_ref_with_defs; ensure ref_path is string by [@itaru2622](https://github.com/itaru2622) in [#1164](https://github.com/PrefectHQ/fastmcp/pull/1164)
+* Fix nesting when making OpenAPI arrays and objects optional by [@melkamar](https://github.com/melkamar) in [#1178](https://github.com/PrefectHQ/fastmcp/pull/1178)
+* Fix `mcp-json` output format to include server name by [@jlowin](https://github.com/jlowin) in [#1185](https://github.com/PrefectHQ/fastmcp/pull/1185)
+* Only configure logging one time by [@jlowin](https://github.com/jlowin) in [#1187](https://github.com/PrefectHQ/fastmcp/pull/1187)
+### Docs 📚
+* Update changelog.mdx by [@jlowin](https://github.com/jlowin) in [#1127](https://github.com/PrefectHQ/fastmcp/pull/1127)
+* Eunomia Authorization with native FastMCP's Middleware by [@tommitt](https://github.com/tommitt) in [#1144](https://github.com/PrefectHQ/fastmcp/pull/1144)
+* update api ref for new `mdxify` version by [@zzstoatzz](https://github.com/zzstoatzz) in [#1182](https://github.com/PrefectHQ/fastmcp/pull/1182)
+### Other Changes 🦾
+* Expand empty parameter filtering and add comprehensive tests by [@jlowin](https://github.com/jlowin) in [#1129](https://github.com/PrefectHQ/fastmcp/pull/1129)
+* Add no-commit-to-branch hook by [@zzstoatzz](https://github.com/zzstoatzz) in [#1149](https://github.com/PrefectHQ/fastmcp/pull/1149)
+* Update README.md by [@jlowin](https://github.com/jlowin) in [#1165](https://github.com/PrefectHQ/fastmcp/pull/1165)
+* skip on rate limit by [@zzstoatzz](https://github.com/zzstoatzz) in [#1183](https://github.com/PrefectHQ/fastmcp/pull/1183)
+* Remove deprecated proxy creation by [@jlowin](https://github.com/jlowin) in [#1186](https://github.com/PrefectHQ/fastmcp/pull/1186)
+* Separate integration tests from unit tests in CI by [@jlowin](https://github.com/jlowin) in [#1188](https://github.com/PrefectHQ/fastmcp/pull/1188)
+
+## New Contributors
+* [@FabrizioSandri](https://github.com/FabrizioSandri) made their first contribution in [#1128](https://github.com/PrefectHQ/fastmcp/pull/1128)
+* [@melkamar](https://github.com/melkamar) made their first contribution in [#1153](https://github.com/PrefectHQ/fastmcp/pull/1153)
+* [@codingjoe](https://github.com/codingjoe) made their first contribution in [#1141](https://github.com/PrefectHQ/fastmcp/pull/1141)
+* [@itaru2622](https://github.com/itaru2622) made their first contribution in [#1164](https://github.com/PrefectHQ/fastmcp/pull/1164)
+* [@ka2048](https://github.com/ka2048) made their first contribution in [#1171](https://github.com/PrefectHQ/fastmcp/pull/1171)
+* [@artificial-aidan](https://github.com/artificial-aidan) made their first contribution in [#1147](https://github.com/PrefectHQ/fastmcp/pull/1147)
+
+**Full Changelog**: [v2.10.5...v2.10.6](https://github.com/PrefectHQ/fastmcp/compare/v2.10.5...v2.10.6)
+
+
+
+
+
+## [v2.10.5: Middle Management](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.5)
+
+A maintenance release focused on OpenAPI refinements and middleware fixes, plus console improvements.
+
+## What's Changed
+### Enhancements 🔧
+* Fix Claude Code CLI detection for npm global installations by [@jlowin](https://github.com/jlowin) in [#1106](https://github.com/PrefectHQ/fastmcp/pull/1106)
+* Fix OpenAPI parameter name collisions with location suffixing by [@jlowin](https://github.com/jlowin) in [#1107](https://github.com/PrefectHQ/fastmcp/pull/1107)
+* Add mirrored component support for proxy servers by [@jlowin](https://github.com/jlowin) in [#1105](https://github.com/PrefectHQ/fastmcp/pull/1105)
+### Fixes 🐞
+* Fix OpenAPI deepObject style parameter encoding by [@jlowin](https://github.com/jlowin) in [#1122](https://github.com/PrefectHQ/fastmcp/pull/1122)
+* xfail when github token is not set ('' or None) by [@jlowin](https://github.com/jlowin) in [#1123](https://github.com/PrefectHQ/fastmcp/pull/1123)
+* fix: replace oneOf with anyOf in OpenAPI output schemas by [@MagnusS0](https://github.com/MagnusS0) in [#1119](https://github.com/PrefectHQ/fastmcp/pull/1119)
+* Fix middleware list result types by [@jlowin](https://github.com/jlowin) in [#1125](https://github.com/PrefectHQ/fastmcp/pull/1125)
+* Improve console width for logo by [@jlowin](https://github.com/jlowin) in [#1126](https://github.com/PrefectHQ/fastmcp/pull/1126)
+### Docs 📚
+* Improve transport + integration docs by [@jlowin](https://github.com/jlowin) in [#1103](https://github.com/PrefectHQ/fastmcp/pull/1103)
+* Update proxy.mdx by [@coldfire-x](https://github.com/coldfire-x) in [#1108](https://github.com/PrefectHQ/fastmcp/pull/1108)
+### Other Changes 🦾
+* Update github remote server tests with secret by [@jlowin](https://github.com/jlowin) in [#1112](https://github.com/PrefectHQ/fastmcp/pull/1112)
+
+## New Contributors
+* [@coldfire-x](https://github.com/coldfire-x) made their first contribution in [#1108](https://github.com/PrefectHQ/fastmcp/pull/1108)
+* [@MagnusS0](https://github.com/MagnusS0) made their first contribution in [#1119](https://github.com/PrefectHQ/fastmcp/pull/1119)
+
+**Full Changelog**: [v2.10.4...v2.10.5](https://github.com/PrefectHQ/fastmcp/compare/v2.10.4...v2.10.5)
+
+
+
+
+
+## [v2.10.4: Transport-ation](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.4)
+
+A quick fix to ensure the CLI accepts "streamable-http" as a valid transport option.
+
+## What's Changed
+### Fixes 🐞
+* Ensure the CLI accepts "streamable-http" as a valid transport by [@jlowin](https://github.com/jlowin) in [#1099](https://github.com/PrefectHQ/fastmcp/pull/1099)
+
+**Full Changelog**: [v2.10.3...v2.10.4](https://github.com/PrefectHQ/fastmcp/compare/v2.10.3...v2.10.4)
+
+
+
+
+
+## [v2.10.3: CLI Me a River](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.3)
+
+A major CLI overhaul featuring a complete refactor from typer to cyclopts, new IDE integrations, and comprehensive OpenAPI improvements.
+
+## What's Changed
+### New Features 🎉
+* Refactor CLI from typer to cyclopts and add comprehensive tests by [@jlowin](https://github.com/jlowin) in [#1062](https://github.com/PrefectHQ/fastmcp/pull/1062)
+* Add output schema support for OpenAPI tools by [@jlowin](https://github.com/jlowin) in [#1073](https://github.com/PrefectHQ/fastmcp/pull/1073)
+### Enhancements 🔧
+* Add Cursor support via CLI integration by [@jlowin](https://github.com/jlowin) in [#1052](https://github.com/PrefectHQ/fastmcp/pull/1052)
+* Add Claude Code install integration by [@jlowin](https://github.com/jlowin) in [#1053](https://github.com/PrefectHQ/fastmcp/pull/1053)
+* Generate MCP JSON config output from CLI as new `fastmcp install` command by [@jlowin](https://github.com/jlowin) in [#1056](https://github.com/PrefectHQ/fastmcp/pull/1056)
+* Use isawaitable instead of iscoroutine by [@jlowin](https://github.com/jlowin) in [#1059](https://github.com/PrefectHQ/fastmcp/pull/1059)
+* feat: Add `--path` Option to CLI for HTTP/SSE Route by [@davidbk-legit](https://github.com/davidbk-legit) in [#1087](https://github.com/PrefectHQ/fastmcp/pull/1087)
+* Fix concurrent proxy client operations with session isolation by [@jlowin](https://github.com/jlowin) in [#1083](https://github.com/PrefectHQ/fastmcp/pull/1083)
+### Fixes 🐞
+* Refactor Client context management to avoid concurrency issue by [@hopeful0](https://github.com/hopeful0) in [#1054](https://github.com/PrefectHQ/fastmcp/pull/1054)
+* Keep json schema $defs on transform by [@strawgate](https://github.com/strawgate) in [#1066](https://github.com/PrefectHQ/fastmcp/pull/1066)
+* Ensure fastmcp version copy is plaintext by [@jlowin](https://github.com/jlowin) in [#1071](https://github.com/PrefectHQ/fastmcp/pull/1071)
+* Fix single-element list unwrapping in tool content by [@jlowin](https://github.com/jlowin) in [#1074](https://github.com/PrefectHQ/fastmcp/pull/1074)
+* Fix max recursion error when pruning OpenAPI definitions by [@dimitribarbot](https://github.com/dimitribarbot) in [#1092](https://github.com/PrefectHQ/fastmcp/pull/1092)
+* Fix OpenAPI tool name registration when modified by mcp_component_fn by [@jlowin](https://github.com/jlowin) in [#1096](https://github.com/PrefectHQ/fastmcp/pull/1096)
+### Docs 📚
+* Docs: add example of more concise way to use bearer auth by [@neilconway](https://github.com/neilconway) in [#1055](https://github.com/PrefectHQ/fastmcp/pull/1055)
+* Update favicon by [@jlowin](https://github.com/jlowin) in [#1058](https://github.com/PrefectHQ/fastmcp/pull/1058)
+* Update environment note by [@jlowin](https://github.com/jlowin) in [#1075](https://github.com/PrefectHQ/fastmcp/pull/1075)
+* Add fastmcp version --copy documentation by [@jlowin](https://github.com/jlowin) in [#1076](https://github.com/PrefectHQ/fastmcp/pull/1076)
+### Other Changes 🦾
+* Remove asserts and add documentation following #1054 by [@jlowin](https://github.com/jlowin) in [#1057](https://github.com/PrefectHQ/fastmcp/pull/1057)
+* Add --copy flag for fastmcp version by [@jlowin](https://github.com/jlowin) in [#1063](https://github.com/PrefectHQ/fastmcp/pull/1063)
+* Fix docstring format for fastmcp.client.Client by [@neilconway](https://github.com/neilconway) in [#1094](https://github.com/PrefectHQ/fastmcp/pull/1094)
+
+## New Contributors
+* [@neilconway](https://github.com/neilconway) made their first contribution in [#1055](https://github.com/PrefectHQ/fastmcp/pull/1055)
+* [@davidbk-legit](https://github.com/davidbk-legit) made their first contribution in [#1087](https://github.com/PrefectHQ/fastmcp/pull/1087)
+* [@dimitribarbot](https://github.com/dimitribarbot) made their first contribution in [#1092](https://github.com/PrefectHQ/fastmcp/pull/1092)
+
+**Full Changelog**: [v2.10.2...v2.10.3](https://github.com/PrefectHQ/fastmcp/compare/v2.10.2...v2.10.3)
+
+
+
+
+
+## [v2.10.2: Forward March](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.2)
+
+The headline feature of this release is the ability to "forward" advanced MCP interactions like logging, progress, and elicitation through proxy servers. If the remote server requests an elicitation, the proxy client will pass that request to the new, "ultimate" client.
+
+## What's Changed
+### New Features 🎉
+* Proxy support advanced MCP features by [@hopeful0](https://github.com/hopeful0) in [#1022](https://github.com/PrefectHQ/fastmcp/pull/1022)
+### Enhancements 🔧
+* Re-add splash screen by [@jlowin](https://github.com/jlowin) in [#1027](https://github.com/PrefectHQ/fastmcp/pull/1027)
+* Reduce banner padding by [@jlowin](https://github.com/jlowin) in [#1030](https://github.com/PrefectHQ/fastmcp/pull/1030)
+* Allow per-server timeouts in MCPConfig by [@cegersdoerfer](https://github.com/cegersdoerfer) in [#1031](https://github.com/PrefectHQ/fastmcp/pull/1031)
+* Support 'scp' claim for OAuth scopes in BearerAuthProvider by [@jlowin](https://github.com/jlowin) in [#1033](https://github.com/PrefectHQ/fastmcp/pull/1033)
+* Add path expansion to image/audio/file by [@jlowin](https://github.com/jlowin) in [#1038](https://github.com/PrefectHQ/fastmcp/pull/1038)
+* Ensure multi-client configurations use new ProxyClient by [@jlowin](https://github.com/jlowin) in [#1045](https://github.com/PrefectHQ/fastmcp/pull/1045)
+### Fixes 🐞
+* Expose stateless_http kwarg for mcp.run() by [@jlowin](https://github.com/jlowin) in [#1018](https://github.com/PrefectHQ/fastmcp/pull/1018)
+* Avoid propagating logs by [@jlowin](https://github.com/jlowin) in [#1042](https://github.com/PrefectHQ/fastmcp/pull/1042)
+### Docs 📚
+* Clean up docs by [@jlowin](https://github.com/jlowin) in [#1028](https://github.com/PrefectHQ/fastmcp/pull/1028)
+* Docs: clarify server URL paths for ChatGPT integration by [@thap2331](https://github.com/thap2331) in [#1017](https://github.com/PrefectHQ/fastmcp/pull/1017)
+### Other Changes 🦾
+* Split giant openapi test file into smaller files by [@jlowin](https://github.com/jlowin) in [#1034](https://github.com/PrefectHQ/fastmcp/pull/1034)
+* Add comprehensive OpenAPI 3.0 vs 3.1 compatibility tests by [@jlowin](https://github.com/jlowin) in [#1035](https://github.com/PrefectHQ/fastmcp/pull/1035)
+* Update banner and use console.log by [@jlowin](https://github.com/jlowin) in [#1041](https://github.com/PrefectHQ/fastmcp/pull/1041)
+
+## New Contributors
+* [@cegersdoerfer](https://github.com/cegersdoerfer) made their first contribution in [#1031](https://github.com/PrefectHQ/fastmcp/pull/1031)
+* [@hopeful0](https://github.com/hopeful0) made their first contribution in [#1022](https://github.com/PrefectHQ/fastmcp/pull/1022)
+* [@thap2331](https://github.com/thap2331) made their first contribution in [#1017](https://github.com/PrefectHQ/fastmcp/pull/1017)
+
+**Full Changelog**: [v2.10.1...v2.10.2](https://github.com/PrefectHQ/fastmcp/compare/v2.10.1...v2.10.2)
+
+
+
+
+
+## [v2.10.1: Revert to Sender](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.1)
+
+A quick patch to revert the CLI banner that was added in v2.10.0.
+
+## What's Changed
+### Docs 📚
+* Update changelog.mdx by [@jlowin](https://github.com/jlowin) in [#1009](https://github.com/PrefectHQ/fastmcp/pull/1009)
+* Revert "Add CLI banner" by [@jlowin](https://github.com/jlowin) in [#1011](https://github.com/PrefectHQ/fastmcp/pull/1011)
+
+**Full Changelog**: [v2.10.0...v2.10.1](https://github.com/PrefectHQ/fastmcp/compare/v2.10.0...v2.10.1)
+
+
+
+
+
+## [v2.10.0: Great Spec-tations](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.10.0)
+
+FastMCP 2.10 brings full compliance with the 6/18/2025 MCP spec update, introducing elicitation support for dynamic server-client communication and output schemas for structured tool responses. Please note that due to these changes, this release also includes a breaking change to the return signature of `client.call_tool()`.
+
+### Elicitation Support
+Elicitation allows MCP servers to request additional information from clients during tool execution, enabling more interactive and dynamic server behavior. This opens up new possibilities for tools that need user input or confirmation during execution.
+
+### Output Schemas
+Tools can now define structured output schemas, ensuring that responses conform to expected formats and making tool integration more predictable and type-safe.
+
+## What's Changed
+### New Features 🎉
+* MCP 6/18/25: Add output schema to tools by [@jlowin](https://github.com/jlowin) in [#901](https://github.com/PrefectHQ/fastmcp/pull/901)
+* MCP 6/18/25: Elicitation support by [@jlowin](https://github.com/jlowin) in [#889](https://github.com/PrefectHQ/fastmcp/pull/889)
+### Enhancements 🔧
+* Update types + tests for SDK changes by [@jlowin](https://github.com/jlowin) in [#888](https://github.com/PrefectHQ/fastmcp/pull/888)
+* MCP 6/18/25: Update auth primitives by [@jlowin](https://github.com/jlowin) in [#966](https://github.com/PrefectHQ/fastmcp/pull/966)
+* Add OpenAPI extensions support to HTTPRoute by [@maddymanu](https://github.com/maddymanu) in [#977](https://github.com/PrefectHQ/fastmcp/pull/977)
+* Add title field support to FastMCP components by [@jlowin](https://github.com/jlowin) in [#982](https://github.com/PrefectHQ/fastmcp/pull/982)
+* Support implicit Elicitation acceptance by [@jlowin](https://github.com/jlowin) in [#983](https://github.com/PrefectHQ/fastmcp/pull/983)
+* Support 'no response' elicitation requests by [@jlowin](https://github.com/jlowin) in [#992](https://github.com/PrefectHQ/fastmcp/pull/992)
+* Add Support for Configurable Algorithms by [@sstene1](https://github.com/sstene1) in [#997](https://github.com/PrefectHQ/fastmcp/pull/997)
+### Fixes 🐞
+* Improve stdio error handling to raise connection failures immediately by [@jlowin](https://github.com/jlowin) in [#984](https://github.com/PrefectHQ/fastmcp/pull/984)
+* Fix type hints for FunctionResource:fn by [@CfirTsabari](https://github.com/CfirTsabari) in [#986](https://github.com/PrefectHQ/fastmcp/pull/986)
+* Update link to OpenAI MCP example by [@mossbanay](https://github.com/mossbanay) in [#985](https://github.com/PrefectHQ/fastmcp/pull/985)
+* Fix output schema generation edge case by [@jlowin](https://github.com/jlowin) in [#995](https://github.com/PrefectHQ/fastmcp/pull/995)
+* Refactor array parameter formatting to reduce code duplication by [@jlowin](https://github.com/jlowin) in [#1007](https://github.com/PrefectHQ/fastmcp/pull/1007)
+* Fix OpenAPI array parameter explode handling by [@jlowin](https://github.com/jlowin) in [#1008](https://github.com/PrefectHQ/fastmcp/pull/1008)
+### Breaking Changes 🛫
+* MCP 6/18/25: Upgrade to mcp 1.10 by [@jlowin](https://github.com/jlowin) in [#887](https://github.com/PrefectHQ/fastmcp/pull/887)
+### Docs 📚
+* Update middleware imports and documentation by [@jlowin](https://github.com/jlowin) in [#999](https://github.com/PrefectHQ/fastmcp/pull/999)
+* Update OpenAI docs by [@jlowin](https://github.com/jlowin) in [#1001](https://github.com/PrefectHQ/fastmcp/pull/1001)
+* Add CLI banner by [@jlowin](https://github.com/jlowin) in [#1005](https://github.com/PrefectHQ/fastmcp/pull/1005)
+### Examples & Contrib 💡
+* Component Manager by [@gorocode](https://github.com/gorocode) in [#976](https://github.com/PrefectHQ/fastmcp/pull/976)
+### Other Changes 🦾
+* Minor auth improvements by [@jlowin](https://github.com/jlowin) in [#967](https://github.com/PrefectHQ/fastmcp/pull/967)
+* Add .ccignore for copychat by [@jlowin](https://github.com/jlowin) in [#1000](https://github.com/PrefectHQ/fastmcp/pull/1000)
+
+## New Contributors
+* [@maddymanu](https://github.com/maddymanu) made their first contribution in [#977](https://github.com/PrefectHQ/fastmcp/pull/977)
+* [@github0hello](https://github.com/github0hello) made their first contribution in [#979](https://github.com/PrefectHQ/fastmcp/pull/979)
+* [@tommitt](https://github.com/tommitt) made their first contribution in [#975](https://github.com/PrefectHQ/fastmcp/pull/975)
+* [@CfirTsabari](https://github.com/CfirTsabari) made their first contribution in [#986](https://github.com/PrefectHQ/fastmcp/pull/986)
+* [@mossbanay](https://github.com/mossbanay) made their first contribution in [#985](https://github.com/PrefectHQ/fastmcp/pull/985)
+* [@sstene1](https://github.com/sstene1) made their first contribution in [#997](https://github.com/PrefectHQ/fastmcp/pull/997)
+
+**Full Changelog**: [v2.9.2...v2.10.0](https://github.com/PrefectHQ/fastmcp/compare/v2.9.2...v2.10.0)
+
+
+
+
+
+## [v2.9.2: Safety Pin](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.9.2)
+
+This is a patch release to pin `mcp` below 1.10, which includes changes related to the 6/18/2025 MCP spec update and could potentially break functionality for some FastMCP users.
+
+## What's Changed
+### Docs 📚
+* Fix version badge for messages by [@jlowin](https://github.com/jlowin) in [#960](https://github.com/PrefectHQ/fastmcp/pull/960)
+### Dependencies 📦
+* Pin mcp dependency by [@jlowin](https://github.com/jlowin) in [#962](https://github.com/PrefectHQ/fastmcp/pull/962)
+
+**Full Changelog**: [v2.9.1...v2.9.2](https://github.com/PrefectHQ/fastmcp/compare/v2.9.1...v2.9.2)
+
+
+
+
+
+## [v2.9.1: Call Me Maybe](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.9.1)
+
+FastMCP 2.9.1 introduces automatic MCP list change notifications, allowing servers to notify clients when tools, resources, or prompts are dynamically updated. This enables more responsive and adaptive MCP integrations.
+
+## What's Changed
+### New Features 🎉
+* Add automatic MCP list change notifications and client message handling by [@jlowin](https://github.com/jlowin) in [#939](https://github.com/PrefectHQ/fastmcp/pull/939)
+### Enhancements 🔧
+* Add debug logging to bearer token authentication by [@jlowin](https://github.com/jlowin) in [#952](https://github.com/PrefectHQ/fastmcp/pull/952)
+### Fixes 🐞
+* Fix duplicate error logging in exception handlers by [@jlowin](https://github.com/jlowin) in [#938](https://github.com/PrefectHQ/fastmcp/pull/938)
+* Fix parameter location enum handling in OpenAPI parser by [@jlowin](https://github.com/jlowin) in [#953](https://github.com/PrefectHQ/fastmcp/pull/953)
+* Fix external schema reference handling in OpenAPI parser by [@jlowin](https://github.com/jlowin) in [#954](https://github.com/PrefectHQ/fastmcp/pull/954)
+### Docs 📚
+* Update changelog for 2.9 release by [@jlowin](https://github.com/jlowin) in [#929](https://github.com/PrefectHQ/fastmcp/pull/929)
+* Regenerate API references by [@zzstoatzz](https://github.com/zzstoatzz) in [#935](https://github.com/PrefectHQ/fastmcp/pull/935)
+* Regenerate API references by [@zzstoatzz](https://github.com/zzstoatzz) in [#947](https://github.com/PrefectHQ/fastmcp/pull/947)
+* Regenerate API references by [@zzstoatzz](https://github.com/zzstoatzz) in [#949](https://github.com/PrefectHQ/fastmcp/pull/949)
+### Examples & Contrib 💡
+* Add `create_thread` tool to bsky MCP server by [@zzstoatzz](https://github.com/zzstoatzz) in [#927](https://github.com/PrefectHQ/fastmcp/pull/927)
+* Update `mount_example.py` to work with current fastmcp API by [@rajephon](https://github.com/rajephon) in [#957](https://github.com/PrefectHQ/fastmcp/pull/957)
+
+## New Contributors
+* [@rajephon](https://github.com/rajephon) made their first contribution in [#957](https://github.com/PrefectHQ/fastmcp/pull/957)
+
+**Full Changelog**: [v2.9.0...v2.9.1](https://github.com/PrefectHQ/fastmcp/compare/v2.9.0...v2.9.1)
+
+
+
+
+
+## [v2.9.0: Stuck in the Middleware With You](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.9.0)
+
+FastMCP 2.9 introduces two important features that push beyond the basic MCP protocol: MCP Middleware and server-side type conversion.
+
+### MCP Middleware
+MCP middleware lets you intercept and modify requests and responses at the protocol level, giving you powerful capabilities for logging, authentication, validation, and more. This is particularly useful for building production-ready MCP servers that need sophisticated request handling.
+
+### Server-side Type Conversion
+This release also introduces server-side type conversion for prompt arguments, ensuring that data is properly formatted before being passed to your functions. This reduces the burden on individual tools and prompts to handle type validation and conversion.
+
+## What's Changed
+### New Features 🎉
+* Add File utility for binary data by [@gorocode](https://github.com/gorocode) in [#843](https://github.com/PrefectHQ/fastmcp/pull/843)
+* Consolidate prefix logic into FastMCP methods by [@jlowin](https://github.com/jlowin) in [#861](https://github.com/PrefectHQ/fastmcp/pull/861)
+* Add MCP Middleware by [@jlowin](https://github.com/jlowin) in [#870](https://github.com/PrefectHQ/fastmcp/pull/870)
+* Implement server-side type conversion for prompt arguments by [@jlowin](https://github.com/jlowin) in [#908](https://github.com/PrefectHQ/fastmcp/pull/908)
+### Enhancements 🔧
+* Fix tool description indentation issue by [@zfflxx](https://github.com/zfflxx) in [#845](https://github.com/PrefectHQ/fastmcp/pull/845)
+* Add version parameter to FastMCP constructor by [@mkyutani](https://github.com/mkyutani) in [#842](https://github.com/PrefectHQ/fastmcp/pull/842)
+* Update version to not be positional by [@jlowin](https://github.com/jlowin) in [#848](https://github.com/PrefectHQ/fastmcp/pull/848)
+* Add key to component by [@jlowin](https://github.com/jlowin) in [#869](https://github.com/PrefectHQ/fastmcp/pull/869)
+* Add session_id property to Context for data sharing by [@jlowin](https://github.com/jlowin) in [#881](https://github.com/PrefectHQ/fastmcp/pull/881)
+* Fix CORS documentation example by [@jlowin](https://github.com/jlowin) in [#895](https://github.com/PrefectHQ/fastmcp/pull/895)
+### Fixes 🐞
+* "report_progress missing passing related_request_id causes notifications not working" by [@alexsee](https://github.com/alexsee) in [#838](https://github.com/PrefectHQ/fastmcp/pull/838)
+* Fix JWT issuer validation to support string values per RFC 7519 by [@jlowin](https://github.com/jlowin) in [#892](https://github.com/PrefectHQ/fastmcp/pull/892)
+* Fix BearerAuthProvider audience type annotations by [@jlowin](https://github.com/jlowin) in [#894](https://github.com/PrefectHQ/fastmcp/pull/894)
+### Docs 📚
+* Add CLAUDE.md development guidelines by [@jlowin](https://github.com/jlowin) in [#880](https://github.com/PrefectHQ/fastmcp/pull/880)
+* Update context docs for session_id property by [@jlowin](https://github.com/jlowin) in [#882](https://github.com/PrefectHQ/fastmcp/pull/882)
+* Add API reference by [@zzstoatzz](https://github.com/zzstoatzz) in [#893](https://github.com/PrefectHQ/fastmcp/pull/893)
+* Fix API ref rendering by [@zzstoatzz](https://github.com/zzstoatzz) in [#900](https://github.com/PrefectHQ/fastmcp/pull/900)
+* Simplify docs nav by [@jlowin](https://github.com/jlowin) in [#902](https://github.com/PrefectHQ/fastmcp/pull/902)
+* Add fastmcp inspect command by [@jlowin](https://github.com/jlowin) in [#904](https://github.com/PrefectHQ/fastmcp/pull/904)
+* Update client docs by [@jlowin](https://github.com/jlowin) in [#912](https://github.com/PrefectHQ/fastmcp/pull/912)
+* Update docs nav by [@jlowin](https://github.com/jlowin) in [#913](https://github.com/PrefectHQ/fastmcp/pull/913)
+* Update integration documentation for Claude Desktop, ChatGPT, and Claude Code by [@jlowin](https://github.com/jlowin) in [#915](https://github.com/PrefectHQ/fastmcp/pull/915)
+* Add http as an alias for streamable http by [@jlowin](https://github.com/jlowin) in [#917](https://github.com/PrefectHQ/fastmcp/pull/917)
+* Clean up parameter documentation by [@jlowin](https://github.com/jlowin) in [#918](https://github.com/PrefectHQ/fastmcp/pull/918)
+* Add middleware examples for timing, logging, rate limiting, and error handling by [@jlowin](https://github.com/jlowin) in [#919](https://github.com/PrefectHQ/fastmcp/pull/919)
+* ControlFlow → FastMCP rename by [@jlowin](https://github.com/jlowin) in [#922](https://github.com/PrefectHQ/fastmcp/pull/922)
+### Examples & Contrib 💡
+* Add contrib.mcp_mixin support for annotations by [@rsp2k](https://github.com/rsp2k) in [#860](https://github.com/PrefectHQ/fastmcp/pull/860)
+* Add ATProto (Bluesky) MCP Server Example by [@zzstoatzz](https://github.com/zzstoatzz) in [#916](https://github.com/PrefectHQ/fastmcp/pull/916)
+* Fix path in atproto example pyproject by [@zzstoatzz](https://github.com/zzstoatzz) in [#920](https://github.com/PrefectHQ/fastmcp/pull/920)
+* Remove uv source in example by [@zzstoatzz](https://github.com/zzstoatzz) in [#921](https://github.com/PrefectHQ/fastmcp/pull/921)
+
+## New Contributors
+* [@alexsee](https://github.com/alexsee) made their first contribution in [#838](https://github.com/PrefectHQ/fastmcp/pull/838)
+* [@zfflxx](https://github.com/zfflxx) made their first contribution in [#845](https://github.com/PrefectHQ/fastmcp/pull/845)
+* [@mkyutani](https://github.com/mkyutani) made their first contribution in [#842](https://github.com/PrefectHQ/fastmcp/pull/842)
+* [@gorocode](https://github.com/gorocode) made their first contribution in [#843](https://github.com/PrefectHQ/fastmcp/pull/843)
+* [@rsp2k](https://github.com/rsp2k) made their first contribution in [#860](https://github.com/PrefectHQ/fastmcp/pull/860)
+* [@owtaylor](https://github.com/owtaylor) made their first contribution in [#897](https://github.com/PrefectHQ/fastmcp/pull/897)
+* [@Jason-CKY](https://github.com/Jason-CKY) made their first contribution in [#906](https://github.com/PrefectHQ/fastmcp/pull/906)
+
+**Full Changelog**: [v2.8.1...v2.9.0](https://github.com/PrefectHQ/fastmcp/compare/v2.8.1...v2.9.0)
+
+
+
+
+
+## [v2.8.1: Sound Judgement](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.8.1)
+
+2.8.1 introduces audio support, as well as minor fixes and updates for deprecated features.
+
+### Audio Support
+This release adds support for audio content in MCP tools and resources, expanding FastMCP's multimedia capabilities beyond text and images.
+
+## What's Changed
+### New Features 🎉
+* Add audio support by [@jlowin](https://github.com/jlowin) in [#833](https://github.com/PrefectHQ/fastmcp/pull/833)
+### Enhancements 🔧
+* Add flag for disabling deprecation warnings by [@jlowin](https://github.com/jlowin) in [#802](https://github.com/PrefectHQ/fastmcp/pull/802)
+* Add examples to Tool Arg Param transformation by [@strawgate](https://github.com/strawgate) in [#806](https://github.com/PrefectHQ/fastmcp/pull/806)
+### Fixes 🐞
+* Restore .settings access as deprecated by [@jlowin](https://github.com/jlowin) in [#800](https://github.com/PrefectHQ/fastmcp/pull/800)
+* Ensure handling of false http kwargs correctly; removed unused kwarg by [@jlowin](https://github.com/jlowin) in [#804](https://github.com/PrefectHQ/fastmcp/pull/804)
+* Bump mcp 1.9.4 by [@jlowin](https://github.com/jlowin) in [#835](https://github.com/PrefectHQ/fastmcp/pull/835)
+### Docs 📚
+* Update changelog for 2.8.0 by [@jlowin](https://github.com/jlowin) in [#794](https://github.com/PrefectHQ/fastmcp/pull/794)
+* Update welcome docs by [@jlowin](https://github.com/jlowin) in [#808](https://github.com/PrefectHQ/fastmcp/pull/808)
+* Update headers in docs by [@jlowin](https://github.com/jlowin) in [#809](https://github.com/PrefectHQ/fastmcp/pull/809)
+* Add MCP group to tutorials by [@jlowin](https://github.com/jlowin) in [#810](https://github.com/PrefectHQ/fastmcp/pull/810)
+* Add Community section to documentation by [@zzstoatzz](https://github.com/zzstoatzz) in [#819](https://github.com/PrefectHQ/fastmcp/pull/819)
+* Add 2.8 update by [@jlowin](https://github.com/jlowin) in [#821](https://github.com/PrefectHQ/fastmcp/pull/821)
+* Embed YouTube videos in community showcase by [@zzstoatzz](https://github.com/zzstoatzz) in [#820](https://github.com/PrefectHQ/fastmcp/pull/820)
+### Other Changes 🦾
+* Ensure http args are passed through by [@jlowin](https://github.com/jlowin) in [#803](https://github.com/PrefectHQ/fastmcp/pull/803)
+* Fix install link in readme by [@jlowin](https://github.com/jlowin) in [#836](https://github.com/PrefectHQ/fastmcp/pull/836)
+
+**Full Changelog**: [v2.8.0...v2.8.1](https://github.com/PrefectHQ/fastmcp/compare/v2.8.0...v2.8.1)
+
+
+
+
+
+## [v2.8.0: Transform and Roll Out](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.8.0)
+
+FastMCP 2.8.0 introduces powerful new ways to customize and control your MCP servers!
+
+### Tool Transformation
+
+The highlight of this release is first-class [**Tool Transformation**](/servers/transforms/tool-transformation), a new feature that lets you create enhanced variations of existing tools. You can now easily rename arguments, hide parameters, modify descriptions, and even wrap tools with custom validation or post-processing logic—all without rewriting the original code. This makes it easier than ever to adapt generic tools for specific LLM use cases or to simplify complex APIs. Huge thanks to [@strawgate](https://github.com/strawgate) for partnering on this, starting with [#591](https://github.com/PrefectHQ/fastmcp/discussions/591) and [#599](https://github.com/PrefectHQ/fastmcp/pull/599) and continuing offline.
+
+### Component Control
+This release also gives you more granular control over which components are exposed to clients. With new [**tag-based filtering**](/servers/server#tag-based-filtering), you can selectively enable or disable tools, resources, and prompts based on tags, perfect for managing different environments or user permissions. Complementing this, every component now supports being [programmatically enabled or disabled](/servers/tools#disabling-tools), offering dynamic control over your server's capabilities.
+
+### Tools-by-Default
+Finally, to improve compatibility with a wider range of LLM clients, this release changes the default behavior for OpenAPI integration: all API endpoints are now converted to `Tools` by default. This is a **breaking change** but pragmatically necessitated by the fact that the majority of MCP clients available today are, sadly, only compatible with MCP tools. Therefore, this change significantly simplifies the out-of-the-box experience and ensures your entire API is immediately accessible to any tool-using agent.
+
+## What's Changed
+### New Features 🎉
+* First-class tool transformation by [@jlowin](https://github.com/jlowin) in [#745](https://github.com/PrefectHQ/fastmcp/pull/745)
+* Support enable/disable for all FastMCP components (tools, prompts, resources, templates) by [@jlowin](https://github.com/jlowin) in [#781](https://github.com/PrefectHQ/fastmcp/pull/781)
+* Add support for tag-based component filtering by [@jlowin](https://github.com/jlowin) in [#748](https://github.com/PrefectHQ/fastmcp/pull/748)
+* Allow tag assignments for OpenAPI by [@jlowin](https://github.com/jlowin) in [#791](https://github.com/PrefectHQ/fastmcp/pull/791)
+### Enhancements 🔧
+* Create common base class for components by [@jlowin](https://github.com/jlowin) in [#776](https://github.com/PrefectHQ/fastmcp/pull/776)
+* Move components to own file; add resource by [@jlowin](https://github.com/jlowin) in [#777](https://github.com/PrefectHQ/fastmcp/pull/777)
+* Update FastMCP component with __eq__ and __repr__ by [@jlowin](https://github.com/jlowin) in [#779](https://github.com/PrefectHQ/fastmcp/pull/779)
+* Remove open-ended and server-specific settings by [@jlowin](https://github.com/jlowin) in [#750](https://github.com/PrefectHQ/fastmcp/pull/750)
+### Fixes 🐞
+* Ensure client is only initialized once by [@jlowin](https://github.com/jlowin) in [#758](https://github.com/PrefectHQ/fastmcp/pull/758)
+* Fix field validator for resource by [@jlowin](https://github.com/jlowin) in [#778](https://github.com/PrefectHQ/fastmcp/pull/778)
+* Ensure proxies can overwrite remote tools without falling back to the remote by [@jlowin](https://github.com/jlowin) in [#782](https://github.com/PrefectHQ/fastmcp/pull/782)
+### Breaking Changes 🛫
+* Treat all openapi routes as tools by [@jlowin](https://github.com/jlowin) in [#788](https://github.com/PrefectHQ/fastmcp/pull/788)
+* Fix issue with global OpenAPI tags by [@jlowin](https://github.com/jlowin) in [#792](https://github.com/PrefectHQ/fastmcp/pull/792)
+### Docs 📚
+* Minor docs updates by [@jlowin](https://github.com/jlowin) in [#755](https://github.com/PrefectHQ/fastmcp/pull/755)
+* Add 2.7 update by [@jlowin](https://github.com/jlowin) in [#756](https://github.com/PrefectHQ/fastmcp/pull/756)
+* Reduce 2.7 image size by [@jlowin](https://github.com/jlowin) in [#757](https://github.com/PrefectHQ/fastmcp/pull/757)
+* Update updates.mdx by [@jlowin](https://github.com/jlowin) in [#765](https://github.com/PrefectHQ/fastmcp/pull/765)
+* Hide docs sidebar scrollbar by default by [@jlowin](https://github.com/jlowin) in [#766](https://github.com/PrefectHQ/fastmcp/pull/766)
+* Add "stop vibe testing" to tutorials by [@jlowin](https://github.com/jlowin) in [#767](https://github.com/PrefectHQ/fastmcp/pull/767)
+* Add docs links by [@jlowin](https://github.com/jlowin) in [#768](https://github.com/PrefectHQ/fastmcp/pull/768)
+* Fix: updated variable name under Gemini remote client by [@yrangana](https://github.com/yrangana) in [#769](https://github.com/PrefectHQ/fastmcp/pull/769)
+* Revert "Hide docs sidebar scrollbar by default" by [@jlowin](https://github.com/jlowin) in [#770](https://github.com/PrefectHQ/fastmcp/pull/770)
+* Add updates by [@jlowin](https://github.com/jlowin) in [#773](https://github.com/PrefectHQ/fastmcp/pull/773)
+* Add tutorials by [@jlowin](https://github.com/jlowin) in [#783](https://github.com/PrefectHQ/fastmcp/pull/783)
+* Update LLM-friendly docs by [@jlowin](https://github.com/jlowin) in [#784](https://github.com/PrefectHQ/fastmcp/pull/784)
+* Update oauth.mdx by [@JeremyCraigMartinez](https://github.com/JeremyCraigMartinez) in [#787](https://github.com/PrefectHQ/fastmcp/pull/787)
+* Add changelog by [@jlowin](https://github.com/jlowin) in [#789](https://github.com/PrefectHQ/fastmcp/pull/789)
+* Add tutorials by [@jlowin](https://github.com/jlowin) in [#790](https://github.com/PrefectHQ/fastmcp/pull/790)
+* Add docs for tag-based filtering by [@jlowin](https://github.com/jlowin) in [#793](https://github.com/PrefectHQ/fastmcp/pull/793)
+### Other Changes 🦾
+* Create dependabot.yml by [@jlowin](https://github.com/jlowin) in [#759](https://github.com/PrefectHQ/fastmcp/pull/759)
+* Bump astral-sh/setup-uv from 3 to 6 by [@dependabot](https://github.com/dependabot) in [#760](https://github.com/PrefectHQ/fastmcp/pull/760)
+* Add dependencies section to release by [@jlowin](https://github.com/jlowin) in [#761](https://github.com/PrefectHQ/fastmcp/pull/761)
+* Remove extra imports for MCPConfig by [@Maanas-Verma](https://github.com/Maanas-Verma) in [#763](https://github.com/PrefectHQ/fastmcp/pull/763)
+* Split out enhancements in release notes by [@jlowin](https://github.com/jlowin) in [#764](https://github.com/PrefectHQ/fastmcp/pull/764)
+
+## New Contributors
+* [@dependabot](https://github.com/dependabot) made their first contribution in [#760](https://github.com/PrefectHQ/fastmcp/pull/760)
+* [@Maanas-Verma](https://github.com/Maanas-Verma) made their first contribution in [#763](https://github.com/PrefectHQ/fastmcp/pull/763)
+* [@JeremyCraigMartinez](https://github.com/JeremyCraigMartinez) made their first contribution in [#787](https://github.com/PrefectHQ/fastmcp/pull/787)
+
+**Full Changelog**: [v2.7.1...v2.8.0](https://github.com/PrefectHQ/fastmcp/compare/v2.7.1...v2.8.0)
+
+
+
+
+
+## [v2.7.1: The Bearer Necessities](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.7.1)
+
+This release primarily contains a fix for parsing string tokens that are provided to FastMCP clients.
+
+### New Features 🎉
+
+* Respect cache setting, set default to 1 second by [@jlowin](https://github.com/jlowin) in [#747](https://github.com/PrefectHQ/fastmcp/pull/747)
+
+### Fixes 🐞
+
+* Ensure event store is properly typed by [@jlowin](https://github.com/jlowin) in [#753](https://github.com/PrefectHQ/fastmcp/pull/753)
+* Fix passing token string to client auth & add auth to MCPConfig clients by [@jlowin](https://github.com/jlowin) in [#754](https://github.com/PrefectHQ/fastmcp/pull/754)
+
+### Docs 📚
+
+* Docs : fix client to mcp\_client in Gemini example by [@yrangana](https://github.com/yrangana) in [#734](https://github.com/PrefectHQ/fastmcp/pull/734)
+* update add tool docstring by [@strawgate](https://github.com/strawgate) in [#739](https://github.com/PrefectHQ/fastmcp/pull/739)
+* Fix contrib link by [@richardkmichael](https://github.com/richardkmichael) in [#749](https://github.com/PrefectHQ/fastmcp/pull/749)
+
+### Other Changes 🦾
+
+* Switch Pydantic defaults to kwargs by [@strawgate](https://github.com/strawgate) in [#731](https://github.com/PrefectHQ/fastmcp/pull/731)
+* Fix Typo in CLI module by [@wfclark5](https://github.com/wfclark5) in [#737](https://github.com/PrefectHQ/fastmcp/pull/737)
+* chore: fix prompt docstring by [@danb27](https://github.com/danb27) in [#752](https://github.com/PrefectHQ/fastmcp/pull/752)
+* Add accept to excluded headers by [@jlowin](https://github.com/jlowin) in [#751](https://github.com/PrefectHQ/fastmcp/pull/751)
+
+### New Contributors
+
+* [@wfclark5](https://github.com/wfclark5) made their first contribution in [#737](https://github.com/PrefectHQ/fastmcp/pull/737)
+* [@richardkmichael](https://github.com/richardkmichael) made their first contribution in [#749](https://github.com/PrefectHQ/fastmcp/pull/749)
+* [@danb27](https://github.com/danb27) made their first contribution in [#752](https://github.com/PrefectHQ/fastmcp/pull/752)
+
+**Full Changelog**: [v2.7.0...v2.7.1](https://github.com/PrefectHQ/fastmcp/compare/v2.7.0...v2.7.1)
+
+
+
+
+## [v2.7.0: Pare Programming](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.7.0)
+
+This is primarily a housekeeping release to remove or deprecate cruft that's accumulated since v1. Primarily, this release refactors FastMCP's internals in preparation for features planned in the next few major releases. However please note that as a result, this release has some minor breaking changes (which is why it's 2.7, not 2.6.2, in accordance with repo guidelines) though not to the core user-facing APIs.
+
+### Breaking Changes 🛫
+
+* decorators return the objects they create, not the decorated function
+* websockets is an optional dependency
+* methods on the server for automatically converting functions into tools/resources/prompts have been deprecated in favor of using the decorators directly
+
+### New Features 🎉
+
+* allow passing flags to servers by [@zzstoatzz](https://github.com/zzstoatzz) in [#690](https://github.com/PrefectHQ/fastmcp/pull/690)
+* replace $ref pointing to `#/components/schemas/` with `#/$defs/` by [@phateffect](https://github.com/phateffect) in [#697](https://github.com/PrefectHQ/fastmcp/pull/697)
+* Split Tool into Tool and FunctionTool by [@jlowin](https://github.com/jlowin) in [#700](https://github.com/PrefectHQ/fastmcp/pull/700)
+* Use strict basemodel for Prompt; relax from\_function deprecation by [@jlowin](https://github.com/jlowin) in [#701](https://github.com/PrefectHQ/fastmcp/pull/701)
+* Formalize resource/functionresource replationship by [@jlowin](https://github.com/jlowin) in [#702](https://github.com/PrefectHQ/fastmcp/pull/702)
+* Formalize template/functiontemplate split by [@jlowin](https://github.com/jlowin) in [#703](https://github.com/PrefectHQ/fastmcp/pull/703)
+* Support flexible @tool decorator call patterns by [@jlowin](https://github.com/jlowin) in [#706](https://github.com/PrefectHQ/fastmcp/pull/706)
+* Ensure deprecation warnings have stacklevel=2 by [@jlowin](https://github.com/jlowin) in [#710](https://github.com/PrefectHQ/fastmcp/pull/710)
+* Allow naked prompt decorator by [@jlowin](https://github.com/jlowin) in [#711](https://github.com/PrefectHQ/fastmcp/pull/711)
+
+### Fixes 🐞
+
+* Updates / Fixes for Tool Content Conversion by [@strawgate](https://github.com/strawgate) in [#642](https://github.com/PrefectHQ/fastmcp/pull/642)
+* Fix pr labeler permissions by [@jlowin](https://github.com/jlowin) in [#708](https://github.com/PrefectHQ/fastmcp/pull/708)
+* remove -n auto by [@jlowin](https://github.com/jlowin) in [#709](https://github.com/PrefectHQ/fastmcp/pull/709)
+* Fix links in README.md by [@alainivars](https://github.com/alainivars) in [#723](https://github.com/PrefectHQ/fastmcp/pull/723)
+
+Happily, this release DOES permit the use of "naked" decorators to align with Pythonic practice:
+
+```python
+@mcp.tool
+def my_tool():
+ ...
+```
+
+**Full Changelog**: [v2.6.2...v2.7.0](https://github.com/PrefectHQ/fastmcp/compare/v2.6.2...v2.7.0)
+
+
+
+
+## [v2.6.1: Blast Auth (second ignition)](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.6.1)
+
+This is a patch release to restore py.typed in #686.
+
+### Docs 📚
+
+* Update readme by [@jlowin](https://github.com/jlowin) in [#679](https://github.com/PrefectHQ/fastmcp/pull/679)
+* Add gemini tutorial by [@jlowin](https://github.com/jlowin) in [#680](https://github.com/PrefectHQ/fastmcp/pull/680)
+* Fix : fix path error to CLI Documentation by [@yrangana](https://github.com/yrangana) in [#684](https://github.com/PrefectHQ/fastmcp/pull/684)
+* Update auth docs by [@jlowin](https://github.com/jlowin) in [#687](https://github.com/PrefectHQ/fastmcp/pull/687)
+
+### Other Changes 🦾
+
+* Remove deprecation notice by [@jlowin](https://github.com/jlowin) in [#677](https://github.com/PrefectHQ/fastmcp/pull/677)
+* Delete server.py by [@jlowin](https://github.com/jlowin) in [#681](https://github.com/PrefectHQ/fastmcp/pull/681)
+* Restore py.typed by [@jlowin](https://github.com/jlowin) in [#686](https://github.com/PrefectHQ/fastmcp/pull/686)
+
+### New Contributors
+
+* [@yrangana](https://github.com/yrangana) made their first contribution in [#684](https://github.com/PrefectHQ/fastmcp/pull/684)
+
+**Full Changelog**: [v2.6.0...v2.6.1](https://github.com/PrefectHQ/fastmcp/compare/v2.6.0...v2.6.1)
+
+
+
+
+## [v2.6.0: Blast Auth](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.6.0)
+
+### New Features 🎉
+
+* Introduce MCP client oauth flow by [@jlowin](https://github.com/jlowin) in [#478](https://github.com/PrefectHQ/fastmcp/pull/478)
+* Support providing tools at init by [@jlowin](https://github.com/jlowin) in [#647](https://github.com/PrefectHQ/fastmcp/pull/647)
+* Simplify code for running servers in processes during tests by [@jlowin](https://github.com/jlowin) in [#649](https://github.com/PrefectHQ/fastmcp/pull/649)
+* Add basic bearer auth for server and client by [@jlowin](https://github.com/jlowin) in [#650](https://github.com/PrefectHQ/fastmcp/pull/650)
+* Support configuring bearer auth from env vars by [@jlowin](https://github.com/jlowin) in [#652](https://github.com/PrefectHQ/fastmcp/pull/652)
+* feat(tool): add support for excluding arguments from tool definition by [@deepak-stratforge](https://github.com/deepak-stratforge) in [#626](https://github.com/PrefectHQ/fastmcp/pull/626)
+* Add docs for server + client auth by [@jlowin](https://github.com/jlowin) in [#655](https://github.com/PrefectHQ/fastmcp/pull/655)
+
+### Fixes 🐞
+
+* fix: Support concurrency in FastMcpProxy (and Client) by [@Sillocan](https://github.com/Sillocan) in [#635](https://github.com/PrefectHQ/fastmcp/pull/635)
+* Ensure Client.close() cleans up client context appropriately by [@jlowin](https://github.com/jlowin) in [#643](https://github.com/PrefectHQ/fastmcp/pull/643)
+* Update client.mdx: ClientError namespace by [@mjkaye](https://github.com/mjkaye) in [#657](https://github.com/PrefectHQ/fastmcp/pull/657)
+
+### Docs 📚
+
+* Make FastMCPTransport support simulated Streamable HTTP Transport (didn't work) by [@jlowin](https://github.com/jlowin) in [#645](https://github.com/PrefectHQ/fastmcp/pull/645)
+* Document exclude\_args by [@jlowin](https://github.com/jlowin) in [#653](https://github.com/PrefectHQ/fastmcp/pull/653)
+* Update welcome by [@jlowin](https://github.com/jlowin) in [#673](https://github.com/PrefectHQ/fastmcp/pull/673)
+* Add Anthropic + Claude desktop integration guides by [@jlowin](https://github.com/jlowin) in [#674](https://github.com/PrefectHQ/fastmcp/pull/674)
+* Minor docs design updates by [@jlowin](https://github.com/jlowin) in [#676](https://github.com/PrefectHQ/fastmcp/pull/676)
+
+### Other Changes 🦾
+
+* Update test typing by [@jlowin](https://github.com/jlowin) in [#646](https://github.com/PrefectHQ/fastmcp/pull/646)
+* Add OpenAI integration docs by [@jlowin](https://github.com/jlowin) in [#660](https://github.com/PrefectHQ/fastmcp/pull/660)
+
+### New Contributors
+
+* [@Sillocan](https://github.com/Sillocan) made their first contribution in [#635](https://github.com/PrefectHQ/fastmcp/pull/635)
+* [@deepak-stratforge](https://github.com/deepak-stratforge) made their first contribution in [#626](https://github.com/PrefectHQ/fastmcp/pull/626)
+* [@mjkaye](https://github.com/mjkaye) made their first contribution in [#657](https://github.com/PrefectHQ/fastmcp/pull/657)
+
+**Full Changelog**: [v2.5.2...v2.6.0](https://github.com/PrefectHQ/fastmcp/compare/v2.5.2...v2.6.0)
+
+
+
+
+## [v2.5.2: Stayin' Alive](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.5.2)
+
+### New Features 🎉
+
+* Add graceful error handling for unreachable mounted servers by [@davenpi](https://github.com/davenpi) in [#605](https://github.com/PrefectHQ/fastmcp/pull/605)
+* Improve type inference from client transport by [@jlowin](https://github.com/jlowin) in [#623](https://github.com/PrefectHQ/fastmcp/pull/623)
+* Add keep\_alive param to reuse subprocess by [@jlowin](https://github.com/jlowin) in [#624](https://github.com/PrefectHQ/fastmcp/pull/624)
+
+### Fixes 🐞
+
+* Fix handling tools without descriptions by [@jlowin](https://github.com/jlowin) in [#610](https://github.com/PrefectHQ/fastmcp/pull/610)
+* Don't print env vars to console when format is wrong by [@jlowin](https://github.com/jlowin) in [#615](https://github.com/PrefectHQ/fastmcp/pull/615)
+* Ensure behavior-affecting headers are excluded when forwarding proxies/openapi by [@jlowin](https://github.com/jlowin) in [#620](https://github.com/PrefectHQ/fastmcp/pull/620)
+
+### Docs 📚
+
+* Add notes about uv and claude desktop by [@jlowin](https://github.com/jlowin) in [#597](https://github.com/PrefectHQ/fastmcp/pull/597)
+
+### Other Changes 🦾
+
+* add init\_timeout for mcp client by [@jfouret](https://github.com/jfouret) in [#607](https://github.com/PrefectHQ/fastmcp/pull/607)
+* Add init\_timeout for mcp client (incl settings) by [@jlowin](https://github.com/jlowin) in [#609](https://github.com/PrefectHQ/fastmcp/pull/609)
+* Support for uppercase letters at the log level by [@ksawaray](https://github.com/ksawaray) in [#625](https://github.com/PrefectHQ/fastmcp/pull/625)
+
+### New Contributors
+
+* [@jfouret](https://github.com/jfouret) made their first contribution in [#607](https://github.com/PrefectHQ/fastmcp/pull/607)
+* [@ksawaray](https://github.com/ksawaray) made their first contribution in [#625](https://github.com/PrefectHQ/fastmcp/pull/625)
+
+**Full Changelog**: [v2.5.1...v2.5.2](https://github.com/PrefectHQ/fastmcp/compare/v2.5.1...v2.5.2)
+
+
+
+
+## [v2.5.1: Route Awakening (Part 2)](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.5.1)
+
+### Fixes 🐞
+
+* Ensure content-length is always stripped from client headers by [@jlowin](https://github.com/jlowin) in [#589](https://github.com/PrefectHQ/fastmcp/pull/589)
+
+### Docs 📚
+
+* Fix redundant section of docs by [@jlowin](https://github.com/jlowin) in [#583](https://github.com/PrefectHQ/fastmcp/pull/583)
+
+**Full Changelog**: [v2.5.0...v2.5.1](https://github.com/PrefectHQ/fastmcp/compare/v2.5.0...v2.5.1)
+
+
+
+
+## [v2.5.0: Route Awakening](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.5.0)
+
+This release introduces completely new tools for generating and customizing MCP servers from OpenAPI specs and FastAPI apps, including popular requests like mechanisms for determining what routes map to what MCP components; renaming routes; and customizing the generated MCP components.
+
+### New Features 🎉
+
+* Add FastMCP 1.0 server support for in-memory Client / Testing by [@jlowin](https://github.com/jlowin) in [#539](https://github.com/PrefectHQ/fastmcp/pull/539)
+* Minor addition: add transport to stdio server in mcpconfig, with default by [@jlowin](https://github.com/jlowin) in [#555](https://github.com/PrefectHQ/fastmcp/pull/555)
+* Raise an error if a Client is created with no servers in config by [@jlowin](https://github.com/jlowin) in [#554](https://github.com/PrefectHQ/fastmcp/pull/554)
+* Expose model preferences in `Context.sample` for flexible model selection. by [@davenpi](https://github.com/davenpi) in [#542](https://github.com/PrefectHQ/fastmcp/pull/542)
+* Ensure custom routes are respected by [@jlowin](https://github.com/jlowin) in [#558](https://github.com/PrefectHQ/fastmcp/pull/558)
+* Add client method to send cancellation notifications by [@davenpi](https://github.com/davenpi) in [#563](https://github.com/PrefectHQ/fastmcp/pull/563)
+* Enhance route map logic for include/exclude OpenAPI routes by [@jlowin](https://github.com/jlowin) in [#564](https://github.com/PrefectHQ/fastmcp/pull/564)
+* Add tag-based route maps by [@jlowin](https://github.com/jlowin) in [#565](https://github.com/PrefectHQ/fastmcp/pull/565)
+* Add advanced control of openAPI route creation by [@jlowin](https://github.com/jlowin) in [#566](https://github.com/PrefectHQ/fastmcp/pull/566)
+* Make error masking configurable by [@jlowin](https://github.com/jlowin) in [#550](https://github.com/PrefectHQ/fastmcp/pull/550)
+* Ensure client headers are passed through to remote servers by [@jlowin](https://github.com/jlowin) in [#575](https://github.com/PrefectHQ/fastmcp/pull/575)
+* Use lowercase name for headers when comparing by [@jlowin](https://github.com/jlowin) in [#576](https://github.com/PrefectHQ/fastmcp/pull/576)
+* Permit more flexible name generation for OpenAPI servers by [@jlowin](https://github.com/jlowin) in [#578](https://github.com/PrefectHQ/fastmcp/pull/578)
+* Ensure that tools/templates/prompts are compatible with callable objects by [@jlowin](https://github.com/jlowin) in [#579](https://github.com/PrefectHQ/fastmcp/pull/579)
+
+### Docs 📚
+
+* Add version badge for prefix formats by [@jlowin](https://github.com/jlowin) in [#537](https://github.com/PrefectHQ/fastmcp/pull/537)
+* Add versioning note to docs by [@jlowin](https://github.com/jlowin) in [#551](https://github.com/PrefectHQ/fastmcp/pull/551)
+* Bump 2.3.6 references to 2.4.0 by [@jlowin](https://github.com/jlowin) in [#567](https://github.com/PrefectHQ/fastmcp/pull/567)
+
+**Full Changelog**: [v2.4.0...v2.5.0](https://github.com/PrefectHQ/fastmcp/compare/v2.4.0...v2.5.0)
+
+
+
+
+## [v2.4.0: Config and Conquer](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.4.0)
+
+**Note**: this release includes a backwards-incompatible change to how resources are prefixed when mounted in composed servers. However, it is only backwards-incompatible if users were running tests or manually loading resources by prefixed key; LLMs should not have any issue discovering the new route.
+
+### New Features 🎉
+
+* Allow \* Methods and all routes as tools shortcuts by [@jlowin](https://github.com/jlowin) in [#520](https://github.com/PrefectHQ/fastmcp/pull/520)
+* Improved support for config dicts by [@jlowin](https://github.com/jlowin) in [#522](https://github.com/PrefectHQ/fastmcp/pull/522)
+* Support creating clients from MCP config dicts, including multi-server clients by [@jlowin](https://github.com/jlowin) in [#527](https://github.com/PrefectHQ/fastmcp/pull/527)
+* Make resource prefix format configurable by [@jlowin](https://github.com/jlowin) in [#534](https://github.com/PrefectHQ/fastmcp/pull/534)
+
+### Fixes 🐞
+
+* Avoid hanging on initializing server session by [@jlowin](https://github.com/jlowin) in [#523](https://github.com/PrefectHQ/fastmcp/pull/523)
+
+### Breaking Changes 🛫
+
+* Remove customizable separators; improve resource separator by [@jlowin](https://github.com/jlowin) in [#526](https://github.com/PrefectHQ/fastmcp/pull/526)
+
+### Docs 📚
+
+* Improve client documentation by [@jlowin](https://github.com/jlowin) in [#517](https://github.com/PrefectHQ/fastmcp/pull/517)
+
+### Other Changes 🦾
+
+* Ensure openapi path params are handled properly by [@jlowin](https://github.com/jlowin) in [#519](https://github.com/PrefectHQ/fastmcp/pull/519)
+* better error when missing lifespan by [@zzstoatzz](https://github.com/zzstoatzz) in [#521](https://github.com/PrefectHQ/fastmcp/pull/521)
+
+**Full Changelog**: [v2.3.5...v2.4.0](https://github.com/PrefectHQ/fastmcp/compare/v2.3.5...v2.4.0)
+
+
+
+
+## [v2.3.5: Making Progress](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.5)
+
+### New Features 🎉
+
+* support messages in progress notifications by [@rickygenhealth](https://github.com/rickygenhealth) in [#471](https://github.com/PrefectHQ/fastmcp/pull/471)
+* feat: Add middleware option in server.run by [@Maxi91f](https://github.com/Maxi91f) in [#475](https://github.com/PrefectHQ/fastmcp/pull/475)
+* Add lifespan property to app by [@jlowin](https://github.com/jlowin) in [#483](https://github.com/PrefectHQ/fastmcp/pull/483)
+* Update `fastmcp run` to work with remote servers by [@jlowin](https://github.com/jlowin) in [#491](https://github.com/PrefectHQ/fastmcp/pull/491)
+* Add FastMCP.as\_proxy() by [@jlowin](https://github.com/jlowin) in [#490](https://github.com/PrefectHQ/fastmcp/pull/490)
+* Infer sse transport from urls containing /sse by [@jlowin](https://github.com/jlowin) in [#512](https://github.com/PrefectHQ/fastmcp/pull/512)
+* Add progress handler to client by [@jlowin](https://github.com/jlowin) in [#513](https://github.com/PrefectHQ/fastmcp/pull/513)
+* Store the initialize result on the client by [@jlowin](https://github.com/jlowin) in [#509](https://github.com/PrefectHQ/fastmcp/pull/509)
+
+### Fixes 🐞
+
+* Remove patch and use upstream SSEServerTransport by [@jlowin](https://github.com/jlowin) in [#425](https://github.com/PrefectHQ/fastmcp/pull/425)
+
+### Docs 📚
+
+* Update transport docs by [@jlowin](https://github.com/jlowin) in [#458](https://github.com/PrefectHQ/fastmcp/pull/458)
+* update proxy docs + example by [@zzstoatzz](https://github.com/zzstoatzz) in [#460](https://github.com/PrefectHQ/fastmcp/pull/460)
+* doc(asgi): Change custom route example to PlainTextResponse by [@mcw0933](https://github.com/mcw0933) in [#477](https://github.com/PrefectHQ/fastmcp/pull/477)
+* Store FastMCP instance on app.state.fastmcp\_server by [@jlowin](https://github.com/jlowin) in [#489](https://github.com/PrefectHQ/fastmcp/pull/489)
+* Improve AGENTS.md overview by [@jlowin](https://github.com/jlowin) in [#492](https://github.com/PrefectHQ/fastmcp/pull/492)
+* Update release numbers for anticipated version by [@jlowin](https://github.com/jlowin) in [#516](https://github.com/PrefectHQ/fastmcp/pull/516)
+
+### Other Changes 🦾
+
+* run tests on all PRs by [@jlowin](https://github.com/jlowin) in [#468](https://github.com/PrefectHQ/fastmcp/pull/468)
+* add null check by [@zzstoatzz](https://github.com/zzstoatzz) in [#473](https://github.com/PrefectHQ/fastmcp/pull/473)
+* strict typing for `server.py` by [@zzstoatzz](https://github.com/zzstoatzz) in [#476](https://github.com/PrefectHQ/fastmcp/pull/476)
+* Doc(quickstart): Fix import statements by [@mai-nakagawa](https://github.com/mai-nakagawa) in [#479](https://github.com/PrefectHQ/fastmcp/pull/479)
+* Add labeler by [@jlowin](https://github.com/jlowin) in [#484](https://github.com/PrefectHQ/fastmcp/pull/484)
+* Fix flaky timeout test by increasing timeout (#474) by [@davenpi](https://github.com/davenpi) in [#486](https://github.com/PrefectHQ/fastmcp/pull/486)
+* Skipping `test_permission_error` if runner is root. by [@ZiadAmerr](https://github.com/ZiadAmerr) in [#502](https://github.com/PrefectHQ/fastmcp/pull/502)
+* allow passing full uvicorn config by [@zzstoatzz](https://github.com/zzstoatzz) in [#504](https://github.com/PrefectHQ/fastmcp/pull/504)
+* Skip timeout tests on windows by [@jlowin](https://github.com/jlowin) in [#514](https://github.com/PrefectHQ/fastmcp/pull/514)
+
+### New Contributors
+
+* [@rickygenhealth](https://github.com/rickygenhealth) made their first contribution in [#471](https://github.com/PrefectHQ/fastmcp/pull/471)
+* [@Maxi91f](https://github.com/Maxi91f) made their first contribution in [#475](https://github.com/PrefectHQ/fastmcp/pull/475)
+* [@mcw0933](https://github.com/mcw0933) made their first contribution in [#477](https://github.com/PrefectHQ/fastmcp/pull/477)
+* [@mai-nakagawa](https://github.com/mai-nakagawa) made their first contribution in [#479](https://github.com/PrefectHQ/fastmcp/pull/479)
+* [@ZiadAmerr](https://github.com/ZiadAmerr) made their first contribution in [#502](https://github.com/PrefectHQ/fastmcp/pull/502)
+
+**Full Changelog**: [v2.3.4...v2.3.5](https://github.com/PrefectHQ/fastmcp/compare/v2.3.4...v2.3.5)
+
+
+
+
+## [v2.3.4: Error Today, Gone Tomorrow](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.4)
+
+### New Features 🎉
+
+* logging stack trace for easier debugging by [@jbkoh](https://github.com/jbkoh) in [#413](https://github.com/PrefectHQ/fastmcp/pull/413)
+* add missing StreamableHttpTransport in client exports by [@yihuang](https://github.com/yihuang) in [#408](https://github.com/PrefectHQ/fastmcp/pull/408)
+* Improve error handling for tools and resources by [@jlowin](https://github.com/jlowin) in [#434](https://github.com/PrefectHQ/fastmcp/pull/434)
+* feat: add support for removing tools from server by [@davenpi](https://github.com/davenpi) in [#437](https://github.com/PrefectHQ/fastmcp/pull/437)
+* Prune titles from JSONSchemas by [@jlowin](https://github.com/jlowin) in [#449](https://github.com/PrefectHQ/fastmcp/pull/449)
+* Declare toolsChanged capability for stdio server. by [@davenpi](https://github.com/davenpi) in [#450](https://github.com/PrefectHQ/fastmcp/pull/450)
+* Improve handling of exceptiongroups when raised in clients by [@jlowin](https://github.com/jlowin) in [#452](https://github.com/PrefectHQ/fastmcp/pull/452)
+* Add timeout support to client by [@jlowin](https://github.com/jlowin) in [#455](https://github.com/PrefectHQ/fastmcp/pull/455)
+
+### Fixes 🐞
+
+* Pin to mcp 1.8.1 to resolve callback deadlocks with SHTTP by [@jlowin](https://github.com/jlowin) in [#427](https://github.com/PrefectHQ/fastmcp/pull/427)
+* Add reprs for OpenAPI objects by [@jlowin](https://github.com/jlowin) in [#447](https://github.com/PrefectHQ/fastmcp/pull/447)
+* Ensure openapi defs for structured objects are loaded properly by [@jlowin](https://github.com/jlowin) in [#448](https://github.com/PrefectHQ/fastmcp/pull/448)
+* Ensure tests run against correct python version by [@jlowin](https://github.com/jlowin) in [#454](https://github.com/PrefectHQ/fastmcp/pull/454)
+* Ensure result is only returned if a new key was found by [@jlowin](https://github.com/jlowin) in [#456](https://github.com/PrefectHQ/fastmcp/pull/456)
+
+### Docs 📚
+
+* Add documentation for tool removal by [@jlowin](https://github.com/jlowin) in [#440](https://github.com/PrefectHQ/fastmcp/pull/440)
+
+### Other Changes 🦾
+
+* Deprecate passing settings to the FastMCP instance by [@jlowin](https://github.com/jlowin) in [#424](https://github.com/PrefectHQ/fastmcp/pull/424)
+* Add path prefix to test by [@jlowin](https://github.com/jlowin) in [#432](https://github.com/PrefectHQ/fastmcp/pull/432)
+
+### New Contributors
+
+* [@jbkoh](https://github.com/jbkoh) made their first contribution in [#413](https://github.com/PrefectHQ/fastmcp/pull/413)
+* [@davenpi](https://github.com/davenpi) made their first contribution in [#437](https://github.com/PrefectHQ/fastmcp/pull/437)
+
+**Full Changelog**: [v2.3.3...v2.3.4](https://github.com/PrefectHQ/fastmcp/compare/v2.3.3...v2.3.4)
+
+
+
+
+## [v2.3.3: SSE you later](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.3)
+
+This is a hotfix for a bug introduced in 2.3.2 that broke SSE servers
+
+### Fixes 🐞
+
+* Fix bug that sets message path and sse path to same value by [@jlowin](https://github.com/jlowin) in [#405](https://github.com/PrefectHQ/fastmcp/pull/405)
+
+### Docs 📚
+
+* Update composition docs by [@jlowin](https://github.com/jlowin) in [#403](https://github.com/PrefectHQ/fastmcp/pull/403)
+
+### Other Changes 🦾
+
+* Add test for no prefix when importing by [@jlowin](https://github.com/jlowin) in [#404](https://github.com/PrefectHQ/fastmcp/pull/404)
+
+**Full Changelog**: [v2.3.2...v2.3.3](https://github.com/PrefectHQ/fastmcp/compare/v2.3.2...v2.3.3)
+
+
+
+
+## [v2.3.2: Stuck in the Middleware With You](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.2)
+
+### New Features 🎉
+
+* Allow users to pass middleware to starlette app constructors by [@jlowin](https://github.com/jlowin) in [#398](https://github.com/PrefectHQ/fastmcp/pull/398)
+* Deprecate transport-specific methods on FastMCP server by [@jlowin](https://github.com/jlowin) in [#401](https://github.com/PrefectHQ/fastmcp/pull/401)
+
+### Docs 📚
+
+* Update CLI docs by [@jlowin](https://github.com/jlowin) in [#402](https://github.com/PrefectHQ/fastmcp/pull/402)
+
+### Other Changes 🦾
+
+* Adding 23 tests for CLI by [@didier-durand](https://github.com/didier-durand) in [#394](https://github.com/PrefectHQ/fastmcp/pull/394)
+
+**Full Changelog**: [v2.3.1...v2.3.2](https://github.com/PrefectHQ/fastmcp/compare/v2.3.1...v2.3.2)
+
+
+
+
+## [v2.3.1: For Good-nests Sake](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.1)
+
+This release primarily patches a long-standing bug with nested ASGI SSE servers.
+
+### Fixes 🐞
+
+* Fix tool result serialization when the tool returns a list by [@strawgate](https://github.com/strawgate) in [#379](https://github.com/PrefectHQ/fastmcp/pull/379)
+* Ensure FastMCP handles nested SSE and SHTTP apps properly in ASGI frameworks by [@jlowin](https://github.com/jlowin) in [#390](https://github.com/PrefectHQ/fastmcp/pull/390)
+
+### Docs 📚
+
+* Update transport docs by [@jlowin](https://github.com/jlowin) in [#377](https://github.com/PrefectHQ/fastmcp/pull/377)
+* Add llms.txt to docs by [@jlowin](https://github.com/jlowin) in [#384](https://github.com/PrefectHQ/fastmcp/pull/384)
+* Fixing various text typos by [@didier-durand](https://github.com/didier-durand) in [#385](https://github.com/PrefectHQ/fastmcp/pull/385)
+
+### Other Changes 🦾
+
+* Adding a few tests to Image type by [@didier-durand](https://github.com/didier-durand) in [#387](https://github.com/PrefectHQ/fastmcp/pull/387)
+* Adding tests for TimedCache by [@didier-durand](https://github.com/didier-durand) in [#388](https://github.com/PrefectHQ/fastmcp/pull/388)
+
+### New Contributors
+
+* [@didier-durand](https://github.com/didier-durand) made their first contribution in [#385](https://github.com/PrefectHQ/fastmcp/pull/385)
+
+**Full Changelog**: [v2.3.0...v2.3.1](https://github.com/PrefectHQ/fastmcp/compare/v2.3.0...v2.3.1)
+
+
+
+
+## [v2.3.0: Stream Me Up, Scotty](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.3.0)
+
+### New Features 🎉
+
+* Add streaming support for HTTP transport by [@jlowin](https://github.com/jlowin) in [#365](https://github.com/PrefectHQ/fastmcp/pull/365)
+* Support streaming HTTP transport in clients by [@jlowin](https://github.com/jlowin) in [#366](https://github.com/PrefectHQ/fastmcp/pull/366)
+* Add streaming support to CLI by [@jlowin](https://github.com/jlowin) in [#367](https://github.com/PrefectHQ/fastmcp/pull/367)
+
+### Fixes 🐞
+
+* Fix streaming transport initialization by [@jlowin](https://github.com/jlowin) in [#368](https://github.com/PrefectHQ/fastmcp/pull/368)
+
+### Docs 📚
+
+* Update transport documentation for streaming support by [@jlowin](https://github.com/jlowin) in [#369](https://github.com/PrefectHQ/fastmcp/pull/369)
+
+**Full Changelog**: [v2.2.10...v2.3.0](https://github.com/PrefectHQ/fastmcp/compare/v2.2.10...v2.3.0)
+
+
+
+
+## [v2.2.10: That's JSON Bourne](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.10)
+
+### Fixes 🐞
+
+* Disable automatic JSON parsing of tool args by [@jlowin](https://github.com/jlowin) in [#341](https://github.com/PrefectHQ/fastmcp/pull/341)
+* Fix prompt test by [@jlowin](https://github.com/jlowin) in [#342](https://github.com/PrefectHQ/fastmcp/pull/342)
+
+### Other Changes 🦾
+
+* Update docs.json by [@jlowin](https://github.com/jlowin) in [#338](https://github.com/PrefectHQ/fastmcp/pull/338)
+* Add test coverage + tests on 4 examples by [@alainivars](https://github.com/alainivars) in [#306](https://github.com/PrefectHQ/fastmcp/pull/306)
+
+### New Contributors
+
+* [@alainivars](https://github.com/alainivars) made their first contribution in [#306](https://github.com/PrefectHQ/fastmcp/pull/306)
+
+**Full Changelog**: [v2.2.9...v2.2.10](https://github.com/PrefectHQ/fastmcp/compare/v2.2.9...v2.2.10)
+
+
+
+
+## [v2.2.9: Str-ing the Pot (Hotfix)](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.9)
+
+This release is a hotfix for the issue detailed in #330
+
+### Fixes 🐞
+
+* Prevent invalid resource URIs by [@jlowin](https://github.com/jlowin) in [#336](https://github.com/PrefectHQ/fastmcp/pull/336)
+* Coerce numbers to str by [@jlowin](https://github.com/jlowin) in [#337](https://github.com/PrefectHQ/fastmcp/pull/337)
+
+### Docs 📚
+
+* Add client badge by [@jlowin](https://github.com/jlowin) in [#327](https://github.com/PrefectHQ/fastmcp/pull/327)
+* Update bug.yml by [@jlowin](https://github.com/jlowin) in [#328](https://github.com/PrefectHQ/fastmcp/pull/328)
+
+### Other Changes 🦾
+
+* Update quickstart.mdx example to include import by [@discdiver](https://github.com/discdiver) in [#329](https://github.com/PrefectHQ/fastmcp/pull/329)
+
+### New Contributors
+
+* [@discdiver](https://github.com/discdiver) made their first contribution in [#329](https://github.com/PrefectHQ/fastmcp/pull/329)
+
+**Full Changelog**: [v2.2.8...v2.2.9](https://github.com/PrefectHQ/fastmcp/compare/v2.2.8...v2.2.9)
+
+
+
+
+## [v2.2.8: Parse and Recreation](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.8)
+
+### New Features 🎉
+
+* Replace custom parsing with TypeAdapter by [@jlowin](https://github.com/jlowin) in [#314](https://github.com/PrefectHQ/fastmcp/pull/314)
+* Handle \*args/\*\*kwargs appropriately for various components by [@jlowin](https://github.com/jlowin) in [#317](https://github.com/PrefectHQ/fastmcp/pull/317)
+* Add timeout-graceful-shutdown as a default config for SSE app by [@jlowin](https://github.com/jlowin) in [#323](https://github.com/PrefectHQ/fastmcp/pull/323)
+* Ensure prompts return descriptions by [@jlowin](https://github.com/jlowin) in [#325](https://github.com/PrefectHQ/fastmcp/pull/325)
+
+### Fixes 🐞
+
+* Ensure that tool serialization has a graceful fallback by [@jlowin](https://github.com/jlowin) in [#310](https://github.com/PrefectHQ/fastmcp/pull/310)
+
+### Docs 📚
+
+* Update docs for clarity by [@jlowin](https://github.com/jlowin) in [#312](https://github.com/PrefectHQ/fastmcp/pull/312)
+
+### Other Changes 🦾
+
+* Remove is\_async attribute by [@jlowin](https://github.com/jlowin) in [#315](https://github.com/PrefectHQ/fastmcp/pull/315)
+* Dry out retrieving context kwarg by [@jlowin](https://github.com/jlowin) in [#316](https://github.com/PrefectHQ/fastmcp/pull/316)
+
+**Full Changelog**: [v2.2.7...v2.2.8](https://github.com/PrefectHQ/fastmcp/compare/v2.2.7...v2.2.8)
+
+
+
+
+## [v2.2.7: You Auth to Know Better](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.7)
+
+### New Features 🎉
+
+* use pydantic\_core.to\_json by [@jlowin](https://github.com/jlowin) in [#290](https://github.com/PrefectHQ/fastmcp/pull/290)
+* Ensure openapi descriptions are included in tool details by [@jlowin](https://github.com/jlowin) in [#293](https://github.com/PrefectHQ/fastmcp/pull/293)
+* Bump mcp to 1.7.1 by [@jlowin](https://github.com/jlowin) in [#298](https://github.com/PrefectHQ/fastmcp/pull/298)
+* Add support for tool annotations by [@jlowin](https://github.com/jlowin) in [#299](https://github.com/PrefectHQ/fastmcp/pull/299)
+* Add auth support by [@jlowin](https://github.com/jlowin) in [#300](https://github.com/PrefectHQ/fastmcp/pull/300)
+* Add low-level methods to client by [@jlowin](https://github.com/jlowin) in [#301](https://github.com/PrefectHQ/fastmcp/pull/301)
+* Add method for retrieving current starlette request to FastMCP context by [@jlowin](https://github.com/jlowin) in [#302](https://github.com/PrefectHQ/fastmcp/pull/302)
+* get\_starlette\_request → get\_http\_request by [@jlowin](https://github.com/jlowin) in [#303](https://github.com/PrefectHQ/fastmcp/pull/303)
+* Support custom Serializer for Tools by [@strawgate](https://github.com/strawgate) in [#308](https://github.com/PrefectHQ/fastmcp/pull/308)
+* Support proxy mount by [@jlowin](https://github.com/jlowin) in [#309](https://github.com/PrefectHQ/fastmcp/pull/309)
+
+### Other Changes 🦾
+
+* Improve context injection type checks by [@jlowin](https://github.com/jlowin) in [#291](https://github.com/PrefectHQ/fastmcp/pull/291)
+* add readme to smarthome example by [@zzstoatzz](https://github.com/zzstoatzz) in [#294](https://github.com/PrefectHQ/fastmcp/pull/294)
+
+**Full Changelog**: [v2.2.6...v2.2.7](https://github.com/PrefectHQ/fastmcp/compare/v2.2.6...v2.2.7)
+
+
+
+
+## [v2.2.6: The REST is History](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.6)
+
+### New Features 🎉
+
+* Added feature : Load MCP server using config by [@sandipan1](https://github.com/sandipan1) in [#260](https://github.com/PrefectHQ/fastmcp/pull/260)
+* small typing fixes by [@zzstoatzz](https://github.com/zzstoatzz) in [#237](https://github.com/PrefectHQ/fastmcp/pull/237)
+* Expose configurable timeout for OpenAPI by [@jlowin](https://github.com/jlowin) in [#279](https://github.com/PrefectHQ/fastmcp/pull/279)
+* Lower websockets pin for compatibility by [@jlowin](https://github.com/jlowin) in [#286](https://github.com/PrefectHQ/fastmcp/pull/286)
+* Improve OpenAPI param handling by [@jlowin](https://github.com/jlowin) in [#287](https://github.com/PrefectHQ/fastmcp/pull/287)
+
+### Fixes 🐞
+
+* Ensure openapi tool responses are properly converted by [@jlowin](https://github.com/jlowin) in [#283](https://github.com/PrefectHQ/fastmcp/pull/283)
+* Fix OpenAPI examples by [@jlowin](https://github.com/jlowin) in [#285](https://github.com/PrefectHQ/fastmcp/pull/285)
+* Fix client docs for advanced features, add tests for logging by [@jlowin](https://github.com/jlowin) in [#284](https://github.com/PrefectHQ/fastmcp/pull/284)
+
+### Other Changes 🦾
+
+* add testing doc by [@jlowin](https://github.com/jlowin) in [#264](https://github.com/PrefectHQ/fastmcp/pull/264)
+* #267 Fix openapi template resource to support multiple path parameters by [@jeger-at](https://github.com/jeger-at) in [#278](https://github.com/PrefectHQ/fastmcp/pull/278)
+
+### New Contributors
+
+* [@sandipan1](https://github.com/sandipan1) made their first contribution in [#260](https://github.com/PrefectHQ/fastmcp/pull/260)
+* [@jeger-at](https://github.com/jeger-at) made their first contribution in [#278](https://github.com/PrefectHQ/fastmcp/pull/278)
+
+**Full Changelog**: [v2.2.5...v2.2.6](https://github.com/PrefectHQ/fastmcp/compare/v2.2.5...v2.2.6)
+
+
+
+
+## [v2.2.5: Context Switching](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.5)
+
+### New Features 🎉
+
+* Add tests for tool return types; improve serialization behavior by [@jlowin](https://github.com/jlowin) in [#262](https://github.com/PrefectHQ/fastmcp/pull/262)
+* Support context injection in resources, templates, and prompts (like tools) by [@jlowin](https://github.com/jlowin) in [#263](https://github.com/PrefectHQ/fastmcp/pull/263)
+
+### Docs 📚
+
+* Update wildcards to 2.2.4 by [@jlowin](https://github.com/jlowin) in [#257](https://github.com/PrefectHQ/fastmcp/pull/257)
+* Update note in templates docs by [@jlowin](https://github.com/jlowin) in [#258](https://github.com/PrefectHQ/fastmcp/pull/258)
+* Significant documentation and test expansion for tool input types by [@jlowin](https://github.com/jlowin) in [#261](https://github.com/PrefectHQ/fastmcp/pull/261)
+
+**Full Changelog**: [v2.2.4...v2.2.5](https://github.com/PrefectHQ/fastmcp/compare/v2.2.4...v2.2.5)
+
+
+
+
+## [v2.2.4: The Wild Side, Actually](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.4)
+
+The wildcard URI templates exposed in v2.2.3 were blocked by a server-level check which is removed in this release.
+
+### New Features 🎉
+
+* Allow customization of inspector proxy port, ui port, and version by [@jlowin](https://github.com/jlowin) in [#253](https://github.com/PrefectHQ/fastmcp/pull/253)
+
+### Fixes 🐞
+
+* fix: unintended type convert by [@cutekibry](https://github.com/cutekibry) in [#252](https://github.com/PrefectHQ/fastmcp/pull/252)
+* Ensure openapi resources return valid responses by [@jlowin](https://github.com/jlowin) in [#254](https://github.com/PrefectHQ/fastmcp/pull/254)
+* Ensure servers expose template wildcards by [@jlowin](https://github.com/jlowin) in [#256](https://github.com/PrefectHQ/fastmcp/pull/256)
+
+### Docs 📚
+
+* Update README.md Grammar error by [@TechWithTy](https://github.com/TechWithTy) in [#249](https://github.com/PrefectHQ/fastmcp/pull/249)
+
+### Other Changes 🦾
+
+* Add resource template tests by [@jlowin](https://github.com/jlowin) in [#255](https://github.com/PrefectHQ/fastmcp/pull/255)
+
+### New Contributors
+
+* [@TechWithTy](https://github.com/TechWithTy) made their first contribution in [#249](https://github.com/PrefectHQ/fastmcp/pull/249)
+* [@cutekibry](https://github.com/cutekibry) made their first contribution in [#252](https://github.com/PrefectHQ/fastmcp/pull/252)
+
+**Full Changelog**: [v2.2.3...v2.2.4](https://github.com/PrefectHQ/fastmcp/compare/v2.2.3...v2.2.4)
+
+
+
+
+## [v2.2.3: The Wild Side](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.3)
+
+### New Features 🎉
+
+* Add wildcard params for resource templates by [@jlowin](https://github.com/jlowin) in [#246](https://github.com/PrefectHQ/fastmcp/pull/246)
+
+### Docs 📚
+
+* Indicate that Image class is for returns by [@jlowin](https://github.com/jlowin) in [#242](https://github.com/PrefectHQ/fastmcp/pull/242)
+* Update mermaid diagram by [@jlowin](https://github.com/jlowin) in [#243](https://github.com/PrefectHQ/fastmcp/pull/243)
+
+### Other Changes 🦾
+
+* update version badges by [@jlowin](https://github.com/jlowin) in [#248](https://github.com/PrefectHQ/fastmcp/pull/248)
+
+**Full Changelog**: [v2.2.2...v2.2.3](https://github.com/PrefectHQ/fastmcp/compare/v2.2.2...v2.2.3)
+
+
+
+
+## [v2.2.2: Prompt and Circumstance](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.2)
+
+### New Features 🎉
+
+* Add prompt support by [@jlowin](https://github.com/jlowin) in [#235](https://github.com/PrefectHQ/fastmcp/pull/235)
+
+### Fixes 🐞
+
+* Ensure that resource templates are properly exposed by [@jlowin](https://github.com/jlowin) in [#238](https://github.com/PrefectHQ/fastmcp/pull/238)
+
+### Docs 📚
+
+* Update docs for prompts by [@jlowin](https://github.com/jlowin) in [#236](https://github.com/PrefectHQ/fastmcp/pull/236)
+
+### Other Changes 🦾
+
+* Add prompt tests by [@jlowin](https://github.com/jlowin) in [#239](https://github.com/PrefectHQ/fastmcp/pull/239)
+
+**Full Changelog**: [v2.2.1...v2.2.2](https://github.com/PrefectHQ/fastmcp/compare/v2.2.1...v2.2.2)
+
+
+
+
+## [v2.2.1: Template for Success](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.1)
+
+### New Features 🎉
+
+* Add resource templates by [@jlowin](https://github.com/jlowin) in [#230](https://github.com/PrefectHQ/fastmcp/pull/230)
+
+### Fixes 🐞
+
+* Ensure that resource templates are properly exposed by [@jlowin](https://github.com/jlowin) in [#231](https://github.com/PrefectHQ/fastmcp/pull/231)
+
+### Docs 📚
+
+* Update docs for resource templates by [@jlowin](https://github.com/jlowin) in [#232](https://github.com/PrefectHQ/fastmcp/pull/232)
+
+### Other Changes 🦾
+
+* Add resource template tests by [@jlowin](https://github.com/jlowin) in [#233](https://github.com/PrefectHQ/fastmcp/pull/233)
+
+**Full Changelog**: [v2.2.0...v2.2.1](https://github.com/PrefectHQ/fastmcp/compare/v2.2.0...v2.2.1)
+
+
+
+
+## [v2.2.0: Compose Yourself](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.2.0)
+
+### New Features 🎉
+
+* Add support for mounting FastMCP servers by [@jlowin](https://github.com/jlowin) in [#175](https://github.com/PrefectHQ/fastmcp/pull/175)
+* Add support for duplicate behavior == ignore by [@jlowin](https://github.com/jlowin) in [#169](https://github.com/PrefectHQ/fastmcp/pull/169)
+
+### Breaking Changes 🛫
+
+* Refactor MCP composition by [@jlowin](https://github.com/jlowin) in [#176](https://github.com/PrefectHQ/fastmcp/pull/176)
+
+### Docs 📚
+
+* Improve integration documentation by [@jlowin](https://github.com/jlowin) in [#184](https://github.com/PrefectHQ/fastmcp/pull/184)
+* Improve documentation by [@jlowin](https://github.com/jlowin) in [#185](https://github.com/PrefectHQ/fastmcp/pull/185)
+
+### Other Changes 🦾
+
+* Add transport kwargs for mcp.run() and fastmcp run by [@jlowin](https://github.com/jlowin) in [#161](https://github.com/PrefectHQ/fastmcp/pull/161)
+* Allow resource templates to have optional / excluded arguments by [@jlowin](https://github.com/jlowin) in [#164](https://github.com/PrefectHQ/fastmcp/pull/164)
+* Update resources.mdx by [@jlowin](https://github.com/jlowin) in [#165](https://github.com/PrefectHQ/fastmcp/pull/165)
+
+### New Contributors
+
+* [@kongqi404](https://github.com/kongqi404) made their first contribution in [#181](https://github.com/PrefectHQ/fastmcp/pull/181)
+
+**Full Changelog**: [v2.1.2...v2.2.0](https://github.com/PrefectHQ/fastmcp/compare/v2.1.2...v2.2.0)
+
+
+
+
+## [v2.1.2: Copy That, Good Buddy](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.1.2)
+
+The main improvement in this release is a fix that allows FastAPI / OpenAPI-generated servers to be mounted as sub-servers.
+
+### Fixes 🐞
+
+* Ensure objects are copied properly and test mounting fastapi by [@jlowin](https://github.com/jlowin) in [#153](https://github.com/PrefectHQ/fastmcp/pull/153)
+
+### Docs 📚
+
+* Fix broken links in docs by [@jlowin](https://github.com/jlowin) in [#154](https://github.com/PrefectHQ/fastmcp/pull/154)
+
+### Other Changes 🦾
+
+* Update README.md by [@jlowin](https://github.com/jlowin) in [#149](https://github.com/PrefectHQ/fastmcp/pull/149)
+* Only apply log config to FastMCP loggers by [@jlowin](https://github.com/jlowin) in [#155](https://github.com/PrefectHQ/fastmcp/pull/155)
+* Update pyproject.toml by [@jlowin](https://github.com/jlowin) in [#156](https://github.com/PrefectHQ/fastmcp/pull/156)
+
+**Full Changelog**: [v2.1.1...v2.1.2](https://github.com/PrefectHQ/fastmcp/compare/v2.1.1...v2.1.2)
+
+
+
+
+## [v2.1.1: Doc Holiday](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.1.1)
+
+FastMCP's docs are now available at gofastmcp.com.
+
+### Docs 📚
+
+* Add docs by [@jlowin](https://github.com/jlowin) in [#136](https://github.com/PrefectHQ/fastmcp/pull/136)
+* Add docs link to readme by [@jlowin](https://github.com/jlowin) in [#137](https://github.com/PrefectHQ/fastmcp/pull/137)
+* Minor docs updates by [@jlowin](https://github.com/jlowin) in [#138](https://github.com/PrefectHQ/fastmcp/pull/138)
+
+### Fixes 🐞
+
+* fix branch name in example by [@zzstoatzz](https://github.com/zzstoatzz) in [#140](https://github.com/PrefectHQ/fastmcp/pull/140)
+
+### Other Changes 🦾
+
+* smart home example by [@zzstoatzz](https://github.com/zzstoatzz) in [#115](https://github.com/PrefectHQ/fastmcp/pull/115)
+* Remove mac os tests by [@jlowin](https://github.com/jlowin) in [#142](https://github.com/PrefectHQ/fastmcp/pull/142)
+* Expand support for various method interactions by [@jlowin](https://github.com/jlowin) in [#143](https://github.com/PrefectHQ/fastmcp/pull/143)
+* Update docs and add\_resource\_fn by [@jlowin](https://github.com/jlowin) in [#144](https://github.com/PrefectHQ/fastmcp/pull/144)
+* Update description by [@jlowin](https://github.com/jlowin) in [#145](https://github.com/PrefectHQ/fastmcp/pull/145)
+* Support openapi 3.0 and 3.1 by [@jlowin](https://github.com/jlowin) in [#147](https://github.com/PrefectHQ/fastmcp/pull/147)
+
+**Full Changelog**: [v2.1.0...v2.1.1](https://github.com/PrefectHQ/fastmcp/compare/v2.1.0...v2.1.1)
+
+
+
+
+## [v2.1.0: Tag, You're It](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.1.0)
+
+The primary motivation for this release is the fix in #128 for Claude desktop compatibility, but the primary new feature of this release is per-object tags. Currently these are for bookkeeping only but will become useful in future releases.
+
+### New Features 🎉
+
+* Add tags for all core MCP objects by [@jlowin](https://github.com/jlowin) in [#121](https://github.com/PrefectHQ/fastmcp/pull/121)
+* Ensure that openapi tags are transferred to MCP objects by [@jlowin](https://github.com/jlowin) in [#124](https://github.com/PrefectHQ/fastmcp/pull/124)
+
+### Fixes 🐞
+
+* Change default mounted tool separator from / to \_ by [@jlowin](https://github.com/jlowin) in [#128](https://github.com/PrefectHQ/fastmcp/pull/128)
+* Enter mounted app lifespans by [@jlowin](https://github.com/jlowin) in [#129](https://github.com/PrefectHQ/fastmcp/pull/129)
+* Fix CLI that called mcp instead of fastmcp by [@jlowin](https://github.com/jlowin) in [#128](https://github.com/PrefectHQ/fastmcp/pull/128)
+
+### Breaking Changes 🛫
+
+* Changed configuration for duplicate resources/tools/prompts by [@jlowin](https://github.com/jlowin) in [#121](https://github.com/PrefectHQ/fastmcp/pull/121)
+* Improve client return types by [@jlowin](https://github.com/jlowin) in [#123](https://github.com/PrefectHQ/fastmcp/pull/123)
+
+### Other Changes 🦾
+
+* Add tests for tags in server decorators by [@jlowin](https://github.com/jlowin) in [#122](https://github.com/PrefectHQ/fastmcp/pull/122)
+* Clean up server tests by [@jlowin](https://github.com/jlowin) in [#125](https://github.com/PrefectHQ/fastmcp/pull/125)
+
+**Full Changelog**: [v2.0.0...v2.1.0](https://github.com/PrefectHQ/fastmcp/compare/v2.0.0...v2.1.0)
+
+
+
+
+## [v2.0.0: Second to None](https://github.com/PrefectHQ/fastmcp/releases/tag/v2.0.0)
+
+### New Features 🎉
+
+* Support mounting FastMCP instances as sub-MCPs by [@jlowin](https://github.com/jlowin) in [#99](https://github.com/PrefectHQ/fastmcp/pull/99)
+* Add in-memory client for calling FastMCP servers (and tests) by [@jlowin](https://github.com/jlowin) in [#100](https://github.com/PrefectHQ/fastmcp/pull/100)
+* Add MCP proxy server by [@jlowin](https://github.com/jlowin) in [#105](https://github.com/PrefectHQ/fastmcp/pull/105)
+* Update FastMCP for upstream changes by [@jlowin](https://github.com/jlowin) in [#107](https://github.com/PrefectHQ/fastmcp/pull/107)
+* Generate FastMCP servers from OpenAPI specs and FastAPI by [@jlowin](https://github.com/jlowin) in [#110](https://github.com/PrefectHQ/fastmcp/pull/110)
+* Reorganize all client / transports by [@jlowin](https://github.com/jlowin) in [#111](https://github.com/PrefectHQ/fastmcp/pull/111)
+* Add sampling and roots by [@jlowin](https://github.com/jlowin) in [#117](https://github.com/PrefectHQ/fastmcp/pull/117)
+
+### Fixes 🐞
+
+* Fix bug with tools that return lists by [@jlowin](https://github.com/jlowin) in [#116](https://github.com/PrefectHQ/fastmcp/pull/116)
+
+### Other Changes 🦾
+
+* Add back FastMCP CLI by [@jlowin](https://github.com/jlowin) in [#108](https://github.com/PrefectHQ/fastmcp/pull/108)
+* Update Readme for v2 by [@jlowin](https://github.com/jlowin) in [#112](https://github.com/PrefectHQ/fastmcp/pull/112)
+* fix deprecation warnings by [@zzstoatzz](https://github.com/zzstoatzz) in [#113](https://github.com/PrefectHQ/fastmcp/pull/113)
+* Readme by [@jlowin](https://github.com/jlowin) in [#118](https://github.com/PrefectHQ/fastmcp/pull/118)
+* FastMCP 2.0 by [@jlowin](https://github.com/jlowin) in [#119](https://github.com/PrefectHQ/fastmcp/pull/119)
+
+**Full Changelog**: [v1.0...v2.0.0](https://github.com/PrefectHQ/fastmcp/compare/v1.0...v2.0.0)
+
+
+
+
+## [v1.0: It's Official](https://github.com/PrefectHQ/fastmcp/releases/tag/v1.0)
+
+This release commemorates FastMCP 1.0, which is included in the official Model Context Protocol SDK:
+
+```python
+from mcp.server.fastmcp import FastMCP
+```
+
+To the best of my knowledge, v1 is identical to the upstream version included with `mcp`.
+
+### Docs 📚
+
+* Update readme to redirect to the official SDK by [@jlowin](https://github.com/jlowin) in [#79](https://github.com/PrefectHQ/fastmcp/pull/79)
+
+### Other Changes 🦾
+
+* fix: use Mount instead of Route for SSE message handling by [@samihamine](https://github.com/samihamine) in [#77](https://github.com/PrefectHQ/fastmcp/pull/77)
+
+### New Contributors
+
+* [@samihamine](https://github.com/samihamine) made their first contribution in [#77](https://github.com/PrefectHQ/fastmcp/pull/77)
+
+**Full Changelog**: [v0.4.1...v1.0](https://github.com/PrefectHQ/fastmcp/compare/v0.4.1...v1.0)
+
+
+
+
+## [v0.4.1: String Theory](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.4.1)
+
+### Fixes 🐞
+
+* fix: handle strings containing numbers correctly by [@sd2k](https://github.com/sd2k) in [#63](https://github.com/PrefectHQ/fastmcp/pull/63)
+
+### Docs 📚
+
+* patch: Update pyproject.toml license by [@leonkozlowski](https://github.com/leonkozlowski) in [#67](https://github.com/PrefectHQ/fastmcp/pull/67)
+
+### Other Changes 🦾
+
+* Avoid new try\_eval\_type unavailable with older pydantic by [@jurasofish](https://github.com/jurasofish) in [#57](https://github.com/PrefectHQ/fastmcp/pull/57)
+* Decorator typing by [@jurasofish](https://github.com/jurasofish) in [#56](https://github.com/PrefectHQ/fastmcp/pull/56)
+
+### New Contributors
+
+* [@leonkozlowski](https://github.com/leonkozlowski) made their first contribution in [#67](https://github.com/PrefectHQ/fastmcp/pull/67)
+
+**Full Changelog**: [v0.4.0...v0.4.1](https://github.com/PrefectHQ/fastmcp/compare/v0.4.0...v0.4.1)
+
+
+
+
+## [v0.4.0: Nice to MIT You](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.4.0)
+
+This is a relatively small release in terms of features, but the version is bumped to 0.4 to reflect that the code is being relicensed from Apache 2.0 to MIT. This is to facilitate FastMCP's inclusion in the official MCP SDK.
+
+### New Features 🎉
+
+* Add pyright + tests by [@jlowin](https://github.com/jlowin) in [#52](https://github.com/PrefectHQ/fastmcp/pull/52)
+* add pgvector memory example by [@zzstoatzz](https://github.com/zzstoatzz) in [#49](https://github.com/PrefectHQ/fastmcp/pull/49)
+
+### Fixes 🐞
+
+* fix: use stderr for logging by [@sd2k](https://github.com/sd2k) in [#51](https://github.com/PrefectHQ/fastmcp/pull/51)
+
+### Docs 📚
+
+* Update ai-labeler.yml by [@jlowin](https://github.com/jlowin) in [#48](https://github.com/PrefectHQ/fastmcp/pull/48)
+* Relicense from Apache 2.0 to MIT by [@jlowin](https://github.com/jlowin) in [#54](https://github.com/PrefectHQ/fastmcp/pull/54)
+
+### Other Changes 🦾
+
+* fix warning and flake by [@zzstoatzz](https://github.com/zzstoatzz) in [#47](https://github.com/PrefectHQ/fastmcp/pull/47)
+
+### New Contributors
+
+* [@sd2k](https://github.com/sd2k) made their first contribution in [#51](https://github.com/PrefectHQ/fastmcp/pull/51)
+
+**Full Changelog**: [v0.3.5...v0.4.0](https://github.com/PrefectHQ/fastmcp/compare/v0.3.5...v0.4.0)
+
+
+
+
+## [v0.3.5: Windows of Opportunity](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.5)
+
+This release is highlighted by the ability to handle complex JSON objects as MCP inputs and improved Windows compatibility.
+
+### New Features 🎉
+
+* Set up multiple os tests by [@jlowin](https://github.com/jlowin) in [#44](https://github.com/PrefectHQ/fastmcp/pull/44)
+* Changes to accommodate windows users. by [@justjoehere](https://github.com/justjoehere) in [#42](https://github.com/PrefectHQ/fastmcp/pull/42)
+* Handle complex inputs by [@jurasofish](https://github.com/jurasofish) in [#31](https://github.com/PrefectHQ/fastmcp/pull/31)
+
+### Docs 📚
+
+* Make AI labeler more conservative by [@jlowin](https://github.com/jlowin) in [#46](https://github.com/PrefectHQ/fastmcp/pull/46)
+
+### Other Changes 🦾
+
+* Additional Windows Fixes for Dev running and for importing modules in a server by [@justjoehere](https://github.com/justjoehere) in [#43](https://github.com/PrefectHQ/fastmcp/pull/43)
+
+### New Contributors
+
+* [@justjoehere](https://github.com/justjoehere) made their first contribution in [#42](https://github.com/PrefectHQ/fastmcp/pull/42)
+* [@jurasofish](https://github.com/jurasofish) made their first contribution in [#31](https://github.com/PrefectHQ/fastmcp/pull/31)
+
+**Full Changelog**: [v0.3.4...v0.3.5](https://github.com/PrefectHQ/fastmcp/compare/v0.3.4...v0.3.5)
+
+
+
+
+## [v0.3.4: URL's Well That Ends Well](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.4)
+
+### Fixes 🐞
+
+* Handle missing config file when installing by [@jlowin](https://github.com/jlowin) in [#37](https://github.com/PrefectHQ/fastmcp/pull/37)
+* Remove BaseURL reference and use AnyURL by [@jlowin](https://github.com/jlowin) in [#40](https://github.com/PrefectHQ/fastmcp/pull/40)
+
+**Full Changelog**: [v0.3.3...v0.3.4](https://github.com/PrefectHQ/fastmcp/compare/v0.3.3...v0.3.4)
+
+
+
+
+## [v0.3.3: Dependence Day](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.3)
+
+### New Features 🎉
+
+* Surge example by [@zzstoatzz](https://github.com/zzstoatzz) in [#29](https://github.com/PrefectHQ/fastmcp/pull/29)
+* Support Python dependencies in Server by [@jlowin](https://github.com/jlowin) in [#34](https://github.com/PrefectHQ/fastmcp/pull/34)
+
+### Docs 📚
+
+* add `Contributing` section to README by [@zzstoatzz](https://github.com/zzstoatzz) in [#32](https://github.com/PrefectHQ/fastmcp/pull/32)
+
+**Full Changelog**: [v0.3.2...v0.3.3](https://github.com/PrefectHQ/fastmcp/compare/v0.3.2...v0.3.3)
+
+
+
+
+## [v0.3.2: Green with ENVy](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.2)
+
+### New Features 🎉
+
+* Support env vars when installing by [@jlowin](https://github.com/jlowin) in [#27](https://github.com/PrefectHQ/fastmcp/pull/27)
+
+### Docs 📚
+
+* Remove top level env var by [@jlowin](https://github.com/jlowin) in [#28](https://github.com/PrefectHQ/fastmcp/pull/28)
+
+**Full Changelog**: [v0.3.1...v0.3.2](https://github.com/PrefectHQ/fastmcp/compare/v0.3.1...v0.3.2)
+
+
+
+
+## [v0.3.1](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.1)
+
+### New Features 🎉
+
+* Update README.md by [@jlowin](https://github.com/jlowin) in [#23](https://github.com/PrefectHQ/fastmcp/pull/23)
+* add rich handler and dotenv loading for settings by [@zzstoatzz](https://github.com/zzstoatzz) in [#22](https://github.com/PrefectHQ/fastmcp/pull/22)
+* print exception when server can't start by [@jlowin](https://github.com/jlowin) in [#25](https://github.com/PrefectHQ/fastmcp/pull/25)
+
+### Docs 📚
+
+* Update README.md by [@jlowin](https://github.com/jlowin) in [#24](https://github.com/PrefectHQ/fastmcp/pull/24)
+
+### Other Changes 🦾
+
+* Remove log by [@jlowin](https://github.com/jlowin) in [#26](https://github.com/PrefectHQ/fastmcp/pull/26)
+
+**Full Changelog**: [v0.3.0...v0.3.1](https://github.com/PrefectHQ/fastmcp/compare/v0.3.0...v0.3.1)
+
+
+
+
+## [v0.3.0: Prompt and Circumstance](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.3.0)
+
+### New Features 🎉
+
+* Update README by [@jlowin](https://github.com/jlowin) in [#3](https://github.com/PrefectHQ/fastmcp/pull/3)
+* Make log levels strings by [@jlowin](https://github.com/jlowin) in [#4](https://github.com/PrefectHQ/fastmcp/pull/4)
+* Make content method a function by [@jlowin](https://github.com/jlowin) in [#5](https://github.com/PrefectHQ/fastmcp/pull/5)
+* Add template support by [@jlowin](https://github.com/jlowin) in [#6](https://github.com/PrefectHQ/fastmcp/pull/6)
+* Refactor resources module by [@jlowin](https://github.com/jlowin) in [#7](https://github.com/PrefectHQ/fastmcp/pull/7)
+* Clean up cli imports by [@jlowin](https://github.com/jlowin) in [#8](https://github.com/PrefectHQ/fastmcp/pull/8)
+* Prepare to list templates by [@jlowin](https://github.com/jlowin) in [#11](https://github.com/PrefectHQ/fastmcp/pull/11)
+* Move image to separate module by [@jlowin](https://github.com/jlowin) in [#9](https://github.com/PrefectHQ/fastmcp/pull/9)
+* Add support for request context, progress, logging, etc. by [@jlowin](https://github.com/jlowin) in [#12](https://github.com/PrefectHQ/fastmcp/pull/12)
+* Add context tests and better runtime loads by [@jlowin](https://github.com/jlowin) in [#13](https://github.com/PrefectHQ/fastmcp/pull/13)
+* Refactor tools + resourcemanager by [@jlowin](https://github.com/jlowin) in [#14](https://github.com/PrefectHQ/fastmcp/pull/14)
+* func → fn everywhere by [@jlowin](https://github.com/jlowin) in [#15](https://github.com/PrefectHQ/fastmcp/pull/15)
+* Add support for prompts by [@jlowin](https://github.com/jlowin) in [#16](https://github.com/PrefectHQ/fastmcp/pull/16)
+* Create LICENSE by [@jlowin](https://github.com/jlowin) in [#18](https://github.com/PrefectHQ/fastmcp/pull/18)
+* Update cli file spec by [@jlowin](https://github.com/jlowin) in [#19](https://github.com/PrefectHQ/fastmcp/pull/19)
+* Update readmeUpdate README by [@jlowin](https://github.com/jlowin) in [#20](https://github.com/PrefectHQ/fastmcp/pull/20)
+* Use hatchling for version by [@jlowin](https://github.com/jlowin) in [#21](https://github.com/PrefectHQ/fastmcp/pull/21)
+
+### Other Changes 🦾
+
+* Add echo server by [@jlowin](https://github.com/jlowin) in [#1](https://github.com/PrefectHQ/fastmcp/pull/1)
+* Add github workflows by [@jlowin](https://github.com/jlowin) in [#2](https://github.com/PrefectHQ/fastmcp/pull/2)
+* typing updates by [@zzstoatzz](https://github.com/zzstoatzz) in [#17](https://github.com/PrefectHQ/fastmcp/pull/17)
+
+### New Contributors
+
+* [@jlowin](https://github.com/jlowin) made their first contribution in [#1](https://github.com/PrefectHQ/fastmcp/pull/1)
+* [@zzstoatzz](https://github.com/zzstoatzz) made their first contribution in [#17](https://github.com/PrefectHQ/fastmcp/pull/17)
+
+**Full Changelog**: [v0.2.0...v0.3.0](https://github.com/PrefectHQ/fastmcp/compare/v0.2.0...v0.3.0)
+
+
+
+
+## [v0.2.0](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.2.0)
+
+**Full Changelog**: [v0.1.0...v0.2.0](https://github.com/PrefectHQ/fastmcp/compare/v0.1.0...v0.2.0)
+
+
+
+
+## [v0.1.0](https://github.com/PrefectHQ/fastmcp/releases/tag/v0.1.0)
+
+The very first release of FastMCP! 🎉
+
+**Full Changelog**: [Initial commits](https://github.com/PrefectHQ/fastmcp/commits/v0.1.0)
+
diff --git a/docs/v3/cli/auth.mdx b/docs/v3/cli/auth.mdx
new file mode 100644
index 000000000..71b89e08a
--- /dev/null
+++ b/docs/v3/cli/auth.mdx
@@ -0,0 +1,85 @@
+---
+title: Auth Utilities
+sidebarTitle: Auth
+description: Create and validate CIMD documents for OAuth
+icon: key
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+The `fastmcp auth` commands help with CIMD (Client ID Metadata Document) management — part of MCP's OAuth authentication flow. A CIMD is a JSON document you host at an HTTPS URL to identify your client application to MCP servers.
+
+## Creating a CIMD
+
+`fastmcp auth cimd create` generates a CIMD document:
+
+```bash
+fastmcp auth cimd create \
+ --name "My App" \
+ --redirect-uri "http://localhost:*/callback"
+```
+
+```json
+{
+ "client_id": "https://your-domain.com/oauth/client.json",
+ "client_name": "My App",
+ "redirect_uris": ["http://localhost:*/callback"],
+ "token_endpoint_auth_method": "none"
+}
+```
+
+The generated document includes a placeholder `client_id` — update it to match the URL where you'll host the document before deploying.
+
+### Options
+
+| Option | Flag | Description |
+| ------ | ---- | ----------- |
+| Name | `--name` | **Required.** Human-readable client name |
+| Redirect URI | `--redirect-uri` | **Required.** Allowed redirect URIs (repeatable) |
+| Client URI | `--client-uri` | Client's home page URL |
+| Logo URI | `--logo-uri` | Client's logo URL |
+| Scope | `--scope` | Space-separated list of scopes |
+| Output | `--output`, `-o` | Save to file (default: stdout) |
+| Pretty | `--pretty` | Pretty-print JSON (default: true) |
+
+### Example
+
+```bash
+fastmcp auth cimd create \
+ --name "My Production App" \
+ --redirect-uri "http://localhost:*/callback" \
+ --redirect-uri "https://myapp.example.com/callback" \
+ --client-uri "https://myapp.example.com" \
+ --scope "read write" \
+ --output client.json
+```
+
+## Validating a CIMD
+
+`fastmcp auth cimd validate` fetches a hosted CIMD and verifies it conforms to the spec:
+
+```bash
+fastmcp auth cimd validate https://myapp.example.com/oauth/client.json
+```
+
+The validator checks that the URL is valid (HTTPS, non-root path), the document is valid JSON, the `client_id` matches the URL, and no shared-secret auth methods are used.
+
+On success:
+
+```
+→ Fetching https://myapp.example.com/oauth/client.json...
+✓ Valid CIMD document
+
+Document details:
+ client_id: https://myapp.example.com/oauth/client.json
+ client_name: My App
+ token_endpoint_auth_method: none
+ redirect_uris:
+ • http://localhost:*/callback
+```
+
+| Option | Flag | Description |
+| ------ | ---- | ----------- |
+| Timeout | `--timeout`, `-t` | HTTP request timeout in seconds (default: 10) |
diff --git a/docs/v3/cli/client.mdx b/docs/v3/cli/client.mdx
new file mode 100644
index 000000000..b5ef1d4e8
--- /dev/null
+++ b/docs/v3/cli/client.mdx
@@ -0,0 +1,144 @@
+---
+title: Client Commands
+sidebarTitle: Client
+description: List tools, call them, and discover configured servers
+icon: satellite-dish
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+The CLI can act as an MCP client — connecting to any server (local or remote) to list what it exposes and call its tools directly. This is useful for development, debugging, scripting, and giving shell-capable LLM agents access to MCP servers.
+
+## Listing Tools
+
+`fastmcp list` connects to a server and prints its tools as function signatures, showing parameter names, types, and descriptions at a glance:
+
+```bash
+fastmcp list http://localhost:8000/mcp
+fastmcp list server.py
+fastmcp list weather # name-based resolution
+```
+
+When you need the full JSON Schema for a tool's inputs or outputs — for understanding nested objects, enum constraints, or complex types — opt in with `--input-schema` or `--output-schema`:
+
+```bash
+fastmcp list server.py --input-schema
+```
+
+### Resources and Prompts
+
+By default, only tools are shown. Add `--resources` or `--prompts` to include those:
+
+```bash
+fastmcp list server.py --resources --prompts
+```
+
+### Machine-Readable Output
+
+The `--json` flag switches to structured JSON with full schemas included. This is the format to use when feeding tool definitions to an LLM or building automation:
+
+```bash
+fastmcp list server.py --json
+```
+
+### Options
+
+| Option | Flag | Description |
+| ------ | ---- | ----------- |
+| Command | `--command` | Connect via stdio (e.g., `'npx -y @mcp/server'`) |
+| Transport | `--transport`, `-t` | Force `http` or `sse` for URL targets |
+| Resources | `--resources` | Include resources in output |
+| Prompts | `--prompts` | Include prompts in output |
+| Input Schema | `--input-schema` | Show full input schemas |
+| Output Schema | `--output-schema` | Show full output schemas |
+| JSON | `--json` | Structured JSON output |
+| Timeout | `--timeout` | Connection timeout in seconds |
+| Auth | `--auth` | `oauth` (default for HTTP), a bearer token, or `none` |
+
+## Calling Tools
+
+`fastmcp call` invokes a single tool on a server. Pass arguments as `key=value` pairs — the CLI fetches the tool's schema and coerces your string values to the right types automatically:
+
+```bash
+fastmcp call server.py greet name=World
+fastmcp call http://localhost:8000/mcp search query=hello limit=5
+```
+
+Type coercion is schema-driven: `"5"` becomes the integer `5` when the schema expects an integer. Booleans accept `true`/`false`, `yes`/`no`, and `1`/`0`. Arrays and objects are parsed as JSON.
+
+### Complex Arguments
+
+For tools with nested or structured parameters, `key=value` syntax gets awkward. Pass a single JSON object instead:
+
+```bash
+fastmcp call server.py create_item '{"name": "Widget", "tags": ["sale"], "metadata": {"color": "blue"}}'
+```
+
+Or use `--input-json` to provide a base dictionary, then override individual keys with `key=value` pairs:
+
+```bash
+fastmcp call server.py search --input-json '{"query": "hello", "limit": 5}' limit=10
+```
+
+### Error Handling
+
+If you misspell a tool name, the CLI suggests corrections via fuzzy matching. Missing required arguments produce a clear message with the tool's signature as a reminder. Tool execution errors are printed with a non-zero exit code, making the CLI straightforward to use in scripts.
+
+### Structured Output
+
+`--json` emits the raw result including content blocks, error status, and structured content:
+
+```bash
+fastmcp call server.py get_weather city=London --json
+```
+
+### Interactive Elicitation
+
+Some tools request additional input during execution through MCP's elicitation mechanism. When this happens, the CLI prompts you in the terminal — showing each field's name, type, and whether it's required. You can type `decline` to skip a question or `cancel` to abort the call entirely.
+
+### Options
+
+| Option | Flag | Description |
+| ------ | ---- | ----------- |
+| Command | `--command` | Connect via stdio |
+| Transport | `--transport`, `-t` | Force `http` or `sse` |
+| Input JSON | `--input-json` | Base arguments as JSON (merged with `key=value`) |
+| JSON | `--json` | Raw JSON output |
+| Timeout | `--timeout` | Connection timeout in seconds |
+| Auth | `--auth` | `oauth`, a bearer token, or `none` |
+
+## Discovering Configured Servers
+
+`fastmcp discover` scans your machine for MCP servers configured in editors and tools. It checks:
+
+- **Claude Desktop** — `claude_desktop_config.json`
+- **Claude Code** — `~/.claude.json`
+- **Cursor** — `.cursor/mcp.json` (walks up from current directory)
+- **Gemini CLI** — `~/.gemini/settings.json`
+- **Goose** — `~/.config/goose/config.yaml`
+- **Project** — `./mcp.json` in the current directory
+
+```bash
+fastmcp discover
+```
+
+The output groups servers by source, showing each server's name and transport. Filter by source or get machine-readable output:
+
+```bash
+fastmcp discover --source claude-code
+fastmcp discover --source cursor --source gemini --json
+```
+
+Any server that appears here can be used by name with `list`, `call`, and other commands — so you can go from "I have a server in Claude Code" to querying it without copying URLs or paths.
+
+## LLM Agent Integration
+
+For LLM agents that can execute shell commands but don't have native MCP support, the CLI provides a clean bridge. The agent calls `fastmcp list --json` to discover available tools with full schemas, then `fastmcp call --json` to invoke them with structured results.
+
+Because the CLI handles connection management, transport selection, and type coercion internally, the agent doesn't need to understand MCP protocol details — it just reads JSON and constructs shell commands.
+
+## Remote Stdio Bridges
+
+For MCP hosts that expect a local stdio command but need to connect to a remote HTTP server, use [`fastmcp-remote`](/clients/fastmcp-remote). It provides a small standalone bridge for host configuration, while `fastmcp list` and `fastmcp call` remain focused on direct inspection and invocation from the terminal.
diff --git a/docs/v3/cli/generate-cli.mdx b/docs/v3/cli/generate-cli.mdx
new file mode 100644
index 000000000..2754d199a
--- /dev/null
+++ b/docs/v3/cli/generate-cli.mdx
@@ -0,0 +1,106 @@
+---
+title: Generate CLI
+sidebarTitle: Generate CLI
+description: Scaffold a standalone typed CLI from any MCP server
+icon: wand-magic-sparkles
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+`fastmcp list` and `fastmcp call` are general-purpose — you always specify the server, the tool name, and the arguments from scratch. `fastmcp generate-cli` goes further: it connects to a server, reads its tool schemas, and writes a standalone Python script where every tool is a proper subcommand with typed flags, help text, and tab completion. The result is a CLI that feels hand-written for that specific server.
+
+MCP tool schemas already contain everything a CLI framework needs — parameter names, types, descriptions, required/optional status, and defaults. `generate-cli` maps that into [cyclopts](https://cyclopts.readthedocs.io/) commands, so JSON Schema types become Python type annotations, descriptions become `--help` text, and required parameters become mandatory flags.
+
+## Generating a Script
+
+Point the command at any [server target](/cli/overview#server-targets) and it writes a CLI script:
+
+```bash
+fastmcp generate-cli weather
+fastmcp generate-cli http://localhost:8000/mcp
+fastmcp generate-cli server.py my_weather_cli.py
+```
+
+The second positional argument sets the output path (defaults to `cli.py`). If the file already exists, pass `-f` to overwrite:
+
+```bash
+fastmcp generate-cli weather -f
+```
+
+## What You Get
+
+The generated script is a regular Python file — executable, editable, and yours:
+
+```
+$ python cli.py call-tool --help
+Usage: weather-cli call-tool COMMAND
+
+Call a tool on the server
+
+Commands:
+ get_forecast Get the weather forecast for a city.
+ search_city Search for a city by name.
+```
+
+Each tool has typed parameters with help text pulled directly from the server's schema:
+
+```
+$ python cli.py call-tool get_forecast --help
+Usage: weather-cli call-tool get_forecast [OPTIONS]
+
+Get the weather forecast for a city.
+
+Options:
+ --city [str] City name (required)
+ --days [int] Number of forecast days (default: 3)
+```
+
+Beyond tool commands, the script includes generic MCP operations — `list-tools`, `list-resources`, `read-resource`, `list-prompts`, and `get-prompt` — that always reflect the server's current state, even if tools have changed since generation.
+
+## Parameter Handling
+
+Parameters are mapped based on their JSON Schema type:
+
+**Simple types** (`string`, `integer`, `number`, `boolean`) become typed flags:
+
+```bash
+python cli.py call-tool get_forecast --city London --days 3
+```
+
+**Arrays of simple types** become repeatable flags:
+
+```bash
+python cli.py call-tool tag_items --tags python --tags fastapi --tags mcp
+```
+
+**Complex types** (objects, nested arrays, unions) accept JSON strings. The `--help` output shows the full schema so you know what structure to pass:
+
+```bash
+python cli.py call-tool create_user \
+ --name John \
+ --metadata '{"role": "admin", "dept": "engineering"}'
+```
+
+## Agent Skill
+
+Alongside the CLI script, `generate-cli` writes a `SKILL.md` file — a [Claude Code agent skill](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/skills) that documents every tool's exact invocation syntax, parameter flags, types, and descriptions. An agent can pick up the CLI immediately without running `--help` or experimenting with flag names.
+
+To skip skill generation:
+
+```bash
+fastmcp generate-cli weather --no-skill
+```
+
+## How It Works
+
+The generated script is a *client*, not a server — it connects to the server on every invocation rather than bundling it. A `CLIENT_SPEC` variable at the top holds the resolved transport (a URL string or `StdioTransport` with baked-in command and arguments).
+
+The most common edit is changing `CLIENT_SPEC` — for example, pointing a script generated from a dev server at production. Beyond that, the helper functions (`_call_tool`, `_print_tool_result`) are thin wrappers around `fastmcp.Client` that are easy to adapt.
+
+The script requires `fastmcp` as a dependency. If it lives outside a project that already has FastMCP installed:
+
+```bash
+uv run --with fastmcp python cli.py call-tool get_forecast --city London
+```
diff --git a/docs/v3/cli/inspecting.mdx b/docs/v3/cli/inspecting.mdx
new file mode 100644
index 000000000..657921357
--- /dev/null
+++ b/docs/v3/cli/inspecting.mdx
@@ -0,0 +1,72 @@
+---
+title: Inspecting Servers
+sidebarTitle: Inspecting
+description: View a server's components and metadata
+icon: magnifying-glass
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+`fastmcp inspect` loads a server and reports what it contains — its tools, resources, prompts, version, and metadata. The default output is a human-readable summary:
+
+```bash
+fastmcp inspect server.py
+```
+
+```
+Server: MyServer
+Instructions: A helpful MCP server
+Version: 1.0.0
+
+Components:
+ Tools: 5
+ Prompts: 2
+ Resources: 3
+ Templates: 1
+
+Environment:
+ FastMCP: 2.0.0
+ MCP: 1.0.0
+
+Use --format [fastmcp|mcp] for complete JSON output
+```
+
+## JSON Output
+
+For programmatic use, two JSON formats are available:
+
+**FastMCP format** (`--format fastmcp`) includes everything FastMCP knows about the server — tool tags, enabled status, output schemas, annotations, and custom metadata. Field names use `snake_case`. This is the format for debugging and introspecting FastMCP servers.
+
+**MCP protocol format** (`--format mcp`) shows exactly what MCP clients see through the protocol — only standard MCP fields, `camelCase` names, no FastMCP-specific extensions. This is the format for verifying client compatibility and debugging what clients actually receive.
+
+```bash
+# Full FastMCP metadata to stdout
+fastmcp inspect server.py --format fastmcp
+
+# MCP protocol view saved to file
+fastmcp inspect server.py --format mcp -o manifest.json
+```
+
+## Options
+
+| Option | Flag | Description |
+| ------ | ---- | ----------- |
+| Format | `--format`, `-f` | `fastmcp` or `mcp` (required when using `-o`) |
+| Output File | `--output`, `-o` | Save to file instead of stdout |
+
+## Entrypoints
+
+The `inspect` command supports the same local entrypoints as [`fastmcp run`](/cli/running): inferred instances, explicit entrypoints, factory functions, and `fastmcp.json` configs.
+
+```bash
+fastmcp inspect server.py # inferred instance
+fastmcp inspect server.py:my_server # explicit entrypoint
+fastmcp inspect server.py:create_server # factory function
+fastmcp inspect fastmcp.json # config file
+```
+
+
+`inspect` only works with local files and `fastmcp.json` — it doesn't connect to remote URLs or standard MCP config files.
+
diff --git a/docs/v3/cli/install-mcp.mdx b/docs/v3/cli/install-mcp.mdx
new file mode 100644
index 000000000..0171b7854
--- /dev/null
+++ b/docs/v3/cli/install-mcp.mdx
@@ -0,0 +1,146 @@
+---
+title: Install MCP Servers
+sidebarTitle: Install MCPs
+description: Install MCP servers into Claude, Cursor, Gemini, and other clients
+icon: download
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+`fastmcp install` registers a server with an MCP client application so the client can launch it automatically. Each MCP client runs servers in its own isolated environment, which means dependencies need to be explicitly declared — you can't rely on whatever happens to be installed locally.
+
+```bash
+fastmcp install claude-desktop server.py
+fastmcp install claude-code server.py --with pandas --with matplotlib
+fastmcp install cursor server.py -e .
+```
+
+
+`uv` must be installed and available in your system PATH. Both Claude Desktop and Cursor run servers in isolated environments managed by `uv`. On macOS, install it globally with Homebrew for Claude Desktop compatibility: `brew install uv`.
+
+
+## Supported Clients
+
+| Client | Install method |
+| ------ | -------------- |
+| `claude-code` | Claude Code's built-in MCP management |
+| `claude-desktop` | Direct config file modification |
+| `cursor` | Deeplink that opens Cursor for confirmation |
+| `gemini-cli` | Gemini CLI's built-in MCP management |
+| `goose` | Deeplink that opens Goose for confirmation (uses `uvx`) |
+| `mcp-json` | Generates standard MCP JSON config for manual use |
+| `stdio` | Outputs the shell command to run via stdio |
+
+## Declaring Dependencies
+
+Because MCP clients run servers in isolation, you need to tell the install command what your server needs. There are two approaches:
+
+**Command-line flags** let you specify dependencies directly:
+
+```bash
+fastmcp install claude-desktop server.py --with pandas --with "sqlalchemy>=2.0"
+fastmcp install cursor server.py -e . --with-requirements requirements.txt
+```
+
+**`fastmcp.json`** configuration files declare dependencies alongside the server definition. When you install from a config file, dependencies are picked up automatically:
+
+```bash
+fastmcp install claude-desktop fastmcp.json
+fastmcp install claude-desktop # auto-detects fastmcp.json in current directory
+```
+
+See [Server Configuration](/deployment/server-configuration) for the full config format.
+
+## Options
+
+| Option | Flag | Description |
+| ------ | ---- | ----------- |
+| Server Name | `--server-name`, `-n` | Custom name for the server |
+| Editable Package | `--with-editable`, `-e` | Install a directory in editable mode |
+| Extra Packages | `--with` | Additional packages (repeatable) |
+| Environment Variables | `--env` | `KEY=VALUE` pairs (repeatable) |
+| Environment File | `--env-file`, `-f` | Load env vars from a `.env` file |
+| Python | `--python` | Python version (e.g., `3.11`) |
+| Project | `--project` | Run within a uv project directory |
+| Requirements | `--with-requirements` | Install from a requirements file |
+| Config Path | `--config-path` | Custom path to Claude Desktop config directory (`claude-desktop` only) |
+
+## Examples
+
+```bash
+# Basic install with auto-detected server instance
+fastmcp install claude-desktop server.py
+
+# Install from fastmcp.json with auto-detection
+fastmcp install claude-desktop
+
+# Explicit entrypoint with dependencies
+fastmcp install claude-desktop server.py:my_server \
+ --server-name "My Analysis Server" \
+ --with pandas
+
+# With environment variables
+fastmcp install claude-code server.py \
+ --env API_KEY=secret \
+ --env DEBUG=true
+
+# With env file
+fastmcp install cursor server.py --env-file .env
+
+# Specific Python version and requirements file
+fastmcp install claude-desktop server.py \
+ --python 3.11 \
+ --with-requirements requirements.txt
+
+# With custom config path (claude-desktop only)
+fastmcp install claude-desktop server.py \
+ --config-path "C:\Users\username\AppData\Local\Packages\Claude_xyz\LocalCache\Roaming\Claude"
+```
+
+## Generating MCP JSON
+
+The `mcp-json` target generates standard MCP configuration JSON instead of installing into a specific client. This is useful for clients that FastMCP doesn't directly support, for CI/CD environments, or for sharing server configs:
+
+```bash
+fastmcp install mcp-json server.py
+```
+
+The output follows the standard format used by Claude Desktop, Cursor, and other MCP clients:
+
+```json
+{
+ "server-name": {
+ "command": "uv",
+ "args": ["run", "--with", "fastmcp", "fastmcp", "run", "/path/to/server.py"],
+ "env": {
+ "API_KEY": "value"
+ }
+ }
+}
+```
+
+Use `--copy` to send it to your clipboard instead of stdout.
+
+## Generating Stdio Commands
+
+The `stdio` target outputs the shell command an MCP host would use to start your server over stdio:
+
+```bash
+fastmcp install stdio server.py
+# Output: uv run --with fastmcp fastmcp run /absolute/path/to/server.py
+```
+
+When installing from a `fastmcp.json`, dependencies from the config are included automatically:
+
+```bash
+fastmcp install stdio fastmcp.json
+# Output: uv run --with fastmcp --with pillow --with 'qrcode[pil]>=8.0' fastmcp run /path/to/server.py
+```
+
+Use `--copy` to copy to clipboard.
+
+
+`fastmcp install` is designed for local server files with stdio transport. For remote servers running over HTTP, use your client's native configuration — FastMCP's value here is simplifying the complex local setup with `uv`, dependencies, and environment variables.
+
diff --git a/docs/v3/cli/overview.mdx b/docs/v3/cli/overview.mdx
new file mode 100644
index 000000000..54783bef0
--- /dev/null
+++ b/docs/v3/cli/overview.mdx
@@ -0,0 +1,104 @@
+---
+title: CLI
+sidebarTitle: Overview
+description: The fastmcp command-line interface
+icon: terminal
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+The `fastmcp` CLI is installed automatically with FastMCP. It's the primary way to run, test, install, and interact with MCP servers from your terminal.
+
+```bash
+fastmcp --help
+```
+
+## Commands at a Glance
+
+| Command | What it does |
+| ------- | ------------ |
+| [`run`](/cli/running) | Run a server (local file, factory function, remote URL, or config file) |
+| [`dev apps`](/cli/running#previewing-apps) | Launch a browser-based preview UI for Prefab App tools |
+| [`dev inspector`](/cli/running#development-with-the-inspector) | Launch a server inside the MCP Inspector for interactive testing |
+| [`install`](/cli/install-mcp) | Install a server into Claude Code, Claude Desktop, Cursor, Gemini CLI, or Goose |
+| [`inspect`](/cli/inspecting) | Print a server's tools, resources, and prompts as a summary or JSON report |
+| [`list`](/cli/client) | List a server's tools (and optionally resources and prompts) |
+| [`call`](/cli/client#calling-tools) | Call a single tool with arguments |
+| [`discover`](/cli/client#discovering-configured-servers) | Find MCP servers configured in your editors and tools |
+| [`generate-cli`](/cli/generate-cli) | Scaffold a standalone typed CLI from a server's tool schemas |
+| [`project prepare`](/cli/running#pre-building-environments) | Pre-install dependencies into a reusable uv project |
+| [`auth cimd`](/cli/auth) | Create and validate CIMD documents for OAuth |
+| `version` | Print version info (`--copy` to copy to clipboard) |
+
+## Server Targets
+
+Most commands need to know *which server* to talk to. You pass a "server spec" as the first argument, and FastMCP resolves the right transport automatically.
+
+**URLs** connect to a running HTTP server:
+
+```bash
+fastmcp list http://localhost:8000/mcp
+fastmcp call http://localhost:8000/mcp get_forecast city=London
+```
+
+**Python files** are loaded directly — no `mcp.run()` boilerplate needed. FastMCP finds a server instance named `mcp`, `server`, or `app` in the file, or you can specify one explicitly:
+
+```bash
+fastmcp list server.py
+fastmcp run server.py:my_custom_server
+```
+
+**Config files** work too — both FastMCP's own `fastmcp.json` format and standard MCP config files with an `mcpServers` key:
+
+```bash
+fastmcp run fastmcp.json
+fastmcp list mcp-config.json
+```
+
+**Stdio commands** connect to any MCP server that speaks over standard I/O. Use `--command` instead of a positional argument:
+
+```bash
+fastmcp list --command 'npx -y @modelcontextprotocol/server-github'
+```
+
+### Name-Based Resolution
+
+If your servers are already configured in an editor or tool, you can refer to them by name. FastMCP scans configs from Claude Desktop, Claude Code, Cursor, Gemini CLI, and Goose:
+
+```bash
+fastmcp list weather
+fastmcp call weather get_forecast city=London
+```
+
+When the same name appears in multiple configs, use the `source:name` form to be specific:
+
+```bash
+fastmcp list claude-code:my-server
+fastmcp call cursor:weather get_forecast city=London
+```
+
+Run [`fastmcp discover`](/cli/client#discovering-configured-servers) to see what's available on your machine.
+
+## Authentication
+
+When targeting an HTTP URL, the CLI enables OAuth authentication by default. If the server requires it, you'll be guided through the flow (typically opening a browser). If it doesn't, the setup is a silent no-op.
+
+To skip authentication entirely — useful for local development servers — pass `--auth none`:
+
+```bash
+fastmcp call http://localhost:8000/mcp my_tool --auth none
+```
+
+You can also pass a bearer token directly:
+
+```bash
+fastmcp list http://localhost:8000/mcp --auth "Bearer sk-..."
+```
+
+## Transport Override
+
+FastMCP defaults to Streamable HTTP for URL targets. If the server only supports Server-Sent Events (SSE), force the older transport:
+
+```bash
+fastmcp list http://localhost:8000 --transport sse
+```
diff --git a/docs/v3/cli/running.mdx b/docs/v3/cli/running.mdx
new file mode 100644
index 000000000..b0cad0a0b
--- /dev/null
+++ b/docs/v3/cli/running.mdx
@@ -0,0 +1,166 @@
+---
+title: Running Servers
+sidebarTitle: Running
+description: Start, develop, and configure servers from the command line
+icon: play
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+## Starting a Server
+
+`fastmcp run` starts a server. Point it at a Python file, a factory function, a remote URL, or a config file:
+
+```bash
+fastmcp run server.py
+fastmcp run server.py:create_server
+fastmcp run https://example.com/mcp
+fastmcp run fastmcp.json
+```
+
+By default, the server runs over **stdio** — the transport that MCP clients like Claude Desktop expect. To serve over HTTP instead, specify the transport:
+
+```bash
+fastmcp run server.py --transport http
+fastmcp run server.py --transport http --host 0.0.0.0 --port 9000
+```
+
+### Entrypoints
+
+FastMCP supports several ways to locate and start your server:
+
+**Inferred instance** — FastMCP imports the file and looks for a variable named `mcp`, `server`, or `app`:
+
+```bash
+fastmcp run server.py
+```
+
+**Explicit instance** — point at a specific variable:
+
+```bash
+fastmcp run server.py:my_server
+```
+
+**Factory function** — FastMCP calls the function and uses the returned server. Useful when your server needs async setup or configuration that runs before startup:
+
+```bash
+fastmcp run server.py:create_server
+```
+
+**Remote URL** — starts a local proxy that bridges to a remote server. Handy for local development against a deployed server, or for bridging a remote HTTP server to stdio:
+
+```bash
+fastmcp run https://example.com/mcp
+```
+
+**FastMCP config** — uses a `fastmcp.json` file that declaratively specifies the server, its dependencies, and deployment settings. When you run `fastmcp run` with no arguments, it auto-detects `fastmcp.json` in the current directory:
+
+```bash
+fastmcp run
+fastmcp run my-config.fastmcp.json
+```
+
+See [Server Configuration](/deployment/server-configuration) for the full `fastmcp.json` format.
+
+**MCP config** — runs servers defined in a standard MCP configuration file (any `.json` with an `mcpServers` key):
+
+```bash
+fastmcp run mcp.json
+```
+
+
+`fastmcp run` completely ignores the `if __name__ == "__main__"` block. Any setup code in that block won't execute. If you need initialization logic to run, use a [factory function](/cli/overview#factory-functions).
+
+
+### Options
+
+| Option | Flag | Description |
+| ------ | ---- | ----------- |
+| Transport | `--transport`, `-t` | `stdio` (default), `http`, or `sse` |
+| Host | `--host` | Bind address for HTTP (default: `127.0.0.1`) |
+| Port | `--port`, `-p` | Bind port for HTTP (default: `8000`) |
+| Path | `--path` | URL path for HTTP (default: `/mcp/`) |
+| Log Level | `--log-level`, `-l` | `DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL` |
+| No Banner | `--no-banner` | Suppress the startup banner |
+| Auto-Reload | `--reload` / `--no-reload` | Watch for file changes and restart automatically |
+| Reload Dirs | `--reload-dir` | Directories to watch (repeatable) |
+| Skip Env | `--skip-env` | Don't set up a uv environment (use when already in one) |
+| Python | `--python` | Python version to use (e.g., `3.11`) |
+| Extra Packages | `--with` | Additional packages to install (repeatable) |
+| Project | `--project` | Run within a specific uv project directory |
+| Requirements | `--with-requirements` | Install from a requirements file |
+
+### Dependency Management
+
+By default, `fastmcp run` uses your current Python environment directly. When you pass `--python`, `--with`, `--project`, or `--with-requirements`, it switches to running via `uv run` in a subprocess, which handles dependency isolation automatically.
+
+The `--skip-env` flag is useful when you're already inside an activated venv, a Docker container with pre-installed dependencies, or a uv-managed project — it prevents uv from trying to set up another environment layer.
+
+## Previewing Apps
+
+
+
+`fastmcp dev apps` launches a browser-based preview UI for servers with [Prefab App tools](/apps/prefab). It starts your MCP server on one port and a local dev UI on another — giving you a live, interactive picker where you can call app tools and see their rendered output without needing a full MCP host client.
+
+```bash
+fastmcp dev apps server.py
+fastmcp dev apps server.py:mcp --mcp-port 9000 --dev-port 9090
+```
+
+The picker auto-generates a form from each tool's input schema. Submit the form and the result opens in a new tab as a rendered Prefab UI.
+
+Auto-reload is on by default — save a file and the MCP server restarts automatically.
+
+
+`fastmcp dev apps` requires `fastmcp[apps]` — install with `pip install "fastmcp[apps]"`.
+
+
+| Option | Flag | Description |
+| ------ | ---- | ----------- |
+| MCP Port | `--mcp-port` | Port for the MCP server (default: `8000`) |
+| Dev Port | `--dev-port` | Port for the dev UI (default: `8080`) |
+| Auto-Reload | `--reload` / `--no-reload` | Watch for file changes (default: on) |
+
+## Development with the Inspector
+
+`fastmcp dev inspector` launches your server inside the [MCP Inspector](https://github.com/modelcontextprotocol/inspector), a browser-based tool for interactively testing MCP servers. Auto-reload is on by default, so your server restarts when you save changes.
+
+```bash
+fastmcp dev inspector server.py
+fastmcp dev inspector server.py -e . --with pandas
+```
+
+
+The Inspector always runs your server via `uv run` in a subprocess — it never uses your local environment directly. Specify dependencies with `--with`, `--with-editable`, `--with-requirements`, or through a `fastmcp.json` file.
+
+
+
+The Inspector connects over **stdio only**. When it launches, you may need to select "STDIO" from the transport dropdown and click connect. To test a server over HTTP, start it separately with `fastmcp run server.py --transport http` and point the Inspector at the URL.
+
+
+| Option | Flag | Description |
+| ------ | ---- | ----------- |
+| Editable Package | `--with-editable`, `-e` | Install a directory in editable mode |
+| Extra Packages | `--with` | Additional packages (repeatable) |
+| Inspector Version | `--inspector-version` | MCP Inspector version to use |
+| UI Port | `--ui-port` | Port for the Inspector UI |
+| Server Port | `--server-port` | Port for the Inspector proxy |
+| Auto-Reload | `--reload` / `--no-reload` | File watching (default: on) |
+| Reload Dirs | `--reload-dir` | Directories to watch (repeatable) |
+| Python | `--python` | Python version |
+| Project | `--project` | Run within a uv project directory |
+| Requirements | `--with-requirements` | Install from a requirements file |
+
+## Pre-Building Environments
+
+`fastmcp project prepare` creates a persistent uv project from a `fastmcp.json` file, pre-installing all dependencies. This separates environment setup from server execution — install once, run many times.
+
+```bash
+# Step 1: Build the environment (slow, does dependency resolution)
+fastmcp project prepare fastmcp.json --output-dir ./env
+
+# Step 2: Run using the prepared environment (fast, no install step)
+fastmcp run fastmcp.json --project ./env
+```
+
+The prepared directory contains a `pyproject.toml`, a `.venv` with all packages installed, and a `uv.lock` for reproducibility. This is particularly useful in deployment scenarios where you want deterministic, pre-built environments.
diff --git a/docs/v3/clients/auth/bearer.mdx b/docs/v3/clients/auth/bearer.mdx
new file mode 100644
index 000000000..2e12fbc13
--- /dev/null
+++ b/docs/v3/clients/auth/bearer.mdx
@@ -0,0 +1,88 @@
+---
+title: Bearer Token Authentication
+sidebarTitle: Bearer Auth
+description: Authenticate your FastMCP client with a Bearer token.
+icon: key
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+
+Bearer Token authentication is only relevant for HTTP-based transports.
+
+
+You can configure your FastMCP client to use **bearer authentication** by supplying a valid access token. This is most appropriate for service accounts, long-lived API keys, CI/CD, applications where authentication is managed separately, or other non-interactive authentication methods.
+
+A Bearer token is a JSON Web Token (JWT) that is used to authenticate a request. It is most commonly used in the `Authorization` header of an HTTP request, using the `Bearer` scheme:
+
+```http
+Authorization: Bearer
+```
+
+
+## Client Usage
+
+The most straightforward way to use a pre-existing Bearer token is to provide it as a string to the `auth` parameter of the `fastmcp.Client` or transport instance. FastMCP will automatically format it correctly for the `Authorization` header and bearer scheme.
+
+
+If you're using a string token, do not include the `Bearer` prefix. FastMCP will add it for you.
+
+
+```python {5}
+from fastmcp import Client
+
+async with Client(
+ "https://your-server.fastmcp.app/mcp",
+ auth="",
+) as client:
+ await client.ping()
+```
+
+You can also supply a Bearer token to a transport instance, such as `StreamableHttpTransport` or `SSETransport`:
+
+```python {6}
+from fastmcp import Client
+from fastmcp.client.transports import StreamableHttpTransport
+
+transport = StreamableHttpTransport(
+ "http://your-server.fastmcp.app/mcp",
+ auth="",
+)
+
+async with Client(transport) as client:
+ await client.ping()
+```
+
+## `BearerAuth` Helper
+
+If you prefer to be more explicit and not rely on FastMCP to transform your string token, you can use the `BearerAuth` class yourself, which implements the `httpx.Auth` interface.
+
+```python {6}
+from fastmcp import Client
+from fastmcp.client.auth import BearerAuth
+
+async with Client(
+ "https://your-server.fastmcp.app/mcp",
+ auth=BearerAuth(token=""),
+) as client:
+ await client.ping()
+```
+
+## Custom Headers
+
+If the MCP server expects a custom header or token scheme, you can manually set the client's `headers` instead of using the `auth` parameter by setting them on your transport:
+
+```python {5}
+from fastmcp import Client
+from fastmcp.client.transports import StreamableHttpTransport
+
+async with Client(
+ transport=StreamableHttpTransport(
+ "https://your-server.fastmcp.app/mcp",
+ headers={"X-API-Key": ""},
+ ),
+) as client:
+ await client.ping()
+```
diff --git a/docs/v3/clients/auth/cimd.mdx b/docs/v3/clients/auth/cimd.mdx
new file mode 100644
index 000000000..c1f92d1c4
--- /dev/null
+++ b/docs/v3/clients/auth/cimd.mdx
@@ -0,0 +1,138 @@
+---
+title: CIMD Authentication
+sidebarTitle: CIMD
+description: Use Client ID Metadata Documents for verifiable, domain-based client identity.
+icon: id-badge
+tag: NEW
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+
+CIMD authentication is only relevant for HTTP-based transports and requires a server that advertises CIMD support.
+
+
+With standard OAuth, your client registers dynamically with every server it connects to, receiving a fresh `client_id` each time. This works, but the server has no way to verify *who* your client actually is — any client can claim any name during registration.
+
+CIMD (Client ID Metadata Documents) flips this around. You host a small JSON document at an HTTPS URL you control, and that URL becomes your `client_id`. When your client connects to a server, the server fetches your metadata document and can verify your identity through your domain ownership. Users see a verified domain badge in the consent screen instead of an unverified client name.
+
+## Client Usage
+
+Pass your CIMD document URL to the `client_metadata_url` parameter of `OAuth`:
+
+```python
+from fastmcp import Client
+from fastmcp.client.auth import OAuth
+
+async with Client(
+ "https://mcp-server.example.com/mcp",
+ auth=OAuth(
+ client_metadata_url="https://myapp.example.com/oauth/client.json",
+ ),
+) as client:
+ await client.ping()
+```
+
+When the server supports CIMD, the client uses your metadata URL as its `client_id` instead of performing Dynamic Client Registration. The server fetches your document, validates it, and proceeds with the standard OAuth authorization flow.
+
+
+You don't need to pass `mcp_url` when using `OAuth` with `Client(auth=...)` — the transport provides the server URL automatically.
+
+
+## Creating a CIMD Document
+
+A CIMD document is a JSON file that describes your client. The most important field is `client_id`, which must exactly match the URL where you host the document.
+
+Use the FastMCP CLI to generate one:
+
+```bash
+fastmcp auth cimd create \
+ --name "My Application" \
+ --redirect-uri "http://localhost:*/callback" \
+ --client-id "https://myapp.example.com/oauth/client.json"
+```
+
+This produces:
+
+```json
+{
+ "client_id": "https://myapp.example.com/oauth/client.json",
+ "client_name": "My Application",
+ "redirect_uris": ["http://localhost:*/callback"],
+ "token_endpoint_auth_method": "none",
+ "grant_types": ["authorization_code"],
+ "response_types": ["code"]
+}
+```
+
+If you omit `--client-id`, the CLI generates a placeholder value and reminds you to update it before hosting.
+
+### CLI Options
+
+The `create` command accepts these flags:
+
+| Flag | Description |
+|------|-------------|
+| `--name` | Human-readable client name (required) |
+| `--redirect-uri`, `-r` | Allowed redirect URIs — can be specified multiple times (required) |
+| `--client-id` | The URL where you'll host this document (sets `client_id` directly) |
+| `--output`, `-o` | Write to a file instead of stdout |
+| `--scope` | Space-separated list of scopes the client may request |
+| `--client-uri` | URL of the client's home page |
+| `--logo-uri` | URL of the client's logo image |
+| `--no-pretty` | Output compact JSON |
+
+### Redirect URIs
+
+The `redirect_uris` field supports wildcard port matching for localhost. The pattern `http://localhost:*/callback` matches any port, which is useful for development clients that bind to random available ports (which is what FastMCP's `OAuth` helper does by default).
+
+## Hosting Requirements
+
+CIMD documents must be hosted at a publicly accessible HTTPS URL with a non-root path:
+
+- **HTTPS required** — HTTP URLs are rejected for security
+- **Non-root path** — The URL must have a path component (e.g., `/oauth/client.json`, not just `/`)
+- **Public accessibility** — The server must be able to fetch the document over the internet
+- **Matching `client_id`** — The `client_id` field in the document must exactly match the hosting URL
+
+Common hosting options include static file hosting services like GitHub Pages, Cloudflare Pages, Vercel, or S3 — anywhere you can serve a JSON file over HTTPS.
+
+## Validating Your Document
+
+Before deploying, verify your hosted document passes validation:
+
+```bash
+fastmcp auth cimd validate https://myapp.example.com/oauth/client.json
+```
+
+The validator fetches the document and checks that:
+- The URL is valid (HTTPS, non-root path)
+- The document is well-formed JSON conforming to the CIMD schema
+- The `client_id` in the document matches the URL it was fetched from
+
+## How It Works
+
+When your client connects to a CIMD-enabled server, the flow works like this:
+
+
+
+Your client sends its `client_metadata_url` as the `client_id` in the OAuth authorization request.
+
+
+The server sees that the `client_id` is an HTTPS URL with a path — the signature of a CIMD client — and skips Dynamic Client Registration.
+
+
+The server fetches your JSON document from the URL, validates that `client_id` matches the URL, and extracts your client metadata (name, redirect URIs, scopes).
+
+
+The standard OAuth flow continues: browser opens for user consent, authorization code exchange, token issuance. The consent screen shows your verified domain.
+
+
+
+The server caches your CIMD document according to HTTP cache headers, so subsequent requests don't require re-fetching.
+
+## Server Configuration
+
+CIMD is a server-side feature that your MCP server must support. FastMCP's OAuth proxy providers (GitHub, Google, Auth0, etc.) support CIMD by default. See the [OAuth Proxy CIMD documentation](/servers/auth/oauth-proxy#cimd-support) for server-side configuration, including private key JWT authentication and security details.
diff --git a/docs/v3/clients/auth/oauth.mdx b/docs/v3/clients/auth/oauth.mdx
new file mode 100644
index 000000000..84fbe2164
--- /dev/null
+++ b/docs/v3/clients/auth/oauth.mdx
@@ -0,0 +1,186 @@
+---
+title: OAuth Authentication
+sidebarTitle: OAuth
+description: Authenticate your FastMCP client via OAuth 2.1.
+icon: window
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+
+OAuth authentication is only relevant for HTTP-based transports and requires user interaction via a web browser.
+
+
+When your FastMCP client needs to access an MCP server protected by OAuth 2.1, and the process requires user interaction (like logging in and granting consent), you should use the Authorization Code Flow. FastMCP provides the `fastmcp.client.auth.OAuth` helper to simplify this entire process.
+
+This flow is common for user-facing applications where the application acts on behalf of the user.
+
+## Client Usage
+
+
+### Default Configuration
+
+The simplest way to use OAuth is to pass the string `"oauth"` to the `auth` parameter of the `Client` or transport instance. FastMCP will automatically configure the client to use OAuth with default settings:
+
+```python {4}
+from fastmcp import Client
+
+# Uses default OAuth settings
+async with Client("https://your-server.fastmcp.app/mcp", auth="oauth") as client:
+ await client.ping()
+```
+
+
+### `OAuth` Helper
+
+To fully configure the OAuth flow, use the `OAuth` helper and pass it to the `auth` parameter of the `Client` or transport instance. `OAuth` manages the complexities of the OAuth 2.1 Authorization Code Grant with PKCE (Proof Key for Code Exchange) for enhanced security, and implements the full `httpx.Auth` interface.
+
+```python {2, 4, 6}
+from fastmcp import Client
+from fastmcp.client.auth import OAuth
+
+oauth = OAuth(scopes=["user"])
+
+async with Client("https://your-server.fastmcp.app/mcp", auth=oauth) as client:
+ await client.ping()
+```
+
+
+You don't need to pass `mcp_url` when using `OAuth` with `Client(auth=...)` — the transport provides the server URL automatically.
+
+
+#### `OAuth` Parameters
+
+- **`scopes`** (`str | list[str]`, optional): OAuth scopes to request. Can be space-separated string or list of strings
+- **`client_name`** (`str`, optional): Client name for dynamic registration. Defaults to `"FastMCP Client"`
+- **`client_id`** (`str`, optional): Pre-registered OAuth client ID. When provided, skips Dynamic Client Registration entirely. See [Pre-Registered Clients](#pre-registered-clients)
+- **`client_secret`** (`str`, optional): OAuth client secret for pre-registered clients. Optional — public clients that rely on PKCE can omit this
+- **`client_metadata_url`** (`str`, optional): URL-based client identity (CIMD). See [CIMD Authentication](/clients/auth/cimd) for details
+- **`token_storage`** (`AsyncKeyValue`, optional): Storage backend for persisting OAuth tokens. Defaults to in-memory storage (tokens lost on restart). See [Token Storage](#token-storage) for encrypted storage options
+- **`additional_client_metadata`** (`dict[str, Any]`, optional): Extra metadata for client registration
+- **`callback_port`** (`int`, optional): Fixed port for OAuth callback server. If not specified, uses a random available port
+- **`httpx_client_factory`** (`McpHttpClientFactory`, optional): Factory for creating httpx clients
+
+
+## OAuth Flow
+
+The OAuth flow is triggered when you use a FastMCP `Client` configured to use OAuth.
+
+
+
+The client first checks the configured `token_storage` backend for existing, valid tokens for the target server. If one is found, it will be used to authenticate the client.
+
+
+If no valid tokens exist, the client attempts to discover the OAuth server's endpoints using a well-known URI (e.g., `/.well-known/oauth-authorization-server`) based on the `mcp_url`.
+
+
+If a `client_id` is provided, the client uses those pre-registered credentials directly and skips this step entirely. Otherwise, if a `client_metadata_url` is configured and the server supports CIMD, the client uses its metadata URL as its identity. As a fallback, the client performs Dynamic Client Registration (RFC 7591) if the server supports it.
+
+
+A temporary local HTTP server is started on an available port (or the port specified via `callback_port`). This server's address (e.g., `http://127.0.0.1:/callback`) acts as the `redirect_uri` for the OAuth flow.
+
+
+The user's default web browser is automatically opened, directing them to the OAuth server's authorization endpoint. The user logs in and grants (or denies) the requested `scopes`.
+
+
+Upon approval, the OAuth server redirects the user's browser to the local callback server with an `authorization_code`. The client captures this code and exchanges it with the OAuth server's token endpoint for an `access_token` (and often a `refresh_token`) using PKCE for security.
+
+
+The obtained tokens are saved to the configured `token_storage` backend for future use, eliminating the need for repeated browser interactions.
+
+
+The access token is automatically included in the `Authorization` header for requests to the MCP server.
+
+
+If the access token expires, the client will automatically use the refresh token to get a new access token.
+
+
+
+## Token Storage
+
+
+
+By default, tokens are stored in memory and lost when your application restarts. For persistent storage, pass an `AsyncKeyValue`-compatible storage backend to the `token_storage` parameter.
+
+
+**Security Consideration**: Use encrypted storage for production. MCP clients can accumulate OAuth credentials for many servers over time, and a compromised token store could expose access to multiple services.
+
+
+```python
+from fastmcp import Client
+from fastmcp.client.auth import OAuth
+from key_value.aio.stores.disk import DiskStore
+from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
+from cryptography.fernet import Fernet
+import os
+
+# Create encrypted disk storage
+encrypted_storage = FernetEncryptionWrapper(
+ key_value=DiskStore(directory="~/.fastmcp/oauth-tokens"),
+ fernet=Fernet(os.environ["OAUTH_STORAGE_ENCRYPTION_KEY"])
+)
+
+oauth = OAuth(token_storage=encrypted_storage)
+
+async with Client("https://your-server.fastmcp.app/mcp", auth=oauth) as client:
+ await client.ping()
+```
+
+You can use any `AsyncKeyValue`-compatible backend from the [key-value library](https://github.com/strawgate/py-key-value) including Redis, DynamoDB, and more. Wrap your storage in `FernetEncryptionWrapper` for encryption.
+
+
+When selecting a storage backend, review the [py-key-value documentation](https://github.com/strawgate/py-key-value) to understand the maturity level and limitations of your chosen backend. Some backends may be in preview or have constraints that affect production suitability.
+
+
+## CIMD Authentication
+
+
+
+Client ID Metadata Documents (CIMD) provide an alternative to Dynamic Client Registration. Instead of registering with each server, your client hosts a static JSON document at an HTTPS URL. That URL becomes your client's identity, and servers can verify who you are through your domain ownership.
+
+```python
+from fastmcp import Client
+from fastmcp.client.auth import OAuth
+
+async with Client(
+ "https://mcp-server.example.com/mcp",
+ auth=OAuth(
+ client_metadata_url="https://myapp.example.com/oauth/client.json",
+ ),
+) as client:
+ await client.ping()
+```
+
+See the [CIMD Authentication](/clients/auth/cimd) page for complete documentation on creating, hosting, and validating CIMD documents.
+
+## Pre-Registered Clients
+
+
+
+Some OAuth servers don't support Dynamic Client Registration — the MCP spec explicitly makes DCR optional. If your client has been pre-registered with the server (you already have a `client_id` and optionally a `client_secret`), you can provide them directly to skip DCR entirely.
+
+```python
+from fastmcp import Client
+from fastmcp.client.auth import OAuth
+
+async with Client(
+ "https://mcp-server.example.com/mcp",
+ auth=OAuth(
+ client_id="my-registered-client-id",
+ client_secret="my-client-secret",
+ ),
+) as client:
+ await client.ping()
+```
+
+Public clients that rely on PKCE for security can omit `client_secret`:
+
+```python
+oauth = OAuth(client_id="my-public-client-id")
+```
+
+
+When using pre-registered credentials, the client will not attempt Dynamic Client Registration. If the server rejects the credentials, the error is surfaced immediately rather than falling back to DCR.
+
diff --git a/docs/clients/cli.mdx b/docs/v3/clients/cli.mdx
similarity index 100%
rename from docs/clients/cli.mdx
rename to docs/v3/clients/cli.mdx
diff --git a/docs/v3/clients/client-only-package.mdx b/docs/v3/clients/client-only-package.mdx
new file mode 100644
index 000000000..020b2f077
--- /dev/null
+++ b/docs/v3/clients/client-only-package.mdx
@@ -0,0 +1,89 @@
+---
+title: Client-Only Package
+description: Use FastMCP's client without installing the full server framework.
+icon: box
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+FastMCP's full `fastmcp` package includes everything needed to build and run MCP servers, apps, proxies, and clients. If you are only embedding an MCP client in another framework, building your own LLM host, or testing MCP servers, you can install the smaller client-only package instead.
+
+```bash
+pip install "fastmcp-slim[client]"
+```
+
+The client-only package uses the `fastmcp` import namespace:
+
+```python
+from fastmcp import Client
+
+client = Client("https://example.com/mcp")
+```
+
+Use `fastmcp-slim[client]` when your code connects to MCP servers but does not define or run FastMCP servers itself. For example, framework authors can depend on `fastmcp-slim[client]` to provide MCP connectivity without requiring users to install the full FastMCP server stack.
+
+## Supported Usage
+
+Client-only installs support remote and subprocess transports:
+
+```python
+from fastmcp import Client
+
+# Remote MCP server
+http_client = Client("https://example.com/mcp")
+
+# Local MCP server over stdio
+stdio_client = Client("my_server.py")
+```
+
+Single-server MCP configuration works as well:
+
+```python
+from fastmcp import Client
+
+config = {
+ "mcpServers": {
+ "weather": {
+ "url": "https://weather.example.com/mcp"
+ }
+ }
+}
+
+client = Client(config)
+```
+
+Optional sampling handlers are available through the same extras as the full package:
+
+```bash
+pip install "fastmcp-slim[client,openai]"
+pip install "fastmcp-slim[client,anthropic]"
+pip install "fastmcp-slim[client,gemini]"
+```
+
+## When to Use the Full Package
+
+Install `fastmcp` when you need server-side FastMCP features:
+
+```bash
+pip install fastmcp
+```
+
+The full package remains the default for most users and continues to support the existing import style:
+
+```python
+from fastmcp import Client, FastMCP
+
+server = FastMCP("Example")
+client = Client(server)
+```
+
+Use the full package for:
+
+- defining or running FastMCP servers
+- in-memory clients connected directly to `FastMCP` server objects
+- multi-server MCP configurations
+- FastMCP apps, proxies, server auth, middleware, and other server-side features
+
+The `fastmcp-slim` package is intentionally narrower: it is for client-only consumers who want FastMCP's MCP client behavior without depending on the full framework.
diff --git a/docs/v3/clients/client.mdx b/docs/v3/clients/client.mdx
new file mode 100644
index 000000000..fc5ddc263
--- /dev/null
+++ b/docs/v3/clients/client.mdx
@@ -0,0 +1,237 @@
+---
+title: The FastMCP Client
+sidebarTitle: Overview
+description: Programmatic client for interacting with MCP servers through a well-typed, Pythonic interface.
+icon: user-robot
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+The `fastmcp.Client` class provides a programmatic interface for interacting with any MCP server. It handles protocol details and connection management automatically, letting you focus on the operations you want to perform.
+
+The FastMCP Client is designed for deterministic, controlled interactions rather than autonomous behavior, making it ideal for testing MCP servers during development, building deterministic applications that need reliable MCP interactions, and creating the foundation for agentic or LLM-based clients with structured, type-safe operations.
+
+
+This is a programmatic client that requires explicit function calls and provides direct control over all MCP operations. Use it as a building block for higher-level systems.
+
+
+## Creating a Client
+
+You provide a server source and the client automatically infers the appropriate transport mechanism.
+
+```python
+import asyncio
+from fastmcp import Client, FastMCP
+
+# In-memory server (ideal for testing)
+server = FastMCP("TestServer")
+client = Client(server)
+
+# HTTP server
+client = Client("https://example.com/mcp")
+
+# Local Python script
+client = Client("my_mcp_server.py")
+
+async def main():
+ async with client:
+ # Basic server interaction
+ await client.ping()
+
+ # List available operations
+ tools = await client.list_tools()
+ resources = await client.list_resources()
+ prompts = await client.list_prompts()
+
+ # Execute operations
+ result = await client.call_tool("example_tool", {"param": "value"})
+ print(result)
+
+asyncio.run(main())
+```
+
+All client operations require using the `async with` context manager for proper connection lifecycle management.
+
+## Choosing a Transport
+
+The client automatically selects a transport based on what you pass to it, but different transports have different characteristics that matter for your use case.
+
+**In-memory transport** connects directly to a FastMCP server instance within the same Python process. Use this for testing and development where you want to eliminate subprocess and network complexity. The server shares your process's environment and memory space.
+
+```python
+from fastmcp import Client, FastMCP
+
+server = FastMCP("TestServer")
+client = Client(server) # In-memory, no network or subprocess
+```
+
+**STDIO transport** launches a server as a subprocess and communicates through stdin/stdout pipes. This is the standard mechanism used by desktop clients like Claude Desktop. The subprocess runs in an isolated environment, so you must explicitly pass any environment variables the server needs.
+
+```python
+from fastmcp import Client
+
+# Simple inference from file path
+client = Client("my_server.py")
+
+# With explicit environment configuration
+client = Client("my_server.py", env={"API_KEY": "secret"})
+```
+
+**HTTP transport** connects to servers running as web services. Use this for production deployments where the server runs independently and manages its own lifecycle.
+
+```python
+from fastmcp import Client
+
+client = Client("https://api.example.com/mcp")
+```
+
+See [Transports](/clients/transports) for detailed configuration options including authentication headers, session persistence, and multi-server configurations.
+
+## Configuration-Based Clients
+
+
+
+Create clients from MCP configuration dictionaries, which can include multiple servers. While there is no official standard for MCP configuration format, FastMCP follows established conventions used by tools like Claude Desktop.
+
+```python
+config = {
+ "mcpServers": {
+ "weather": {
+ "url": "https://weather-api.example.com/mcp"
+ },
+ "assistant": {
+ "command": "python",
+ "args": ["./assistant_server.py"]
+ }
+ }
+}
+
+client = Client(config)
+
+async with client:
+ # Tools are prefixed with server names
+ weather_data = await client.call_tool("weather_get_forecast", {"city": "London"})
+ response = await client.call_tool("assistant_answer_question", {"question": "What's the capital of France?"})
+
+ # Resources use prefixed URIs
+ icons = await client.read_resource("weather://weather/icons/sunny")
+```
+
+## Connection Lifecycle
+
+The client uses context managers for connection management. When you enter the context, the client establishes a connection and performs an MCP initialization handshake with the server. This handshake exchanges capabilities, server metadata, and instructions.
+
+```python
+from fastmcp import Client, FastMCP
+
+mcp = FastMCP(name="MyServer", instructions="Use the greet tool to say hello!")
+
+@mcp.tool
+def greet(name: str) -> str:
+ """Greet a user by name."""
+ return f"Hello, {name}!"
+
+async with Client(mcp) as client:
+ # Initialization already happened automatically
+ print(f"Server: {client.initialize_result.serverInfo.name}")
+ print(f"Instructions: {client.initialize_result.instructions}")
+ print(f"Capabilities: {client.initialize_result.capabilities.tools}")
+```
+
+For advanced scenarios where you need precise control over when initialization happens, disable automatic initialization and call `initialize()` manually:
+
+```python
+from fastmcp import Client
+
+client = Client("my_mcp_server.py", auto_initialize=False)
+
+async with client:
+ # Connection established, but not initialized yet
+ print(f"Connected: {client.is_connected()}")
+ print(f"Initialized: {client.initialize_result is not None}") # False
+
+ # Initialize manually with custom timeout
+ result = await client.initialize(timeout=10.0)
+ print(f"Server: {result.serverInfo.name}")
+
+ # Now ready for operations
+ tools = await client.list_tools()
+```
+
+## Operations
+
+FastMCP clients interact with three types of server components.
+
+**Tools** are server-side functions that the client can execute with arguments. Call them with `call_tool()` and receive structured results.
+
+```python
+async with client:
+ tools = await client.list_tools()
+ result = await client.call_tool("multiply", {"a": 5, "b": 3})
+ print(result.data) # 15
+```
+
+See [Tools](/clients/tools) for detailed documentation including version selection, error handling, and structured output.
+
+**Resources** are data sources that the client can read, either static or templated. Access them with `read_resource()` using URIs.
+
+```python
+async with client:
+ resources = await client.list_resources()
+ content = await client.read_resource("file:///config/settings.json")
+ print(content[0].text)
+```
+
+See [Resources](/clients/resources) for detailed documentation including templates and binary content.
+
+**Prompts** are reusable message templates that can accept arguments. Retrieve rendered prompts with `get_prompt()`.
+
+```python
+async with client:
+ prompts = await client.list_prompts()
+ messages = await client.get_prompt("analyze_data", {"data": [1, 2, 3]})
+ print(messages.messages)
+```
+
+See [Prompts](/clients/prompts) for detailed documentation including argument serialization.
+
+## Callback Handlers
+
+The client supports callback handlers for advanced server interactions. These let you respond to server-initiated requests and receive notifications.
+
+```python
+from fastmcp import Client
+from fastmcp.client.logging import LogMessage
+
+async def log_handler(message: LogMessage):
+ print(f"Server log: {message.data}")
+
+async def progress_handler(progress: float, total: float | None, message: str | None):
+ print(f"Progress: {progress}/{total} - {message}")
+
+async def sampling_handler(messages, params, context):
+ # Integrate with your LLM service here
+ return "Generated response"
+
+client = Client(
+ "my_mcp_server.py",
+ log_handler=log_handler,
+ progress_handler=progress_handler,
+ sampling_handler=sampling_handler,
+ timeout=30.0
+)
+```
+
+Each handler type has its own documentation:
+
+- **[Sampling](/clients/sampling)** - Respond to server LLM requests
+- **[Elicitation](/clients/elicitation)** - Handle server requests for user input
+- **[Progress](/clients/progress)** - Monitor long-running operations
+- **[Logging](/clients/logging)** - Handle server log messages
+- **[Roots](/clients/roots)** - Provide local context to servers
+
+
+The FastMCP Client is designed as a foundational tool. Use it directly for deterministic operations, or build higher-level agentic systems on top of its reliable, type-safe interface.
+
diff --git a/docs/v3/clients/elicitation.mdx b/docs/v3/clients/elicitation.mdx
new file mode 100644
index 000000000..33adbb6d6
--- /dev/null
+++ b/docs/v3/clients/elicitation.mdx
@@ -0,0 +1,138 @@
+---
+title: User Elicitation
+sidebarTitle: Elicitation
+description: Handle server requests for structured user input.
+icon: message-question
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx";
+
+
+
+Use this when you need to respond to server requests for user input during tool execution.
+
+Elicitation allows MCP servers to request structured input from users during operations. Instead of requiring all inputs upfront, servers can interactively ask for missing parameters, request clarification, or gather additional context.
+
+## Handler Template
+
+```python
+from fastmcp import Client
+from fastmcp.client.elicitation import ElicitResult, ElicitRequestParams, RequestContext
+
+async def elicitation_handler(
+ message: str,
+ response_type: type | None,
+ params: ElicitRequestParams,
+ context: RequestContext
+) -> ElicitResult | object:
+ """
+ Handle server requests for user input.
+
+ Args:
+ message: The prompt to display to the user
+ response_type: Python dataclass type for the response (None if no data expected)
+ params: Original MCP elicitation parameters including raw JSON schema
+ context: Request context with metadata
+
+ Returns:
+ - Data directly (implicitly accepts the elicitation)
+ - ElicitResult for explicit control over the action
+ """
+ # Present the message and collect input
+ user_input = input(f"{message}: ")
+
+ if not user_input:
+ return ElicitResult(action="decline")
+
+ # Create response using the provided dataclass type
+ return response_type(value=user_input)
+
+client = Client(
+ "my_mcp_server.py",
+ elicitation_handler=elicitation_handler,
+)
+```
+
+## How It Works
+
+When a server needs user input, it sends an elicitation request with a message prompt and a JSON schema describing the expected response structure. FastMCP automatically converts this schema into a Python dataclass type, making it easy to construct properly typed responses without manually parsing JSON schemas.
+
+The handler receives four parameters:
+
+
+
+ The prompt message to display to the user
+
+
+
+ A Python dataclass type that FastMCP created from the server's JSON schema. Use this to construct your response with proper typing. If the server requests an empty object, this will be `None`.
+
+
+
+ The original MCP elicitation parameters, including the raw JSON schema in `params.requestedSchema`
+
+
+
+ Request context containing metadata about the elicitation request
+
+
+
+## Response Actions
+
+You can return data directly, which implicitly accepts the elicitation:
+
+```python
+async def elicitation_handler(message, response_type, params, context):
+ user_input = input(f"{message}: ")
+ return response_type(value=user_input) # Implicit accept
+```
+
+Or return an `ElicitResult` for explicit control over the action:
+
+```python
+from fastmcp.client.elicitation import ElicitResult
+
+async def elicitation_handler(message, response_type, params, context):
+ user_input = input(f"{message}: ")
+
+ if not user_input:
+ return ElicitResult(action="decline") # User declined
+
+ if user_input == "cancel":
+ return ElicitResult(action="cancel") # Cancel entire operation
+
+ return ElicitResult(
+ action="accept",
+ content=response_type(value=user_input)
+ )
+```
+
+**Action types:**
+- **`accept`**: User provided valid input. Include the data in the `content` field.
+- **`decline`**: User chose not to provide the requested information. Omit `content`.
+- **`cancel`**: User cancelled the entire operation. Omit `content`.
+
+## Example
+
+A file management tool might ask which directory to create:
+
+```python
+from fastmcp import Client
+from fastmcp.client.elicitation import ElicitResult
+
+async def elicitation_handler(message, response_type, params, context):
+ print(f"Server asks: {message}")
+
+ user_response = input("Your response: ")
+
+ if not user_response:
+ return ElicitResult(action="decline")
+
+ # Use the response_type dataclass to create a properly structured response
+ return response_type(value=user_response)
+
+client = Client(
+ "my_mcp_server.py",
+ elicitation_handler=elicitation_handler
+)
+```
diff --git a/docs/v3/clients/fastmcp-remote.mdx b/docs/v3/clients/fastmcp-remote.mdx
new file mode 100644
index 000000000..ee218afe0
--- /dev/null
+++ b/docs/v3/clients/fastmcp-remote.mdx
@@ -0,0 +1,169 @@
+---
+title: fastmcp-remote
+description: Bridge remote MCP servers into stdio-only MCP hosts with uvx fastmcp-remote.
+icon: bridge
+---
+
+`fastmcp-remote` is FastMCP's standalone stdio bridge for remote MCP servers. Use it when an MCP host expects to launch a local command, but the server you want to use is hosted over Streamable HTTP or SSE.
+
+```json
+{
+ "mcpServers": {
+ "linear": {
+ "command": "uvx",
+ "args": ["fastmcp-remote", "https://mcp.linear.app/mcp"]
+ }
+ }
+}
+```
+
+The package is powered by FastMCP. It builds one FastMCP client for the remote URL, exposes that client as a local stdio proxy, and keeps the executable focused on that bridge. For running Python server files, local project environments, FastMCP config files, and development reload loops, use [`fastmcp run`](/cli/running).
+
+The command shape follows the original [`mcp-remote`](https://github.com/geelen/mcp-remote) npm project, which established this stdio-to-remote bridge pattern for MCP hosts.
+
+## Installation
+
+Most MCP hosts can run `fastmcp-remote` directly through `uvx`, so you usually do not need to install it yourself:
+
+```bash
+uvx fastmcp-remote https://example.com/mcp
+```
+
+If your host requires an already-installed command, install the package with your Python package manager:
+
+```bash
+uv tool install fastmcp-remote
+```
+
+## Host Configuration
+
+For hosts that use `mcpServers` JSON configuration, set the command to `uvx` and pass `fastmcp-remote` plus the remote server URL as arguments:
+
+```json
+{
+ "mcpServers": {
+ "remote-api": {
+ "command": "uvx",
+ "args": ["fastmcp-remote", "https://example.com/mcp"]
+ }
+ }
+}
+```
+
+## Endpoint URLs and Connection Status
+
+Pass the full MCP endpoint URL for the remote server. Many FastMCP HTTP servers expose MCP at `/mcp`, so a local development server may need `http://localhost:8000/mcp` rather than `http://localhost:8000`.
+
+`fastmcp-remote` starts a local stdio bridge, then connects to the upstream server when the MCP host initializes that bridge. If the upstream server is unavailable, the URL does not point to an MCP endpoint, or authentication cannot complete, initialization fails and the host should report the remote server as failed. After initialization succeeds, later tool, resource, prompt, and ping requests continue to proxy through the same remote server configuration.
+
+OAuth is enabled automatically for HTTPS servers. The first connection opens the browser-based OAuth flow when the server requires authentication, then stores tokens locally for future runs.
+
+To pass a bearer token or another custom header directly, provide `--header` in `Name: Value` form. The header name ends at the first colon, so values can contain additional colons. Quote the header when the value contains spaces, just like any other shell argument. An `Authorization` header disables OAuth by default:
+
+```json
+{
+ "mcpServers": {
+ "private-api": {
+ "command": "uvx",
+ "args": [
+ "fastmcp-remote",
+ "https://example.com/mcp",
+ "--header",
+ "Authorization: Bearer "
+ ]
+ }
+ }
+}
+```
+
+Repeat `--header` to send multiple headers:
+
+```bash
+uvx fastmcp-remote https://example.com/mcp \
+ --header "Authorization: Bearer " \
+ --header "X-Workspace: production" \
+ --header "X-Client-Name: My MCP Host" \
+ --header "X-Callback-Url: https://example.com/oauth/callback"
+```
+
+Some MCP hosts on Windows have trouble preserving spaces inside command arguments. Put the spaced value in an environment variable and reference it from the header value:
+
+```json
+{
+ "mcpServers": {
+ "remote-api": {
+ "command": "uvx",
+ "args": [
+ "fastmcp-remote",
+ "https://example.com/mcp",
+ "--header",
+ "Authorization:${AUTH_HEADER}"
+ ],
+ "env": {
+ "AUTH_HEADER": "Bearer "
+ }
+ }
+ }
+}
+```
+
+For local development servers over plain HTTP, disable OAuth when the server is unauthenticated:
+
+```bash
+uvx fastmcp-remote http://localhost:8000/mcp --auth none
+```
+
+## Self-Signed Certificates
+
+For servers behind a self-signed certificate, point `--verify` at a CA bundle that trusts the certificate:
+
+```bash
+uvx fastmcp-remote https://internal.example.com/mcp --verify /path/to/ca-bundle.pem
+```
+
+To disable certificate verification entirely, pass `--verify false`. This is insecure and should only be used for trusted servers on private networks:
+
+```bash
+uvx fastmcp-remote https://internal.example.com/mcp --verify false
+```
+
+To trust a CA bundle without a flag, set the standard `SSL_CERT_FILE` environment variable, which OpenSSL reads automatically:
+
+```bash
+SSL_CERT_FILE=/path/to/ca-bundle.pem uvx fastmcp-remote https://internal.example.com/mcp
+```
+
+## OAuth Storage
+
+OAuth tokens are stored under `~/.fastmcp/remote` by default. Set `FASTMCP_REMOTE_CONFIG_DIR` to use another directory:
+
+```bash
+FASTMCP_REMOTE_CONFIG_DIR=~/.config/fastmcp-remote uvx fastmcp-remote https://example.com/mcp
+```
+
+Use `--resource` to isolate tokens for a particular remote server identity:
+
+```bash
+uvx fastmcp-remote https://example.com/mcp --resource example-prod
+```
+
+If the remote authorization server requires a fixed callback port or hostname, pass them after the URL:
+
+```bash
+uvx fastmcp-remote https://example.com/mcp 3334 --host 127.0.0.1
+```
+
+## Options
+
+| Option | Description |
+| ------ | ----------- |
+| `--transport` | Choose `http` or `sse`. Defaults to `http`. |
+| `--header` | Add a header to upstream requests, for example `--header "Authorization: Bearer "`. Values may contain colons. Quote headers whose values contain spaces. Use `${VAR}` to expand environment variables inside values. Repeat for multiple headers. |
+| `--auth` | Choose `oauth` or `none`. The default uses OAuth unless an `Authorization` header is provided. |
+| `--verify` | Control TLS certificate verification. Pass a path to a CA bundle to trust a self-signed certificate, or `false` to disable verification (insecure). Defaults to verification enabled. |
+| `--resource` | Isolate OAuth token storage for a named remote resource. |
+| `--host` | Set the OAuth callback hostname. Defaults to `localhost`. |
+| `--auth-timeout` | Set how long to wait for the OAuth callback. Defaults to 300 seconds. |
+| `--ignore-tool` | Hide tools whose names match a glob pattern. Repeat for multiple patterns. |
+| `--debug` | Enable debug logging. |
+| `--silent` | Suppress non-critical logs. |
diff --git a/docs/clients/generate-cli.mdx b/docs/v3/clients/generate-cli.mdx
similarity index 100%
rename from docs/clients/generate-cli.mdx
rename to docs/v3/clients/generate-cli.mdx
diff --git a/docs/v3/clients/logging.mdx b/docs/v3/clients/logging.mdx
new file mode 100644
index 000000000..eea9ff322
--- /dev/null
+++ b/docs/v3/clients/logging.mdx
@@ -0,0 +1,92 @@
+---
+title: Server Logging
+sidebarTitle: Logging
+description: Receive and handle log messages from MCP servers.
+icon: receipt
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Use this when you need to capture or process log messages sent by the server.
+
+MCP servers can emit log messages to clients. The client handles these through a log handler callback.
+
+## Log Handler
+
+Provide a `log_handler` function when creating the client:
+
+```python
+import logging
+from fastmcp import Client
+from fastmcp.client.logging import LogMessage
+
+logging.basicConfig(
+ level=logging.INFO,
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
+)
+
+logger = logging.getLogger(__name__)
+LOGGING_LEVEL_MAP = logging.getLevelNamesMapping()
+
+async def log_handler(message: LogMessage):
+ """Forward MCP server logs to Python's logging system."""
+ msg = message.data.get('msg')
+ extra = message.data.get('extra')
+
+ level = LOGGING_LEVEL_MAP.get(message.level.upper(), logging.INFO)
+ logger.log(level, msg, extra=extra)
+
+client = Client(
+ "my_mcp_server.py",
+ log_handler=log_handler,
+)
+```
+
+The handler receives a `LogMessage` object:
+
+
+
+ The log level
+
+
+
+ The logger name (may be None)
+
+
+
+ The log payload, containing `msg` and `extra` keys
+
+
+
+## Structured Logs
+
+The `message.data` attribute is a dictionary containing the log payload. This enables structured logging with rich contextual information.
+
+```python
+async def detailed_log_handler(message: LogMessage):
+ msg = message.data.get('msg')
+ extra = message.data.get('extra')
+
+ if message.level == "error":
+ print(f"ERROR: {msg} | Details: {extra}")
+ elif message.level == "warning":
+ print(f"WARNING: {msg} | Details: {extra}")
+ else:
+ print(f"{message.level.upper()}: {msg}")
+```
+
+This structure is preserved even when logs are forwarded through a FastMCP proxy, making it useful for debugging multi-server applications.
+
+## Default Behavior
+
+If you do not provide a custom `log_handler`, FastMCP's default handler routes server logs to Python's logging system at the appropriate severity level. The MCP levels map as follows: `notice` becomes INFO; `alert` and `emergency` become CRITICAL.
+
+```python
+client = Client("my_mcp_server.py")
+
+async with client:
+ # Server logs are forwarded at proper severity automatically
+ await client.call_tool("some_tool")
+```
diff --git a/docs/v3/clients/notifications.mdx b/docs/v3/clients/notifications.mdx
new file mode 100644
index 000000000..5e1b447aa
--- /dev/null
+++ b/docs/v3/clients/notifications.mdx
@@ -0,0 +1,155 @@
+---
+title: Notifications
+sidebarTitle: Notifications
+description: Handle server-sent notifications for list changes and other events.
+icon: envelope
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx";
+
+
+
+Use this when you need to react to server-side changes like tool list updates or resource modifications.
+
+MCP servers can send notifications to inform clients about state changes. The message handler provides a unified way to process these notifications.
+
+## Handling Notifications
+
+The simplest approach is a function that receives all messages and filters for the notifications you care about:
+
+```python
+from fastmcp import Client
+
+async def message_handler(message):
+ """Handle MCP notifications from the server."""
+ if hasattr(message, 'root'):
+ method = message.root.method
+
+ if method == "notifications/tools/list_changed":
+ print("Tools have changed - refresh tool cache")
+ elif method == "notifications/resources/list_changed":
+ print("Resources have changed")
+ elif method == "notifications/prompts/list_changed":
+ print("Prompts have changed")
+
+client = Client(
+ "my_mcp_server.py",
+ message_handler=message_handler,
+)
+```
+
+## MessageHandler Class
+
+For fine-grained targeting, subclass `MessageHandler` to use specific hooks:
+
+```python
+from fastmcp import Client
+from fastmcp.client.messages import MessageHandler
+import mcp.types
+
+class MyMessageHandler(MessageHandler):
+ async def on_tool_list_changed(
+ self, notification: mcp.types.ToolListChangedNotification
+ ) -> None:
+ """Handle tool list changes."""
+ print("Tool list changed - refreshing available tools")
+
+ async def on_resource_list_changed(
+ self, notification: mcp.types.ResourceListChangedNotification
+ ) -> None:
+ """Handle resource list changes."""
+ print("Resource list changed")
+
+ async def on_prompt_list_changed(
+ self, notification: mcp.types.PromptListChangedNotification
+ ) -> None:
+ """Handle prompt list changes."""
+ print("Prompt list changed")
+
+client = Client(
+ "my_mcp_server.py",
+ message_handler=MyMessageHandler(),
+)
+```
+
+### Handler Template
+
+```python
+from fastmcp.client.messages import MessageHandler
+import mcp.types
+
+class MyMessageHandler(MessageHandler):
+ async def on_message(self, message) -> None:
+ """Called for ALL messages (requests and notifications)."""
+ pass
+
+ async def on_notification(
+ self, notification: mcp.types.ServerNotification
+ ) -> None:
+ """Called for notifications (fire-and-forget)."""
+ pass
+
+ async def on_tool_list_changed(
+ self, notification: mcp.types.ToolListChangedNotification
+ ) -> None:
+ """Called when the server's tool list changes."""
+ pass
+
+ async def on_resource_list_changed(
+ self, notification: mcp.types.ResourceListChangedNotification
+ ) -> None:
+ """Called when the server's resource list changes."""
+ pass
+
+ async def on_prompt_list_changed(
+ self, notification: mcp.types.PromptListChangedNotification
+ ) -> None:
+ """Called when the server's prompt list changes."""
+ pass
+
+ async def on_progress(
+ self, notification: mcp.types.ProgressNotification
+ ) -> None:
+ """Called for progress updates during long-running operations."""
+ pass
+
+ async def on_logging_message(
+ self, notification: mcp.types.LoggingMessageNotification
+ ) -> None:
+ """Called for log messages from the server."""
+ pass
+```
+
+## List Change Notifications
+
+A practical example of maintaining a tool cache that refreshes when tools change:
+
+```python
+from fastmcp import Client
+from fastmcp.client.messages import MessageHandler
+import mcp.types
+
+class ToolCacheHandler(MessageHandler):
+ def __init__(self):
+ self.cached_tools = []
+
+ async def on_tool_list_changed(
+ self, notification: mcp.types.ToolListChangedNotification
+ ) -> None:
+ """Clear tool cache when tools change."""
+ print("Tools changed - clearing cache")
+ self.cached_tools = [] # Force refresh on next access
+
+client = Client("server.py", message_handler=ToolCacheHandler())
+```
+
+## Server Requests
+
+While the message handler receives server-initiated requests, you should use dedicated callback parameters for most interactive scenarios:
+
+- **Sampling requests**: Use [`sampling_handler`](/clients/sampling)
+- **Elicitation requests**: Use [`elicitation_handler`](/clients/elicitation)
+- **Progress updates**: Use [`progress_handler`](/clients/progress)
+- **Log messages**: Use [`log_handler`](/clients/logging)
+
+The message handler is primarily for monitoring and handling notifications rather than responding to requests.
diff --git a/docs/v3/clients/progress.mdx b/docs/v3/clients/progress.mdx
new file mode 100644
index 000000000..707ab8dac
--- /dev/null
+++ b/docs/v3/clients/progress.mdx
@@ -0,0 +1,67 @@
+---
+title: Progress Monitoring
+sidebarTitle: Progress
+description: Handle progress notifications from long-running server operations.
+icon: bars-progress
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Use this when you need to track progress of long-running operations.
+
+MCP servers can report progress during operations. The client receives these updates through a progress handler.
+
+## Progress Handler
+
+Set a handler when creating the client:
+
+```python
+from fastmcp import Client
+
+async def progress_handler(
+ progress: float,
+ total: float | None,
+ message: str | None
+) -> None:
+ if total is not None:
+ percentage = (progress / total) * 100
+ print(f"Progress: {percentage:.1f}% - {message or ''}")
+ else:
+ print(f"Progress: {progress} - {message or ''}")
+
+client = Client(
+ "my_mcp_server.py",
+ progress_handler=progress_handler
+)
+```
+
+The handler receives three parameters:
+
+
+
+ Current progress value
+
+
+
+ Expected total value (may be None if unknown)
+
+
+
+ Optional status message
+
+
+
+## Per-Call Handler
+
+Override the client-level handler for specific tool calls:
+
+```python
+async with client:
+ result = await client.call_tool(
+ "long_running_task",
+ {"param": "value"},
+ progress_handler=my_progress_handler
+ )
+```
diff --git a/docs/v3/clients/prompts.mdx b/docs/v3/clients/prompts.mdx
new file mode 100644
index 000000000..bb50d475f
--- /dev/null
+++ b/docs/v3/clients/prompts.mdx
@@ -0,0 +1,147 @@
+---
+title: Getting Prompts
+sidebarTitle: Prompts
+description: Retrieve rendered message templates with automatic argument serialization.
+icon: message-lines
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Use this when you need to retrieve server-defined message templates for LLM interactions.
+
+Prompts are reusable message templates exposed by MCP servers. They can accept arguments to generate personalized message sequences for LLM interactions.
+
+## Basic Usage
+
+Request a rendered prompt with `get_prompt()`:
+
+```python
+async with client:
+ # Simple prompt without arguments
+ result = await client.get_prompt("welcome_message")
+ # result -> mcp.types.GetPromptResult
+
+ # Access the generated messages
+ for message in result.messages:
+ print(f"Role: {message.role}")
+ print(f"Content: {message.content}")
+```
+
+Pass arguments to customize the prompt:
+
+```python
+async with client:
+ result = await client.get_prompt("user_greeting", {
+ "name": "Alice",
+ "role": "administrator"
+ })
+
+ for message in result.messages:
+ print(f"Generated message: {message.content}")
+```
+
+## Argument Serialization
+
+
+
+FastMCP automatically serializes complex arguments to JSON strings as required by the MCP specification. You can pass typed objects directly:
+
+```python
+from dataclasses import dataclass
+
+@dataclass
+class UserData:
+ name: str
+ age: int
+
+async with client:
+ result = await client.get_prompt("analyze_user", {
+ "user": UserData(name="Alice", age=30), # Automatically serialized
+ "preferences": {"theme": "dark"}, # Dict serialized
+ "scores": [85, 92, 78], # List serialized
+ "simple_name": "Bob" # Strings unchanged
+ })
+```
+
+The client handles serialization using `pydantic_core.to_json()` for consistent formatting. FastMCP servers automatically deserialize these JSON strings back to the expected types.
+
+## Working with Results
+
+The `get_prompt()` method returns a `GetPromptResult` containing a list of messages:
+
+```python
+async with client:
+ result = await client.get_prompt("conversation_starter", {"topic": "climate"})
+
+ for i, message in enumerate(result.messages):
+ print(f"Message {i + 1}:")
+ print(f" Role: {message.role}")
+ print(f" Content: {message.content.text if hasattr(message.content, 'text') else message.content}")
+```
+
+Prompts can generate different message types. System messages configure LLM behavior:
+
+```python
+async with client:
+ result = await client.get_prompt("system_configuration", {
+ "role": "helpful assistant",
+ "expertise": "python programming"
+ })
+
+ # Access the returned messages
+ message = result.messages[0]
+ print(f"Prompt: {message.content}")
+```
+
+Conversation templates generate multi-turn flows:
+
+```python
+async with client:
+ result = await client.get_prompt("interview_template", {
+ "candidate_name": "Alice",
+ "position": "Senior Developer"
+ })
+
+ # Multiple messages for a conversation flow
+ for message in result.messages:
+ print(f"{message.role}: {message.content}")
+```
+
+## Version Selection
+
+
+
+When a server exposes multiple versions of a prompt, you can request a specific version:
+
+```python
+async with client:
+ # Get the highest version (default)
+ result = await client.get_prompt("summarize", {"text": "..."})
+
+ # Get a specific version
+ result_v1 = await client.get_prompt("summarize", {"text": "..."}, version="1.0")
+```
+
+See [Metadata](/servers/versioning#version-discovery) for how to discover available versions.
+
+## Multi-Server Clients
+
+When using multi-server clients, prompts are accessible directly without prefixing:
+
+```python
+async with client: # Multi-server client
+ result1 = await client.get_prompt("weather_prompt", {"city": "London"})
+ result2 = await client.get_prompt("assistant_prompt", {"query": "help"})
+```
+
+## Raw Protocol Access
+
+For complete control, use `get_prompt_mcp()` which returns the full MCP protocol object:
+
+```python
+async with client:
+ result = await client.get_prompt_mcp("example_prompt", {"arg": "value"})
+ # result -> mcp.types.GetPromptResult
+```
diff --git a/docs/v3/clients/resources.mdx b/docs/v3/clients/resources.mdx
new file mode 100644
index 000000000..a3e9300da
--- /dev/null
+++ b/docs/v3/clients/resources.mdx
@@ -0,0 +1,110 @@
+---
+title: Reading Resources
+sidebarTitle: Resources
+description: Access static and templated data sources from MCP servers.
+icon: folder-open
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Use this when you need to read data from server-exposed resources like configuration files, generated content, or external data sources.
+
+Resources are data sources exposed by MCP servers. They can be static files with fixed content, or dynamic templates that generate content based on parameters in the URI.
+
+## Reading Resources
+
+Read a resource using its URI:
+
+```python
+async with client:
+ content = await client.read_resource("file:///path/to/README.md")
+ # content -> list[TextResourceContents | BlobResourceContents]
+
+ # Access text content
+ if hasattr(content[0], 'text'):
+ print(content[0].text)
+
+ # Access binary content
+ if hasattr(content[0], 'blob'):
+ print(f"Binary data: {len(content[0].blob)} bytes")
+```
+
+Resource templates generate content based on URI parameters. The template defines a pattern like `weather://{{city}}/current`, and you fill in the parameters when reading:
+
+```python
+async with client:
+ # Read from a resource template
+ weather_content = await client.read_resource("weather://london/current")
+ print(weather_content[0].text)
+```
+
+## Content Types
+
+Resources return different content types depending on what they expose.
+
+Text resources include configuration files, JSON data, and other human-readable content:
+
+```python
+async with client:
+ content = await client.read_resource("resource://config/settings.json")
+
+ for item in content:
+ if hasattr(item, 'text'):
+ print(f"Text content: {item.text}")
+ print(f"MIME type: {item.mimeType}")
+```
+
+Binary resources include images, PDFs, and other non-text data:
+
+```python
+async with client:
+ content = await client.read_resource("resource://images/logo.png")
+
+ for item in content:
+ if hasattr(item, 'blob'):
+ print(f"Binary content: {len(item.blob)} bytes")
+ print(f"MIME type: {item.mimeType}")
+
+ # Save to file
+ with open("downloaded_logo.png", "wb") as f:
+ f.write(item.blob)
+```
+
+## Multi-Server Clients
+
+When using multi-server clients, resource URIs are prefixed with the server name:
+
+```python
+async with client: # Multi-server client
+ weather_icons = await client.read_resource("weather://weather/icons/sunny")
+ templates = await client.read_resource("resource://assistant/templates/list")
+```
+
+## Version Selection
+
+
+
+When a server exposes multiple versions of a resource, you can request a specific version:
+
+```python
+async with client:
+ # Read the highest version (default)
+ content = await client.read_resource("data://config")
+
+ # Read a specific version
+ content_v1 = await client.read_resource("data://config", version="1.0")
+```
+
+See [Metadata](/servers/versioning#version-discovery) for how to discover available versions.
+
+## Raw Protocol Access
+
+For complete control, use `read_resource_mcp()` which returns the full MCP protocol object:
+
+```python
+async with client:
+ result = await client.read_resource_mcp("resource://example")
+ # result -> mcp.types.ReadResourceResult
+```
diff --git a/docs/v3/clients/roots.mdx b/docs/v3/clients/roots.mdx
new file mode 100644
index 000000000..0370c119a
--- /dev/null
+++ b/docs/v3/clients/roots.mdx
@@ -0,0 +1,45 @@
+---
+title: Client Roots
+sidebarTitle: Roots
+description: Provide local context and resource boundaries to MCP servers.
+icon: folder-tree
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Use this when you need to tell servers what local resources the client has access to.
+
+Roots inform servers about resources the client can provide. Servers can use this information to adjust behavior or provide more relevant responses.
+
+## Static Roots
+
+Provide a list of roots when creating the client:
+
+```python
+from fastmcp import Client
+
+client = Client(
+ "my_mcp_server.py",
+ roots=["/path/to/root1", "/path/to/root2"]
+)
+```
+
+## Dynamic Roots
+
+Use a callback to compute roots dynamically when the server requests them:
+
+```python
+from fastmcp import Client
+from fastmcp.client.roots import RequestContext
+
+async def roots_callback(context: RequestContext) -> list[str]:
+ print(f"Server requested roots (Request ID: {context.request_id})")
+ return ["/path/to/root1", "/path/to/root2"]
+
+client = Client(
+ "my_mcp_server.py",
+ roots=roots_callback
+)
+```
diff --git a/docs/v3/clients/sampling.mdx b/docs/v3/clients/sampling.mdx
new file mode 100644
index 000000000..6b845c3bb
--- /dev/null
+++ b/docs/v3/clients/sampling.mdx
@@ -0,0 +1,190 @@
+---
+title: LLM Sampling
+sidebarTitle: Sampling
+description: Handle server-initiated LLM completion requests.
+icon: robot
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx";
+
+
+
+Use this when you need to respond to server requests for LLM completions.
+
+MCP servers can request LLM completions from clients during tool execution. This enables servers to delegate AI reasoning to the client, which controls which LLM is used and how requests are made.
+
+## Handler Template
+
+```python
+from fastmcp import Client
+from fastmcp.client.sampling import SamplingMessage, SamplingParams, RequestContext
+
+async def sampling_handler(
+ messages: list[SamplingMessage],
+ params: SamplingParams,
+ context: RequestContext
+) -> str:
+ """
+ Handle server requests for LLM completions.
+
+ Args:
+ messages: Conversation messages to send to the LLM
+ params: Sampling parameters (temperature, max_tokens, etc.)
+ context: Request context with metadata
+
+ Returns:
+ Generated text response from your LLM
+ """
+ # Extract message content
+ conversation = []
+ for message in messages:
+ content = message.content.text if hasattr(message.content, 'text') else str(message.content)
+ conversation.append(f"{message.role}: {content}")
+
+ # Use the system prompt if provided
+ system_prompt = params.systemPrompt or "You are a helpful assistant."
+
+ # Integrate with your LLM service here
+ return "Generated response based on the messages"
+
+client = Client(
+ "my_mcp_server.py",
+ sampling_handler=sampling_handler,
+)
+```
+
+## Handler Parameters
+
+
+
+ The role of the message
+
+
+
+ The content of the message. TextContent has a `.text` attribute.
+
+
+
+
+
+ Optional system prompt the server wants to use
+
+
+
+ Server preferences for model selection (hints, cost/speed/intelligence priorities)
+
+
+
+ Sampling temperature
+
+
+
+ Maximum tokens to generate
+
+
+
+ Stop sequences for sampling
+
+
+
+ Tools the LLM can use during sampling
+
+
+
+ Tool usage behavior (`auto`, `required`, or `none`)
+
+
+
+## Built-in Handlers
+
+FastMCP provides built-in handlers for OpenAI, Anthropic, and Google Gemini APIs that support the full sampling API including tool use.
+
+### OpenAI Handler
+
+
+
+```python
+from fastmcp import Client
+from fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler
+
+client = Client(
+ "my_mcp_server.py",
+ sampling_handler=OpenAISamplingHandler(default_model="gpt-4o"),
+)
+```
+
+For OpenAI-compatible APIs (like local models):
+
+```python
+from openai import AsyncOpenAI
+
+client = Client(
+ "my_mcp_server.py",
+ sampling_handler=OpenAISamplingHandler(
+ default_model="llama-3.1-70b",
+ client=AsyncOpenAI(base_url="http://localhost:8000/v1"),
+ ),
+)
+```
+
+
+Install the OpenAI handler with `pip install fastmcp[openai]`.
+
+
+### Anthropic Handler
+
+
+
+```python
+from fastmcp import Client
+from fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler
+
+client = Client(
+ "my_mcp_server.py",
+ sampling_handler=AnthropicSamplingHandler(default_model="claude-sonnet-4-5"),
+)
+```
+
+
+Install the Anthropic handler with `pip install fastmcp[anthropic]`.
+
+
+### Google Gemini Handler
+
+
+
+```python
+from fastmcp import Client
+from fastmcp.client.sampling.handlers.google_genai import GoogleGenaiSamplingHandler
+
+client = Client(
+ "my_mcp_server.py",
+ sampling_handler=GoogleGenaiSamplingHandler(default_model="gemini-2.0-flash"),
+)
+```
+
+
+Install the Google Gemini handler with `pip install fastmcp[gemini]`.
+
+
+## Sampling Capabilities
+
+When you provide a `sampling_handler`, FastMCP automatically advertises full sampling capabilities to the server, including tool support. To disable tool support for simpler handlers:
+
+```python
+from mcp.types import SamplingCapability
+
+client = Client(
+ "my_mcp_server.py",
+ sampling_handler=basic_handler,
+ sampling_capabilities=SamplingCapability(), # No tool support
+)
+```
+
+## Tool Execution
+
+Tool execution happens on the server side. The client's role is to pass tools to the LLM and return the LLM's response (which may include tool use requests). The server then executes the tools and may send follow-up sampling requests with tool results.
+
+
+To implement a custom sampling handler, see the [handler source code](https://github.com/PrefectHQ/fastmcp/tree/main/fastmcp_slim/fastmcp/client/sampling/handlers) as a reference.
+
diff --git a/docs/v3/clients/tasks.mdx b/docs/v3/clients/tasks.mdx
new file mode 100644
index 000000000..ce27520e4
--- /dev/null
+++ b/docs/v3/clients/tasks.mdx
@@ -0,0 +1,182 @@
+---
+title: Background Tasks
+sidebarTitle: Tasks
+description: Execute operations asynchronously and track their progress.
+icon: clock
+tag: "NEW"
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Use this when you need to run long operations asynchronously while doing other work.
+
+The MCP task protocol lets you request operations to run in the background. The call returns a Task object immediately, letting you track progress, cancel operations, or await results.
+
+## Requesting Background Execution
+
+Pass `task=True` to run an operation as a background task:
+
+```python
+from fastmcp import Client
+
+async with Client(server) as client:
+ # Start a background task
+ task = await client.call_tool("slow_computation", {"duration": 10}, task=True)
+
+ print(f"Task started: {task.task_id}")
+
+ # Do other work while it runs...
+
+ # Get the result when ready
+ result = await task.result()
+```
+
+This works with tools, resources, and prompts:
+
+```python
+tool_task = await client.call_tool("my_tool", args, task=True)
+resource_task = await client.read_resource("file://large.txt", task=True)
+prompt_task = await client.get_prompt("my_prompt", args, task=True)
+```
+
+## Task API
+
+All task types share a common interface.
+
+### Getting Results
+
+Call `await task.result()` or simply `await task` to block until the task completes:
+
+```python
+task = await client.call_tool("analyze", {"text": "hello"}, task=True)
+
+# Wait for result (blocking)
+result = await task.result()
+# or: result = await task
+```
+
+### Checking Status
+
+Check the current status without blocking:
+
+```python
+status = await task.status()
+print(f"{status.status}: {status.statusMessage}")
+# status.status is "working", "completed", "failed", or "cancelled"
+```
+
+### Waiting with Control
+
+Use `task.wait()` for more control over waiting:
+
+```python
+# Wait up to 30 seconds for completion
+status = await task.wait(timeout=30.0)
+
+# Wait for a specific state
+status = await task.wait(state="completed", timeout=30.0)
+```
+
+### Cancellation
+
+Cancel a running task:
+
+```python
+await task.cancel()
+```
+
+## Status Updates
+
+Register callbacks to receive real-time status updates as the server reports progress:
+
+```python
+def on_status_change(status):
+ print(f"Task {status.taskId}: {status.status} - {status.statusMessage}")
+
+task.on_status_change(on_status_change)
+
+# Async callbacks work too
+async def on_status_async(status):
+ await log_status(status)
+
+task.on_status_change(on_status_async)
+```
+
+### Handler Template
+
+```python
+from fastmcp import Client
+
+def status_handler(status):
+ """
+ Handle task status updates.
+
+ Args:
+ status: Task status object with:
+ - taskId: Unique task identifier
+ - status: "working", "completed", "failed", or "cancelled"
+ - statusMessage: Optional progress message from server
+ """
+ if status.status == "working":
+ print(f"Progress: {status.statusMessage}")
+ elif status.status == "completed":
+ print("Task completed")
+ elif status.status == "failed":
+ print(f"Task failed: {status.statusMessage}")
+
+task.on_status_change(status_handler)
+```
+
+## Graceful Degradation
+
+You can always pass `task=True` regardless of whether the server supports background tasks. Per the MCP specification, servers without task support execute the operation immediately and return the result inline.
+
+```python
+task = await client.call_tool("my_tool", args, task=True)
+
+if task.returned_immediately:
+ print("Server executed immediately (no background support)")
+else:
+ print("Running in background")
+
+# Either way, this works
+result = await task.result()
+```
+
+This lets you write task-aware client code without worrying about server capabilities.
+
+## Example
+
+```python
+import asyncio
+from fastmcp import Client
+
+async def main():
+ async with Client(server) as client:
+ # Start background task
+ task = await client.call_tool(
+ "slow_computation",
+ {"duration": 10},
+ task=True,
+ )
+
+ # Subscribe to updates
+ def on_update(status):
+ print(f"Progress: {status.statusMessage}")
+
+ task.on_status_change(on_update)
+
+ # Do other work while task runs
+ print("Doing other work...")
+ await asyncio.sleep(2)
+
+ # Wait for completion and get result
+ result = await task.result()
+ print(f"Result: {result.content}")
+
+asyncio.run(main())
+```
+
+See [Server Background Tasks](/servers/tasks) for how to enable background task support on the server side.
diff --git a/docs/v3/clients/tools.mdx b/docs/v3/clients/tools.mdx
new file mode 100644
index 000000000..1541f593e
--- /dev/null
+++ b/docs/v3/clients/tools.mdx
@@ -0,0 +1,183 @@
+---
+title: Calling Tools
+sidebarTitle: Tools
+description: Execute server-side tools and handle structured results.
+icon: wrench
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Use this when you need to execute server-side functions and process their results.
+
+Tools are executable functions exposed by MCP servers. The client's `call_tool()` method executes a tool by name with arguments and returns structured results.
+
+## Basic Execution
+
+```python
+async with client:
+ result = await client.call_tool("add", {"a": 5, "b": 3})
+ # result -> CallToolResult with structured and unstructured data
+
+ # Access structured data (automatically deserialized)
+ print(result.data) # 8
+
+ # Access traditional content blocks
+ print(result.content[0].text) # "8"
+```
+
+Arguments are passed as a dictionary. For multi-server clients, tool names are automatically prefixed with the server name (e.g., `weather_get_forecast` for a tool named `get_forecast` on the `weather` server).
+
+## Execution Options
+
+The `call_tool()` method supports timeout control and progress monitoring:
+
+```python
+async with client:
+ # With timeout (aborts if execution takes longer than 2 seconds)
+ result = await client.call_tool(
+ "long_running_task",
+ {"param": "value"},
+ timeout=2.0
+ )
+
+ # With progress handler
+ result = await client.call_tool(
+ "long_running_task",
+ {"param": "value"},
+ progress_handler=my_progress_handler
+ )
+```
+
+## Structured Results
+
+
+
+Tool execution returns a `CallToolResult` object. The `.data` property provides fully hydrated Python objects including complex types like datetimes and UUIDs, reconstructed from the server's output schema.
+
+```python
+from datetime import datetime
+from uuid import UUID
+
+async with client:
+ result = await client.call_tool("get_weather", {"city": "London"})
+
+ # FastMCP reconstructs complete Python objects
+ weather = result.data
+ print(f"Temperature: {weather.temperature}C at {weather.timestamp}")
+
+ # Complex types are properly deserialized
+ assert isinstance(weather.timestamp, datetime)
+ assert isinstance(weather.station_id, UUID)
+
+ # Raw structured JSON is also available
+ print(f"Raw JSON: {result.structured_content}")
+```
+
+
+
+ Fully hydrated Python objects with complex type support (datetimes, UUIDs, custom classes). FastMCP exclusive.
+
+
+
+ Standard MCP content blocks (`TextContent`, `ImageContent`, `AudioContent`, etc.).
+
+
+
+ Standard MCP structured JSON data as sent by the server.
+
+
+
+ Boolean indicating if the tool execution failed.
+
+
+
+For tools without output schemas or when deserialization fails, `.data` will be `None`. Fall back to content blocks in that case:
+
+```python
+async with client:
+ result = await client.call_tool("legacy_tool", {"param": "value"})
+
+ if result.data is not None:
+ print(f"Structured: {result.data}")
+ else:
+ for content in result.content:
+ if hasattr(content, 'text'):
+ print(f"Text result: {content.text}")
+```
+
+
+FastMCP servers automatically wrap primitive results (like `int`, `str`, `bool`) in a `{"result": value}` structure. FastMCP clients automatically unwrap this, so you get the original value in `.data`.
+
+
+## Error Handling
+
+By default, `call_tool()` raises a `ToolError` if the tool execution fails:
+
+```python
+from fastmcp.exceptions import ToolError
+
+async with client:
+ try:
+ result = await client.call_tool("potentially_failing_tool", {"param": "value"})
+ print("Tool succeeded:", result.data)
+ except ToolError as e:
+ print(f"Tool failed: {e}")
+```
+
+To handle errors manually instead of catching exceptions, disable automatic error raising:
+
+```python
+async with client:
+ result = await client.call_tool(
+ "potentially_failing_tool",
+ {"param": "value"},
+ raise_on_error=False
+ )
+
+ if result.is_error:
+ print(f"Tool failed: {result.content[0].text}")
+ else:
+ print(f"Tool succeeded: {result.data}")
+```
+
+## Sending Metadata
+
+
+
+The `meta` parameter sends ancillary information alongside tool calls for observability, debugging, or client identification:
+
+```python
+async with client:
+ result = await client.call_tool(
+ name="send_email",
+ arguments={
+ "to": "user@example.com",
+ "subject": "Hello",
+ "body": "Welcome!"
+ },
+ meta={
+ "trace_id": "abc-123",
+ "request_source": "mobile_app"
+ }
+ )
+```
+
+See [Client Metadata](/servers/context#client-metadata) to learn how servers access this data.
+
+## Raw Protocol Access
+
+For complete control, use `call_tool_mcp()` which returns the raw MCP protocol object:
+
+```python
+async with client:
+ result = await client.call_tool_mcp("my_tool", {"param": "value"})
+ # result -> mcp.types.CallToolResult
+
+ if result.isError:
+ print(f"Tool failed: {result.content}")
+ else:
+ print(f"Tool succeeded: {result.content}")
+ # Note: No automatic deserialization with call_tool_mcp()
+```
diff --git a/docs/v3/clients/transports.mdx b/docs/v3/clients/transports.mdx
new file mode 100644
index 000000000..efcda3366
--- /dev/null
+++ b/docs/v3/clients/transports.mdx
@@ -0,0 +1,267 @@
+---
+title: Client Transports
+sidebarTitle: Transports
+description: Configure how clients connect to and communicate with MCP servers.
+icon: link
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Transports handle the underlying connection between your client and MCP servers. While the client can automatically select a transport based on what you pass to it, instantiating transports explicitly gives you full control over configuration.
+
+## STDIO Transport
+
+STDIO transport communicates with MCP servers through subprocess pipes. When using STDIO, your client launches and manages the server process, controlling its lifecycle and environment.
+
+
+STDIO servers run in isolated environments by default. They do not inherit your shell's environment variables. You must explicitly pass any configuration the server needs.
+
+
+```python
+from fastmcp import Client
+from fastmcp.client.transports import StdioTransport
+
+transport = StdioTransport(
+ command="python",
+ args=["my_server.py", "--verbose"],
+ env={"API_KEY": "secret", "LOG_LEVEL": "DEBUG"},
+ cwd="/path/to/server"
+)
+client = Client(transport)
+```
+
+For convenience, the client can infer STDIO transport from file paths, though this limits configuration options:
+
+```python
+from fastmcp import Client
+
+client = Client("my_server.py") # Limited - no configuration options
+```
+
+### Environment Variables
+
+Since STDIO servers do not inherit your environment, you need strategies for passing configuration.
+
+**Selective forwarding** passes only the variables your server needs:
+
+```python
+import os
+from fastmcp.client.transports import StdioTransport
+
+required_vars = ["API_KEY", "DATABASE_URL", "REDIS_HOST"]
+env = {var: os.environ[var] for var in required_vars if var in os.environ}
+
+transport = StdioTransport(command="python", args=["server.py"], env=env)
+client = Client(transport)
+```
+
+**Loading from .env files** keeps configuration separate from code:
+
+```python
+from dotenv import dotenv_values
+from fastmcp.client.transports import StdioTransport
+
+env = dotenv_values(".env")
+transport = StdioTransport(command="python", args=["server.py"], env=env)
+client = Client(transport)
+```
+
+### Session Persistence
+
+STDIO transports maintain sessions across multiple client contexts by default (`keep_alive=True`). This reuses the same subprocess for multiple connections, improving performance.
+
+```python
+from fastmcp.client.transports import StdioTransport
+
+transport = StdioTransport(command="python", args=["server.py"])
+client = Client(transport)
+
+async def efficient_multiple_operations():
+ async with client:
+ await client.ping()
+
+ async with client: # Reuses the same subprocess
+ await client.call_tool("process_data", {"file": "data.csv"})
+```
+
+For complete isolation between connections, disable session persistence:
+
+```python
+transport = StdioTransport(command="python", args=["server.py"], keep_alive=False)
+```
+
+## HTTP Transport
+
+
+
+HTTP transport connects to MCP servers running as web services. This is the recommended transport for production deployments.
+
+```python
+from fastmcp import Client
+from fastmcp.client.transports import StreamableHttpTransport
+
+transport = StreamableHttpTransport(
+ url="https://api.example.com/mcp",
+ headers={
+ "Authorization": "Bearer your-token-here",
+ "X-Custom-Header": "value"
+ }
+)
+client = Client(transport)
+```
+
+FastMCP also provides authentication helpers:
+
+```python
+from fastmcp import Client
+from fastmcp.client.auth import BearerAuth
+
+client = Client(
+ "https://api.example.com/mcp",
+ auth=BearerAuth("your-token-here")
+)
+```
+
+### SSL Verification
+
+By default, HTTPS connections verify the server's SSL certificate. You can customize this behavior with the `verify` parameter, which accepts the same values as [httpx](https://www.python-httpx.org/advanced/ssl/):
+
+```python
+from fastmcp import Client
+
+# Disable SSL verification (e.g., for self-signed certs in development)
+client = Client("https://dev-server.internal/mcp", verify=False)
+
+# Use a custom CA bundle
+client = Client("https://corp-server.internal/mcp", verify="/path/to/ca-bundle.pem")
+
+# Use a custom SSL context for full control
+import ssl
+ctx = ssl.create_default_context()
+ctx.load_verify_locations("/path/to/internal-ca.pem")
+client = Client("https://corp-server.internal/mcp", verify=ctx)
+```
+
+The `verify` parameter is also available directly on `StreamableHttpTransport` and `SSETransport`:
+
+```python
+from fastmcp.client.transports import StreamableHttpTransport
+
+transport = StreamableHttpTransport(
+ url="https://dev-server.internal/mcp",
+ verify=False,
+)
+client = Client(transport)
+```
+
+### SSE Transport
+
+Server-Sent Events transport is maintained for backward compatibility. Use Streamable HTTP for new deployments unless you have specific infrastructure requirements.
+
+```python
+from fastmcp.client.transports import SSETransport
+
+transport = SSETransport(
+ url="https://api.example.com/sse",
+ headers={"Authorization": "Bearer token"}
+)
+client = Client(transport)
+```
+
+## In-Memory Transport
+
+In-memory transport connects directly to a FastMCP server instance within the same Python process. This eliminates both subprocess management and network overhead, making it ideal for testing.
+
+```python
+from fastmcp import FastMCP, Client
+import os
+
+mcp = FastMCP("TestServer")
+
+@mcp.tool
+def greet(name: str) -> str:
+ prefix = os.environ.get("GREETING_PREFIX", "Hello")
+ return f"{prefix}, {name}!"
+
+client = Client(mcp)
+
+async with client:
+ result = await client.call_tool("greet", {"name": "World"})
+```
+
+
+Unlike STDIO transports, in-memory servers share the same memory space and environment variables as your client code.
+
+
+## Multi-Server Configuration
+
+
+
+Connect to multiple servers defined in a configuration dictionary:
+
+```python
+from fastmcp import Client
+
+config = {
+ "mcpServers": {
+ "weather": {
+ "url": "https://weather.example.com/mcp",
+ "transport": "http"
+ },
+ "assistant": {
+ "command": "python",
+ "args": ["./assistant.py"],
+ "env": {"LOG_LEVEL": "INFO"}
+ }
+ }
+}
+
+client = Client(config)
+
+async with client:
+ # Tools are namespaced by server
+ weather = await client.call_tool("weather_get_forecast", {"city": "NYC"})
+ answer = await client.call_tool("assistant_ask", {"question": "What?"})
+```
+
+### Tool Transformations
+
+FastMCP supports tool transformations within the configuration. You can change names, descriptions, tags, and arguments for tools from a server.
+
+```python
+config = {
+ "mcpServers": {
+ "weather": {
+ "url": "https://weather.example.com/mcp",
+ "transport": "http",
+ "tools": {
+ "weather_get_forecast": {
+ "name": "miami_weather",
+ "description": "Get the weather for Miami",
+ "arguments": {
+ "city": {
+ "default": "Miami",
+ "hide": True,
+ }
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+To filter tools by tag, use `include_tags` or `exclude_tags` at the server level:
+
+```python
+config = {
+ "mcpServers": {
+ "weather": {
+ "url": "https://weather.example.com/mcp",
+ "include_tags": ["forecast"] # Only tools with this tag
+ }
+ }
+}
+```
diff --git a/docs/v3/community/README.md b/docs/v3/community/README.md
new file mode 100644
index 000000000..b61f5cf6d
--- /dev/null
+++ b/docs/v3/community/README.md
@@ -0,0 +1,22 @@
+# Community Section
+
+This directory contains community-contributed content and showcases for FastMCP.
+
+## Structure
+
+- `showcase.mdx` - Main community showcase page featuring high-quality projects and examples
+
+## Adding Content
+
+To add new community content:
+1. Create a new MDX file in this directory
+2. Update `docs.json` to include it in the navigation
+3. Follow the existing format for consistency
+
+## Guidelines
+
+Community content should:
+- Demonstrate best practices
+- Provide educational value
+- Include proper documentation
+- Be maintained and up-to-date
\ No newline at end of file
diff --git a/docs/v3/community/showcase.mdx b/docs/v3/community/showcase.mdx
new file mode 100644
index 000000000..9ba4c6877
--- /dev/null
+++ b/docs/v3/community/showcase.mdx
@@ -0,0 +1,65 @@
+---
+title: 'Community Showcase'
+description: 'High-quality projects and examples from the FastMCP community'
+icon: 'users'
+---
+
+import { YouTubeEmbed } from '/snippets/youtube-embed.mdx'
+
+## Join the Community
+
+
+ Connect with other FastMCP developers, share your projects, and discuss ideas.
+
+
+## Featured Projects
+
+Discover exemplary MCP servers and implementations created by our community. These projects demonstrate best practices and innovative uses of FastMCP.
+
+### Learning Resources
+
+
+ A comprehensive educational example demonstrating FastMCP best practices with professional dual-transport server implementation, interactive test client, and detailed documentation.
+
+
+#### Video Tutorials
+
+**Build Remote MCP Servers w/ Python & FastMCP** - Claude Integrations Tutorial by Greg + Code
+
+
+
+**FastMCP — the best way to build an MCP server with Python** - Tutorial by ZazenCodes
+
+
+
+**Speedrun a MCP server for Claude Desktop (fastmcp)** - Tutorial by Nate from Prefect
+
+
+
+### Community Examples
+
+Have you built something interesting with FastMCP? We'd love to feature high-quality examples here! Start a [discussion on GitHub](https://github.com/PrefectHQ/fastmcp/discussions) to share your project.
+
+## Contributing
+
+To get your project featured:
+
+1. Ensure your project demonstrates best practices
+2. Include comprehensive documentation
+3. Add clear usage examples
+4. Open a discussion in our [GitHub Discussions](https://github.com/PrefectHQ/fastmcp/discussions)
+
+We review submissions regularly and feature projects that provide value to the FastMCP community.
+
+## Further Reading
+
+- [Contrib Modules](/patterns/contrib) - Community-contributed modules that are distributed with FastMCP itself
\ No newline at end of file
diff --git a/docs/v3/deployment/http.mdx b/docs/v3/deployment/http.mdx
new file mode 100644
index 000000000..16c9fadfa
--- /dev/null
+++ b/docs/v3/deployment/http.mdx
@@ -0,0 +1,924 @@
+---
+title: HTTP Deployment
+sidebarTitle: HTTP Deployment
+description: Deploy your FastMCP server over HTTP for remote access
+icon: server
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx";
+
+
+STDIO transport is perfect for local development and desktop applications. But to unlock the full potential of MCP—centralized services, multi-client access, and network availability—you need remote HTTP deployment.
+
+
+This guide walks you through deploying your FastMCP server as a remote MCP service that's accessible via a URL. Once deployed, your MCP server will be available over the network, allowing multiple clients to connect simultaneously and enabling integration with cloud-based LLM applications. This guide focuses specifically on remote MCP deployment, not local STDIO servers.
+
+## Choosing Your Approach
+
+FastMCP provides two ways to deploy your server as an HTTP service. Understanding the trade-offs helps you choose the right approach for your needs.
+
+The **direct HTTP server** approach is simpler and perfect for getting started quickly. You modify your server's `run()` method to use HTTP transport, and FastMCP handles all the web server configuration. This approach works well for standalone deployments where you want your MCP server to be the only service running on a port.
+
+The **ASGI application** approach gives you more control and flexibility. Instead of running the server directly, you create an ASGI application that can be served by Uvicorn. This approach is better when you need advanced server features like multiple workers, custom middleware, or when you're integrating with existing web applications.
+
+### Direct HTTP Server
+
+The simplest way to get your MCP server online is to use the built-in `run()` method with HTTP transport. This approach handles all the server configuration for you and is ideal when you want a standalone MCP server without additional complexity.
+
+```python server.py
+from fastmcp import FastMCP
+
+mcp = FastMCP("My Server")
+
+@mcp.tool
+def process_data(input: str) -> str:
+ """Process data on the server"""
+ return f"Processed: {input}"
+
+if __name__ == "__main__":
+ mcp.run(transport="http", host="0.0.0.0", port=8000)
+```
+
+Run your server with a simple Python command:
+```bash
+python server.py
+```
+
+Your server is now accessible at `http://localhost:8000/mcp` (or use your server's actual IP address for remote access).
+
+This approach is ideal when you want to get online quickly with minimal configuration. It's perfect for internal tools, development environments, or simple deployments where you don't need advanced server features. The built-in server handles all the HTTP details, letting you focus on your MCP implementation.
+
+### ASGI Application
+
+For production deployments, you'll often want more control over how your server runs. FastMCP can create a standard ASGI application that works with any ASGI server like Uvicorn, Gunicorn, or Hypercorn. This approach is particularly useful when you need to configure advanced server options, run multiple workers, or integrate with existing infrastructure.
+
+```python app.py
+from fastmcp import FastMCP
+
+mcp = FastMCP("My Server")
+
+@mcp.tool
+def process_data(input: str) -> str:
+ """Process data on the server"""
+ return f"Processed: {input}"
+
+# Create ASGI application
+app = mcp.http_app()
+```
+
+Run with any ASGI server - here's an example with Uvicorn:
+```bash
+uvicorn app:app --host 0.0.0.0 --port 8000
+```
+
+Your server is accessible at the same URL: `http://localhost:8000/mcp` (or use your server's actual IP address for remote access).
+
+The ASGI approach shines in production environments where you need reliability and performance. You can run multiple worker processes to handle concurrent requests, add custom middleware for logging or monitoring, integrate with existing deployment pipelines, or mount your MCP server as part of a larger application.
+
+## Configuring Your Server
+
+### Custom Path
+
+By default, your MCP server is accessible at `/mcp/` on your domain. You can customize this path to fit your URL structure or avoid conflicts with existing endpoints. This is particularly useful when integrating MCP into an existing application or following specific API conventions.
+
+```python
+# Option 1: With mcp.run()
+mcp.run(transport="http", host="0.0.0.0", port=8000, path="/api/mcp/")
+
+# Option 2: With ASGI app
+app = mcp.http_app(path="/api/mcp/")
+```
+
+Now your server is accessible at `http://localhost:8000/api/mcp/`.
+
+### Authentication
+
+
+Authentication is **highly recommended** for remote MCP servers. Some LLM clients require authentication for remote servers and will refuse to connect without it.
+
+
+FastMCP supports multiple authentication methods to secure your remote server. See the [Authentication Overview](/servers/auth/authentication) for complete configuration options including Bearer tokens, JWT, and OAuth.
+
+If you're mounting an authenticated server under a path prefix, see [Mounting Authenticated Servers](#mounting-authenticated-servers) below for important routing considerations.
+
+### Host and Origin Protection
+
+FastMCP can validate `Host` and browser `Origin` headers for Streamable HTTP requests before they reach MCP session handling. This request guard protects localhost-bound servers from DNS rebinding attacks, and it remains opt-in in FastMCP 3.x to preserve compatibility with existing ASGI, serverless, and reverse-proxy deployments.
+
+Think of this as a request guard rather than CORS middleware. It decides whether a request can reach MCP session handling. CORS remains a separate browser response-header policy; configure CORS middleware separately when browser JavaScript must read cross-origin responses.
+
+Enable strict validation with `host_origin_protection=True`. When you deploy behind a public hostname, add the hostname clients use to reach your MCP endpoint. If a browser-based MCP client runs on a separate origin, add that origin as well:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("My Server")
+
+app = mcp.http_app(
+ host_origin_protection=True,
+ allowed_hosts=["mcp.example.com"],
+ allowed_origins=["https://app.example.com"],
+)
+```
+
+For the direct server approach, pass the same values to `run()`:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("My Server")
+
+if __name__ == "__main__":
+ mcp.run(
+ transport="http",
+ host="0.0.0.0",
+ port=8000,
+ host_origin_protection=True,
+ allowed_hosts=["mcp.example.com"],
+ allowed_origins=["https://app.example.com"],
+ )
+```
+
+You can also configure these values with environment variables:
+
+```bash
+export FASTMCP_HTTP_HOST_ORIGIN_PROTECTION=true
+export FASTMCP_HTTP_ALLOWED_HOSTS='["mcp.example.com"]'
+export FASTMCP_HTTP_ALLOWED_ORIGINS='["https://app.example.com"]'
+```
+
+Use `host_origin_protection="auto"` to protect localhost-bound direct servers while allowing ASGI, serverless, and reverse-proxy deployments to keep their existing Host handling unless they configure explicit trust rules. Use `host_origin_protection=False` to keep the request guard disabled.
+
+### Health Checks
+
+Health check endpoints are essential for monitoring your deployed server and ensuring it's responding correctly. FastMCP allows you to add custom routes alongside your MCP endpoints, making it easy to implement health checks that work with both deployment approaches.
+
+```python
+from starlette.responses import JSONResponse
+
+@mcp.custom_route("/health", methods=["GET"])
+async def health_check(request):
+ return JSONResponse({"status": "healthy", "service": "mcp-server"})
+```
+
+This health endpoint will be available at `http://localhost:8000/health` and can be used by load balancers, monitoring systems, or deployment platforms to verify your server is running.
+
+
+Custom routes are never protected by the server's authentication middleware, even when an `AuthProvider` is configured. This is by design — the primary use case for custom routes is unauthenticated operational endpoints like health checks and readiness probes. If you need authenticated HTTP endpoints alongside your MCP server, [mount it in a FastAPI app](/integrations/fastapi) and use FastAPI's `Depends()` for auth on your routes.
+
+
+### Custom Middleware
+
+
+
+
+Add custom Starlette middleware to your FastMCP ASGI apps:
+
+```python
+from fastmcp import FastMCP
+from starlette.middleware import Middleware
+from starlette.middleware.cors import CORSMiddleware
+
+# Create your FastMCP server
+mcp = FastMCP("MyServer")
+
+# Define middleware
+middleware = [
+ Middleware(
+ CORSMiddleware,
+ allow_origins=["*"],
+ allow_methods=["*"],
+ allow_headers=["*"],
+ )
+]
+
+# Create ASGI app with middleware
+http_app = mcp.http_app(middleware=middleware)
+```
+
+### CORS for Browser-Based Clients
+
+
+Most MCP clients, including those that you access through a browser like ChatGPT or Claude, don't need CORS configuration. Only enable CORS if you're working with an MCP client that connects directly from a browser, such as debugging tools or inspectors.
+
+
+CORS (Cross-Origin Resource Sharing) is needed when JavaScript running in a web browser connects directly to your MCP server. This is different from using an LLM through a browser—in that case, the browser connects to the LLM service, and the LLM service connects to your MCP server (no CORS needed).
+
+Host and Origin protection runs before CORS when it is active for a request. Add browser client origins to `allowed_origins` so trusted browser requests reach the CORS middleware, then configure CORS to let browser JavaScript read the MCP response headers it needs. Setting `allowed_origins` trusts the request; it does not emit `Access-Control-Allow-Origin` or other CORS response headers.
+
+Browser-based MCP clients that need CORS include:
+
+- **MCP Inspector** - Browser-based debugging tool for testing MCP servers
+- **Custom browser-based MCP clients** - If you're building a web app that directly connects to MCP servers
+
+For these scenarios, add CORS middleware with the specific headers required for MCP protocol:
+
+```python
+from fastmcp import FastMCP
+from starlette.middleware import Middleware
+from starlette.middleware.cors import CORSMiddleware
+
+mcp = FastMCP("MyServer")
+
+# Configure CORS for browser-based clients
+middleware = [
+ Middleware(
+ CORSMiddleware,
+ allow_origins=["*"], # Allow all origins; use specific origins for security
+ allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
+ allow_headers=[
+ "mcp-protocol-version",
+ "mcp-session-id",
+ "Authorization",
+ "Content-Type",
+ ],
+ expose_headers=["mcp-session-id"],
+ )
+]
+
+app = mcp.http_app(middleware=middleware)
+```
+
+**Key configuration details:**
+
+- **`allow_origins`**: Specify exact origins (e.g., `["http://localhost:3000"]`) rather than `["*"]` for production deployments
+- **`allow_headers`**: Must include `mcp-protocol-version`, `mcp-session-id`, and `Authorization` (for authenticated servers)
+- **`expose_headers`**: Must include `mcp-session-id` so JavaScript can read the session ID from responses and send it in subsequent requests
+
+Without `expose_headers=["mcp-session-id"]`, browsers will receive the session ID but JavaScript won't be able to access it, causing session management to fail.
+
+
+**Production Security**: Never use `allow_origins=["*"]` in production. Specify the exact origins of your browser-based clients. Using wildcards exposes your server to unauthorized access from any website.
+
+
+### SSE Polling for Long-Running Operations
+
+
+
+
+This feature only applies to the **StreamableHTTP transport** (the default for `http_app()`). It does not apply to the legacy SSE transport (`transport="sse"`).
+
+
+When running tools that take a long time to complete, you may encounter issues with load balancers or proxies terminating connections that stay idle too long. [SEP-1699](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1699) introduces SSE polling to solve this by allowing the server to gracefully close connections and have clients automatically reconnect.
+
+To enable SSE polling, configure an `EventStore` when creating your HTTP application:
+
+```python
+from fastmcp import FastMCP, Context
+from fastmcp.server.event_store import EventStore
+
+mcp = FastMCP("My Server")
+
+@mcp.tool
+async def long_running_task(ctx: Context) -> str:
+ """A task that takes several minutes to complete."""
+ for i in range(100):
+ await ctx.report_progress(i, 100)
+
+ # Periodically close the connection to avoid load balancer timeouts
+ # Client will automatically reconnect and resume receiving progress
+ if i % 30 == 0 and i > 0:
+ await ctx.close_sse_stream()
+
+ await do_expensive_work()
+
+ return "Done!"
+
+# Configure with EventStore for resumability
+event_store = EventStore()
+app = mcp.http_app(
+ event_store=event_store,
+ retry_interval=2000, # Client reconnects after 2 seconds
+)
+```
+
+**How it works:**
+
+1. When `event_store` is configured, the server stores all events (progress updates, results) with unique IDs
+2. Calling `ctx.close_sse_stream()` gracefully closes the HTTP connection
+3. The client automatically reconnects with a `Last-Event-ID` header
+4. The server replays any events the client missed during the disconnection
+
+The `retry_interval` parameter (in milliseconds) controls how long clients wait before reconnecting. Choose a value that balances responsiveness with server load.
+
+
+`close_sse_stream()` is a no-op if called without an `EventStore` configured, so you can safely include it in tools that may run in different deployment configurations.
+
+
+#### Custom Storage Backends
+
+By default, `EventStore` uses in-memory storage. For production deployments with multiple server instances, you can provide a custom storage backend using the `key_value` package:
+
+```python
+from fastmcp.server.event_store import EventStore
+from key_value.aio.stores.redis import RedisStore
+
+# Use Redis for distributed deployments
+redis_store = RedisStore(url="redis://localhost:6379")
+event_store = EventStore(
+ storage=redis_store,
+ max_events_per_stream=100, # Keep last 100 events per stream
+ ttl=3600, # Events expire after 1 hour
+)
+
+app = mcp.http_app(event_store=event_store)
+```
+
+## Integration with Web Frameworks
+
+If you already have a web application running, you can add MCP capabilities by mounting a FastMCP server as a sub-application. This allows you to expose MCP tools alongside your existing API endpoints, sharing the same domain and infrastructure. The MCP server becomes just another route in your application, making it easy to manage and deploy.
+
+### Mounting in Starlette
+
+Mount your FastMCP server in a Starlette application:
+
+```python
+from fastmcp import FastMCP
+from starlette.applications import Starlette
+from starlette.routing import Mount
+
+# Create your FastMCP server
+mcp = FastMCP("MyServer")
+
+@mcp.tool
+def analyze(data: str) -> dict:
+ return {"result": f"Analyzed: {data}"}
+
+# Create the ASGI app
+mcp_app = mcp.http_app(path='/mcp')
+
+# Create a Starlette app and mount the MCP server
+app = Starlette(
+ routes=[
+ Mount("/mcp-server", app=mcp_app),
+ # Add other routes as needed
+ ],
+ lifespan=mcp_app.lifespan,
+)
+```
+
+The MCP endpoint will be available at `/mcp-server/mcp/` of the resulting Starlette app.
+
+
+For Streamable HTTP transport, you **must** pass the lifespan context from the FastMCP app to the resulting Starlette app, as nested lifespans are not recognized. Otherwise, the FastMCP server's session manager will not be properly initialized.
+
+
+#### Nested Mounts
+
+You can create complex routing structures by nesting mounts:
+
+```python
+from fastmcp import FastMCP
+from starlette.applications import Starlette
+from starlette.routing import Mount
+
+# Create your FastMCP server
+mcp = FastMCP("MyServer")
+
+# Create the ASGI app
+mcp_app = mcp.http_app(path='/mcp')
+
+# Create nested application structure
+inner_app = Starlette(routes=[Mount("/inner", app=mcp_app)])
+app = Starlette(
+ routes=[Mount("/outer", app=inner_app)],
+ lifespan=mcp_app.lifespan,
+)
+```
+
+In this setup, the MCP server is accessible at the `/outer/inner/mcp/` path.
+
+### FastAPI Integration
+
+For FastAPI-specific integration patterns including both mounting MCP servers into FastAPI apps and generating MCP servers from FastAPI apps, see the [FastAPI Integration guide](/integrations/fastapi).
+
+Here's a quick example showing how to add MCP to an existing FastAPI application:
+
+```python
+from fastapi import FastAPI
+from fastmcp import FastMCP
+
+# Create your MCP server
+mcp = FastMCP("API Tools")
+
+@mcp.tool
+def query_database(query: str) -> dict:
+ """Run a database query"""
+ return {"result": "data"}
+
+# Create the MCP ASGI app with path="/" since we'll mount at /mcp
+mcp_app = mcp.http_app(path="/")
+
+# Create FastAPI app with MCP lifespan (required for session management)
+api = FastAPI(lifespan=mcp_app.lifespan)
+
+@api.get("/api/status")
+def status():
+ return {"status": "ok"}
+
+# Mount MCP at /mcp
+api.mount("/mcp", mcp_app)
+
+# Run with: uvicorn app:api --host 0.0.0.0 --port 8000
+```
+
+Your existing API remains at `http://localhost:8000/api` while MCP is available at `http://localhost:8000/mcp`.
+
+
+Just like with Starlette, you **must** pass the lifespan from the MCP app to FastAPI. Without this, the session manager won't initialize properly and requests will fail.
+
+
+## Mounting Authenticated Servers
+
+
+
+
+This section only applies if you're **mounting an OAuth-protected FastMCP server under a path prefix** (like `/api`) inside another application using `Mount()`.
+
+If you're deploying your FastMCP server at root level without any `Mount()` prefix, the well-known routes are automatically included in `mcp.http_app()` and you don't need to do anything special.
+
+
+OAuth specifications (RFC 8414 and RFC 9728) require discovery metadata to be accessible at well-known paths under the root level of your domain. When you mount an OAuth-protected FastMCP server under a path prefix like `/api`, this creates a routing challenge: your operational OAuth endpoints move under the prefix, but discovery endpoints must remain at the root.
+
+
+**Common Mistakes to Avoid:**
+
+1. **Forgetting to mount `.well-known` routes at root** - FastMCP cannot do this automatically when your server is mounted under a path prefix. You must explicitly mount well-known routes at the root level.
+
+2. **Including mount prefix in both base_url AND mcp_path** - The mount prefix (like `/api`) should only be in `base_url`, not in `mcp_path`. Otherwise you'll get double paths.
+
+ ✅ **Correct:**
+ ```python
+ base_url = "http://localhost:8000/api"
+ mcp_path = "/mcp"
+ # Result: /api/mcp
+ ```
+
+ ❌ **Wrong:**
+ ```python
+ base_url = "http://localhost:8000/api"
+ mcp_path = "/api/mcp"
+ # Result: /api/api/mcp (double prefix!)
+ ```
+
+Follow the configuration instructions below to set up mounting correctly.
+
+
+
+**CORS Middleware Conflicts:**
+
+If you're integrating FastMCP into an existing application with its own CORS middleware, be aware that layering CORS middleware can cause conflicts (such as 404 errors on `.well-known` routes or OPTIONS requests).
+
+FastMCP and the MCP SDK already handle CORS for OAuth routes. If you need CORS on your own application routes, consider using the sub-app pattern: mount FastMCP and your routes as separate apps, each with their own middleware, rather than adding application-wide CORS middleware.
+
+
+### Route Types
+
+OAuth-protected MCP servers expose two categories of routes:
+
+**Operational routes** handle the OAuth flow and MCP protocol:
+- `/authorize` - OAuth authorization endpoint
+- `/token` - Token exchange endpoint
+- `/auth/callback` - OAuth callback handler
+- `/mcp` - MCP protocol endpoint
+
+**Discovery routes** provide metadata for OAuth clients:
+- `/.well-known/oauth-authorization-server` - Authorization server metadata
+- `/.well-known/oauth-protected-resource/*` - Protected resource metadata
+
+When you mount your MCP app under a prefix, operational routes move with it, but discovery routes must stay at root level for RFC compliance.
+
+### Configuration Parameters
+
+Three parameters control where routes are located and how they combine:
+
+**`base_url`** tells clients where to find operational endpoints. This includes any Starlette `Mount()` path prefix (e.g., `/api`):
+
+```python
+base_url="http://localhost:8000/api" # Includes mount prefix
+```
+
+**`mcp_path`** is the internal FastMCP endpoint path, which gets appended to `base_url`:
+
+```python
+mcp_path="/mcp" # Internal MCP path, NOT the mount prefix
+```
+
+**`issuer_url`** (optional) controls the authorization server identity for OAuth discovery. Defaults to `base_url`.
+
+```python
+# Usually not needed - just set base_url and it works
+issuer_url="http://localhost:8000" # Only if you want root-level discovery
+```
+
+When `issuer_url` has a path (either explicitly or by defaulting from `base_url`), FastMCP creates path-aware discovery routes per RFC 8414. For example, if `base_url` is `http://localhost:8000/api`, the authorization server metadata will be at `/.well-known/oauth-authorization-server/api`.
+
+**Key Invariant:** `base_url + mcp_path = actual externally-accessible MCP URL`
+
+Example:
+- `base_url`: `http://localhost:8000/api` (mount prefix `/api`)
+- `mcp_path`: `/mcp` (internal path)
+- Result: `http://localhost:8000/api/mcp` (final MCP endpoint)
+
+Note that the mount prefix (`/api` from `Mount("/api", ...)`) goes in `base_url`, while `mcp_path` is just the internal MCP route. Don't include the mount prefix in both places or you'll get `/api/api/mcp`.
+
+### Mounting Strategy
+
+When mounting an OAuth-protected server under a path prefix, declare your URLs upfront to make the relationships clear:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.github import GitHubProvider
+from starlette.applications import Starlette
+from starlette.routing import Mount
+
+# Define the routing structure
+ROOT_URL = "http://localhost:8000"
+MOUNT_PREFIX = "/api"
+MCP_PATH = "/mcp"
+```
+
+Create the auth provider with `base_url`:
+
+```python
+auth = GitHubProvider(
+ client_id="your-client-id",
+ client_secret="your-client-secret",
+ base_url=f"{ROOT_URL}{MOUNT_PREFIX}", # Operational endpoints under prefix
+ # issuer_url defaults to base_url - path-aware discovery works automatically
+)
+```
+
+Create the MCP app, which generates operational routes at the specified path:
+
+```python
+mcp = FastMCP("Protected Server", auth=auth)
+mcp_app = mcp.http_app(path=MCP_PATH)
+```
+
+Retrieve the discovery routes from the auth provider. The `mcp_path` argument should match the path used when creating the MCP app:
+
+```python
+well_known_routes = auth.get_well_known_routes(mcp_path=MCP_PATH)
+```
+
+Finally, mount everything in the Starlette app with discovery routes at root and the MCP app under the prefix:
+
+```python
+app = Starlette(
+ routes=[
+ *well_known_routes, # Discovery routes at root level
+ Mount(MOUNT_PREFIX, app=mcp_app), # Operational routes under prefix
+ ],
+ lifespan=mcp_app.lifespan,
+)
+```
+
+This configuration produces the following URL structure:
+
+- MCP endpoint: `http://localhost:8000/api/mcp`
+- OAuth authorization: `http://localhost:8000/api/authorize`
+- OAuth callback: `http://localhost:8000/api/auth/callback`
+- Authorization server metadata: `http://localhost:8000/.well-known/oauth-authorization-server/api`
+- Protected resource metadata: `http://localhost:8000/.well-known/oauth-protected-resource/api/mcp`
+
+Both discovery endpoints use path-aware URLs per RFC 8414 and RFC 9728, matching the `base_url` path.
+
+### Complete Example
+
+Here's a complete working example showing all the pieces together:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.github import GitHubProvider
+from starlette.applications import Starlette
+from starlette.routing import Mount
+import uvicorn
+
+# Define routing structure
+ROOT_URL = "http://localhost:8000"
+MOUNT_PREFIX = "/api"
+MCP_PATH = "/mcp"
+
+# Create OAuth provider
+auth = GitHubProvider(
+ client_id="your-client-id",
+ client_secret="your-client-secret",
+ base_url=f"{ROOT_URL}{MOUNT_PREFIX}",
+ # issuer_url defaults to base_url - path-aware discovery works automatically
+)
+
+# Create MCP server
+mcp = FastMCP("Protected Server", auth=auth)
+
+@mcp.tool
+def analyze(data: str) -> dict:
+ return {"result": f"Analyzed: {data}"}
+
+# Create MCP app
+mcp_app = mcp.http_app(path=MCP_PATH)
+
+# Get discovery routes for root level
+well_known_routes = auth.get_well_known_routes(mcp_path=MCP_PATH)
+
+# Assemble the application
+app = Starlette(
+ routes=[
+ *well_known_routes,
+ Mount(MOUNT_PREFIX, app=mcp_app),
+ ],
+ lifespan=mcp_app.lifespan,
+)
+
+if __name__ == "__main__":
+ uvicorn.run(app, host="0.0.0.0", port=8000)
+```
+
+For more details on OAuth authentication, see the [Authentication guide](/servers/auth/authentication).
+
+## Production Deployment
+
+### Running with Uvicorn
+
+When deploying to production, you'll want to optimize your server for performance and reliability. Uvicorn provides several options to improve your server's capabilities:
+
+```bash
+# Run with basic configuration
+uvicorn app:app --host 0.0.0.0 --port 8000
+
+# Run with multiple workers for production (requires stateless mode - see below)
+uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4
+```
+
+### Horizontal Scaling
+
+
+
+When deploying FastMCP behind a load balancer or running multiple server instances, you need to understand how the HTTP transport handles sessions and configure your server appropriately.
+
+#### Understanding Sessions
+
+By default, FastMCP's Streamable HTTP transport maintains server-side sessions. Sessions enable stateful MCP features like [elicitation](/servers/elicitation) and [sampling](/servers/sampling), where the server needs to maintain context across multiple requests from the same client.
+
+This works perfectly for single-instance deployments. However, sessions are stored in memory on each server instance, which creates challenges when scaling horizontally.
+
+#### Without Stateless Mode
+
+When running multiple server instances behind a load balancer (Traefik, nginx, HAProxy, Kubernetes, etc.), requests from the same client may be routed to different instances:
+
+1. Client connects to Instance A → session created on Instance A
+2. Next request routes to Instance B → session doesn't exist → **request fails**
+
+You might expect sticky sessions (session affinity) to solve this, but they don't work reliably with MCP clients.
+
+
+**Why sticky sessions don't work:** Most MCP clients—including Cursor and Claude Code—use `fetch()` internally and don't properly forward `Set-Cookie` headers. Without cookies, load balancers can't identify which instance should handle subsequent requests. This is a limitation in how these clients implement HTTP, not something you can fix with load balancer configuration.
+
+
+#### Enabling Stateless Mode
+
+For horizontally scaled deployments, enable stateless HTTP mode. In stateless mode, each request creates a fresh transport context, eliminating the need for session affinity entirely.
+
+**Option 1: Via `http_app()`**
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("My Server")
+
+@mcp.tool
+def process(data: str) -> str:
+ return f"Processed: {data}"
+
+app = mcp.http_app(stateless_http=True)
+```
+
+**Option 2: Via `run()`**
+
+```python
+if __name__ == "__main__":
+ mcp.run(transport="http", stateless_http=True)
+```
+
+**Option 3: Via environment variable**
+
+```bash
+FASTMCP_STATELESS_HTTP=true uvicorn app:app --host 0.0.0.0 --port 8000 --workers 4
+```
+
+### Environment Variables
+
+Production deployments should never hardcode sensitive information like API keys or authentication tokens. Instead, use environment variables to configure your server at runtime. This keeps your code secure and makes it easy to deploy the same code to different environments with different configurations.
+
+Here's an example using static token authentication for development (OAuth is recommended for production):
+
+```python
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth import StaticTokenVerifier
+
+# Read configuration from environment
+auth_token = os.environ.get("MCP_AUTH_TOKEN")
+if auth_token:
+ auth = StaticTokenVerifier(tokens={auth_token: {"sub": "admin", "client_id": "cli"}})
+ mcp = FastMCP("Production Server", auth=auth)
+else:
+ mcp = FastMCP("Production Server")
+
+app = mcp.http_app()
+```
+
+Deploy with your secrets safely stored in environment variables:
+```bash
+MCP_AUTH_TOKEN=secret uvicorn app:app --host 0.0.0.0 --port 8000
+```
+
+### OAuth Token Security
+
+
+
+If you're using the [OAuth Proxy](/servers/auth/oauth-proxy), FastMCP issues its own JWT tokens to clients instead of forwarding upstream provider tokens. This maintains proper OAuth 2.0 token boundaries.
+
+**Default Behavior (Development Only):**
+
+By default, FastMCP automatically manages cryptographic keys:
+- **Mac/Windows**: Keys are generated and stored in your system keyring, surviving server restarts. Suitable **only** for development and local testing.
+- **Linux**: Keys are ephemeral (random salt at startup), so tokens are invalidated on restart.
+
+This automatic approach is convenient for development but not suitable for production deployments.
+
+**For Production:**
+
+Production requires explicit key management to ensure tokens survive restarts and can be shared across multiple server instances. This requires the following two things working together:
+
+1. **Explicit JWT signing key** for signing tokens issued to clients
+3. **Persistent network-accessible storage** for upstream tokens (wrapped in `FernetEncryptionWrapper` to encrypt sensitive data at rest)
+
+**Configuration:**
+
+Add two parameters to your auth provider:
+
+```python {8-12}
+from key_value.aio.stores.redis import RedisStore
+from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
+from cryptography.fernet import Fernet
+
+auth = GitHubProvider(
+ client_id=os.environ["GITHUB_CLIENT_ID"],
+ client_secret=os.environ["GITHUB_CLIENT_SECRET"],
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ client_storage=FernetEncryptionWrapper(
+ key_value=RedisStore(host="redis.example.com", port=6379),
+ fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
+ ),
+ base_url="https://your-server.com" # use HTTPS
+)
+```
+
+Both parameters are required for production. Without an explicit signing key, keys are signed using a key derived from the client_secret, which will cause invalidation upon rotation of the client secret. Without persistent storage, tokens are local to the server and won't be trusted across hosts. **Wrap your storage backend in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without encryption, tokens are stored in plaintext.
+
+For more details on the token architecture and key management, see [OAuth Proxy Key and Storage Management](/servers/auth/oauth-proxy#key-and-storage-management).
+
+## Reverse Proxy (nginx)
+
+In production, you'll typically run your FastMCP server behind a reverse proxy like nginx. A reverse proxy provides TLS termination, domain-based routing, static file serving, and an additional layer of security between the internet and your application.
+
+### Running FastMCP as a Linux Service
+
+Before configuring nginx, you need your FastMCP server running as a background service. A systemd unit file ensures your server starts automatically and restarts on failure.
+
+Create a file at `/etc/systemd/system/fastmcp.service`:
+
+```ini
+[Unit]
+Description=FastMCP Server
+After=network.target
+
+[Service]
+User=www-data
+Group=www-data
+WorkingDirectory=/opt/fastmcp
+ExecStart=/opt/fastmcp/.venv/bin/uvicorn app:app --host 127.0.0.1 --port 8000
+Restart=always
+RestartSec=5
+Environment="PATH=/opt/fastmcp/.venv/bin"
+
+[Install]
+WantedBy=multi-user.target
+```
+
+Enable and start the service:
+
+```bash
+sudo systemctl daemon-reload
+sudo systemctl enable fastmcp
+sudo systemctl start fastmcp
+```
+
+This assumes your ASGI application is in `/opt/fastmcp/app.py` with a virtual environment at `/opt/fastmcp/.venv`. Adjust paths to match your deployment layout.
+
+### nginx Configuration
+
+FastMCP's Streamable HTTP transport uses Server-Sent Events (SSE) for streaming responses. This requires specific nginx settings to prevent buffering from breaking the event stream.
+
+Create a site configuration at `/etc/nginx/sites-available/fastmcp`:
+
+```nginx
+server {
+ listen 80;
+ server_name mcp.example.com;
+
+ # Redirect HTTP to HTTPS
+ return 301 https://$host$request_uri;
+}
+
+server {
+ listen 443 ssl;
+ server_name mcp.example.com;
+
+ ssl_certificate /etc/letsencrypt/live/mcp.example.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/mcp.example.com/privkey.pem;
+
+ location / {
+ proxy_pass http://127.0.0.1:8000;
+ proxy_http_version 1.1;
+ proxy_set_header Connection '';
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # Required for SSE (Server-Sent Events) streaming
+ proxy_buffering off;
+ proxy_cache off;
+
+ # Allow long-lived connections for streaming responses
+ proxy_read_timeout 300s;
+ proxy_send_timeout 300s;
+ }
+}
+```
+
+Enable the site and reload nginx:
+
+```bash
+sudo ln -s /etc/nginx/sites-available/fastmcp /etc/nginx/sites-enabled/
+sudo nginx -t
+sudo systemctl reload nginx
+```
+
+Your FastMCP server is now accessible at `https://mcp.example.com/mcp`.
+
+
+**SSE buffering is the most common issue.** If clients connect but never receive streaming responses (progress updates, tool results), verify that `proxy_buffering off` is set. Without it, nginx buffers the entire SSE stream and delivers it only when the connection closes, which breaks real-time communication.
+
+
+### Key Considerations
+
+When deploying FastMCP behind a reverse proxy, keep these points in mind:
+
+- **Disable buffering**: SSE requires `proxy_buffering off` so events reach clients immediately. This is the single most important setting.
+- **Increase timeouts**: The default nginx `proxy_read_timeout` is 60 seconds. Long-running MCP tools will cause the connection to drop. Set timeouts to at least 300 seconds, or higher if your tools run longer. For tools that may exceed any timeout, use [SSE Polling](#sse-polling-for-long-running-operations) to gracefully handle proxy disconnections.
+- **Use HTTP/1.1**: Set `proxy_http_version 1.1` and `proxy_set_header Connection ''` to enable keep-alive connections between nginx and your server. Clearing the `Connection` header prevents clients from sending `Connection: close` to your upstream, which would break SSE streams. Both settings are required for proper SSE support.
+- **Forward headers**: Pass `X-Forwarded-For` and `X-Forwarded-Proto` so your FastMCP server can determine the real client IP and protocol. This is important for logging and for OAuth redirect URLs.
+- **TLS termination**: Let nginx handle TLS certificates (e.g., via Let's Encrypt with Certbot). Your FastMCP server can then run on plain HTTP internally.
+
+### Mounting Under a Path Prefix
+
+If you want your MCP server available at a subpath like `https://example.com/api/mcp` instead of at the root domain, adjust the nginx `location` block:
+
+```nginx
+location /api/ {
+ proxy_pass http://127.0.0.1:8000/;
+ proxy_http_version 1.1;
+ proxy_set_header Connection '';
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # Required for SSE streaming
+ proxy_buffering off;
+ proxy_cache off;
+ proxy_read_timeout 300s;
+ proxy_send_timeout 300s;
+}
+```
+
+Note the trailing `/` on both `location /api/` and `proxy_pass http://127.0.0.1:8000/` — this ensures nginx strips the `/api` prefix before forwarding to your server. If you're using OAuth authentication with a mount prefix, see [Mounting Authenticated Servers](#mounting-authenticated-servers) for additional configuration.
+
+## Testing Your Deployment
+
+Once your server is deployed, you'll need to verify it's accessible and functioning correctly. For comprehensive testing strategies including connectivity tests, client testing, and authentication testing, see the [Testing Your Server](/development/tests) guide.
+
+## Hosting Your Server
+
+This guide has shown you how to create an HTTP-accessible MCP server, but you'll still need a hosting provider to make it available on the internet. Your FastMCP server can run anywhere that supports Python web applications:
+
+- **Cloud VMs** (AWS EC2, Google Compute Engine, Azure VMs)
+- **Container platforms** (Cloud Run, Container Instances, ECS)
+- **Platform-as-a-Service** (Railway, Render, Vercel)
+- **Edge platforms** (Cloudflare Workers)
+- **Kubernetes clusters** (self-managed or managed)
+
+The key requirements are Python 3.10+ support and the ability to expose an HTTP port. Most providers will require you to package your server (requirements.txt, Dockerfile, etc.) according to their deployment format. For managed, zero-configuration deployment, see [Prefect Horizon](/deployment/prefect-horizon).
diff --git a/docs/v3/deployment/prefect-horizon.mdx b/docs/v3/deployment/prefect-horizon.mdx
new file mode 100644
index 000000000..6a26fa19e
--- /dev/null
+++ b/docs/v3/deployment/prefect-horizon.mdx
@@ -0,0 +1,120 @@
+---
+title: Prefect Horizon
+sidebarTitle: Prefect Horizon
+description: The MCP platform from the FastMCP team
+icon: cloud
+---
+
+[Prefect Horizon](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_horizon&utm_content=v3_guide_intro) is a platform for deploying and managing MCP servers. Built by the FastMCP team at [Prefect](https://www.prefect.io), Horizon provides managed hosting, authentication, access control, and a registry of MCP capabilities.
+
+Horizon includes a **free personal tier for FastMCP users**, making it the fastest way to get a secure, production-ready server URL with built-in OAuth authentication.
+
+
+Horizon is free for personal projects. Enterprise governance features are available for teams deploying to thousands of users.
+
+
+## The Platform
+
+Horizon is organized into four integrated pillars:
+
+- **Deploy**: Managed hosting with CI/CD, scaling, monitoring, and rollbacks. Push code and get a live, governed endpoint in 60 seconds.
+- **Registry**: A central catalog of MCP servers across your organization—first-party, third-party, and curated remix servers composed from multiple sources.
+- **Gateway**: Role-based access control, authentication, and audit logs. Define what agents can see and do at the tool level.
+- **Agents**: A permissioned chat interface for interacting with any MCP server or curated combination of servers.
+
+This guide focuses on **Horizon Deploy**, the managed hosting layer that gives you the fastest path from a FastMCP server to a production URL.
+
+## Prerequisites
+
+To use Horizon, you'll need a [GitHub](https://github.com) account and a GitHub repo containing a FastMCP server. If you don't have one yet, Horizon can create a starter repo for you during onboarding.
+
+Your repo can be public or private, but must include at least a Python file containing a FastMCP server instance.
+
+
+To verify your file is compatible with Horizon, run `fastmcp inspect ` to see what Horizon will see when it runs your server.
+
+
+If you have a `requirements.txt` or `pyproject.toml` in the repo, Horizon will automatically detect your server's dependencies and install them. Your file *can* have an `if __name__ == "__main__"` block, but it will be ignored by Horizon.
+
+For example, a minimal server file might look like:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("MyServer")
+
+@mcp.tool
+def hello(name: str) -> str:
+ return f"Hello, {name}!"
+```
+
+## Getting Started
+
+There are just three steps to deploying a server to Horizon:
+
+### Step 1: Select a Repository
+
+Visit [horizon.prefect.io](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) and sign in with your GitHub account. Connect your GitHub account to grant Horizon access to your repositories, then select the repo you want to deploy.
+
+
+
+### Step 2: Configure Your Server
+
+Next, you'll configure how Horizon should build and deploy your server.
+
+
+
+The configuration screen lets you specify:
+- **Server name**: A unique name for your server. This determines your server's URL.
+- **Description**: A brief description of what your server does.
+- **Entrypoint**: The Python file containing your FastMCP server (e.g., `main.py`). This field has the same syntax as the `fastmcp run` command—use `main.py:mcp` to specify a specific object in the file.
+- **Authentication**: When enabled, only authenticated users in your organization can connect. Horizon handles all the OAuth complexity for you.
+
+Horizon will automatically detect your server's Python dependencies from either a `requirements.txt` or `pyproject.toml` file.
+
+### Step 3: Deploy and Connect
+
+Click **Deploy Server** and Horizon will clone your repository, build your server, and deploy it to a unique URL—typically in under 60 seconds.
+
+
+
+Once deployed, your server is accessible at a URL like:
+
+```
+https://your-server-name.fastmcp.app/mcp
+```
+
+Horizon monitors your repo and redeploys automatically whenever you push to `main`. It also builds preview deployments for every PR, so you can test changes before they go live.
+
+## Testing Your Server
+
+Horizon provides two ways to verify your server is working before connecting external clients.
+
+### Inspector
+
+The Inspector gives you a structured view of everything your server exposes—tools, resources, and prompts. You can click any tool, fill in the inputs, execute it, and see the output. This is useful for systematically validating each capability and debugging specific behaviors.
+
+### ChatMCP
+
+For quick end-to-end testing, ChatMCP lets you interact with your server conversationally. It uses a fast model optimized for rapid iteration—you can verify the server works, test tool calls in context, and confirm the overall behavior before sharing it with others.
+
+
+
+ChatMCP is designed for testing, not as a daily work environment. Once you've confirmed your server works, you can copy connection snippets for Claude Desktop, Cursor, Claude Code, and other MCP clients—or use the FastMCP client library to connect programmatically.
+
+## Horizon Agents
+
+Beyond testing individual servers, Horizon lets you create **Agents**—chat interfaces backed by one or more MCP servers. While ChatMCP tests a single server, Agents let you compose capabilities from multiple servers into a unified experience.
+
+
+
+To create an agent:
+1. Navigate to **Agents** in the sidebar
+2. Click **Create Agent** and give it a name and description
+3. Add MCP servers to the agent—these can be servers you've deployed to Horizon or external servers in the registry
+
+Once configured, you can chat with your agent directly in Horizon:
+
+
+
+Agents are useful for creating purpose-built interfaces that combine tools from different servers. For example, you might create an agent that has access to both your company's internal data server and a general-purpose utilities server.
diff --git a/docs/v3/deployment/running-server.mdx b/docs/v3/deployment/running-server.mdx
new file mode 100644
index 000000000..c10855345
--- /dev/null
+++ b/docs/v3/deployment/running-server.mdx
@@ -0,0 +1,286 @@
+---
+title: Running Your Server
+sidebarTitle: Running Your Server
+description: Learn how to run your FastMCP server locally for development and testing
+icon: circle-play
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+FastMCP servers can be run in different ways depending on your needs. This guide focuses on running servers locally for development and testing. For production deployment to a URL, see the [HTTP Deployment](/deployment/http) guide.
+
+## The `run()` Method
+
+Every FastMCP server needs to be started to accept connections. The simplest way to run a server is by calling the `run()` method on your FastMCP instance. This method starts the server and blocks until it's stopped, handling all the connection management for you.
+
+
+For maximum compatibility, it's best practice to place the `run()` call within an `if __name__ == "__main__":` block. This ensures the server starts only when the script is executed directly, not when imported as a module.
+
+
+```python {9-10} my_server.py
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="MyServer")
+
+@mcp.tool
+def hello(name: str) -> str:
+ return f"Hello, {name}!"
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+You can now run this MCP server by executing `python my_server.py`.
+
+## Transport Protocols
+
+MCP servers communicate with clients through different transport protocols. Think of transports as the "language" your server speaks to communicate with clients. FastMCP supports three main transport protocols, each designed for specific use cases and deployment scenarios.
+
+The choice of transport determines how clients connect to your server, what network capabilities are available, and how many clients can connect simultaneously. Understanding these transports helps you choose the right approach for your application.
+
+### STDIO Transport (Default)
+
+STDIO (Standard Input/Output) is the default transport for FastMCP servers. When you call `run()` without arguments, your server uses STDIO transport. This transport communicates through standard input and output streams, making it perfect for command-line tools and desktop applications like Claude Desktop.
+
+With STDIO transport, the client spawns a new server process for each session and manages its lifecycle. The server reads MCP messages from stdin and writes responses to stdout. This is why STDIO servers don't stay running - they're started on-demand by the client.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("MyServer")
+
+@mcp.tool
+def hello(name: str) -> str:
+ return f"Hello, {name}!"
+
+if __name__ == "__main__":
+ mcp.run() # Uses STDIO transport by default
+```
+
+STDIO is ideal for:
+- Local development and testing
+- Claude Desktop integration
+- Command-line tools
+- Single-user applications
+
+### HTTP Transport (Streamable)
+
+HTTP transport turns your MCP server into a web service accessible via a URL. This transport uses the Streamable HTTP protocol, which allows clients to connect over the network. Unlike STDIO where each client gets its own process, an HTTP server can handle multiple clients simultaneously.
+
+The Streamable HTTP protocol provides full bidirectional communication between client and server, supporting all MCP operations including streaming responses. This makes it the recommended choice for network-based deployments.
+
+To use HTTP transport, specify it in the `run()` method along with networking options:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("MyServer")
+
+@mcp.tool
+def hello(name: str) -> str:
+ return f"Hello, {name}!"
+
+if __name__ == "__main__":
+ # Start an HTTP server on port 8000
+ mcp.run(transport="http", host="127.0.0.1", port=8000)
+```
+
+Your server is now accessible at `http://localhost:8000/mcp`. This URL is the MCP endpoint that clients will connect to. HTTP transport enables:
+- Network accessibility
+- Multiple concurrent clients
+- Integration with web infrastructure
+- Remote deployment capabilities
+
+For production HTTP deployment with authentication and advanced configuration, see the [HTTP Deployment](/deployment/http) guide.
+
+### SSE Transport (Legacy)
+
+Server-Sent Events (SSE) transport was the original HTTP-based transport for MCP. While still supported for backward compatibility, it has limitations compared to the newer Streamable HTTP transport. SSE only supports server-to-client streaming, making it less efficient for bidirectional communication.
+
+```python
+if __name__ == "__main__":
+ # SSE transport - use HTTP instead for new projects
+ mcp.run(transport="sse", host="127.0.0.1", port=8000)
+```
+
+We recommend using HTTP transport instead of SSE for all new projects. SSE remains available only for compatibility with older clients that haven't upgraded to Streamable HTTP.
+
+### Choosing the Right Transport
+
+Each transport serves different needs. STDIO is perfect when you need simple, local execution - it's what Claude Desktop and most command-line tools expect. HTTP transport is essential when you need network access, want to serve multiple clients, or plan to deploy your server remotely. SSE exists only for backward compatibility and shouldn't be used in new projects.
+
+Consider your deployment scenario: Are you building a tool for local use? STDIO is your best choice. Need a centralized service that multiple clients can access? HTTP transport is the way to go.
+
+## The FastMCP CLI
+
+FastMCP provides a powerful command-line interface for running servers without modifying the source code. The CLI can automatically find and run your server with different transports, manage dependencies, and handle development workflows:
+
+```bash
+fastmcp run server.py
+```
+
+The CLI automatically finds a FastMCP instance in your file (named `mcp`, `server`, or `app`) and runs it with the specified options. This is particularly useful for testing different transports or configurations without changing your code.
+
+### Dependency Management
+
+The CLI integrates with `uv` to manage Python environments and dependencies:
+
+```bash
+# Run with a specific Python version
+fastmcp run server.py --python 3.11
+
+# Run with additional packages
+fastmcp run server.py --with pandas --with numpy
+
+# Run with dependencies from a requirements file
+fastmcp run server.py --with-requirements requirements.txt
+
+# Combine multiple options
+fastmcp run server.py --python 3.10 --with httpx --transport http
+
+# Run within a specific project directory
+fastmcp run server.py --project /path/to/project
+```
+
+
+When using `--python`, `--with`, `--project`, or `--with-requirements`, the server runs via `uv run` subprocess instead of using your local environment.
+
+
+### Passing Arguments to Servers
+
+When servers accept command line arguments (using argparse, click, or other libraries), you can pass them after `--`:
+
+```bash
+fastmcp run config_server.py -- --config config.json
+fastmcp run database_server.py -- --database-path /tmp/db.sqlite --debug
+```
+
+This is useful for servers that need configuration files, database paths, API keys, or other runtime options.
+
+For more CLI features including development mode with the MCP Inspector, see the [CLI documentation](/cli/running).
+
+### Auto-Reload for Development
+
+
+
+During development, you can use the `--reload` flag to automatically restart your server when source files change:
+
+```bash
+fastmcp run server.py --reload
+```
+
+The server watches for changes to Python files in the current directory and restarts automatically when you save changes. This provides a fast feedback loop during development without manually stopping and starting the server.
+
+```bash
+# Watch specific directories for changes
+fastmcp run server.py --reload --reload-dir ./src --reload-dir ./lib
+
+# Combine with other options
+fastmcp run server.py --reload --transport http --port 8080
+```
+
+
+Auto-reload uses stateless mode to enable seamless restarts. For stdio transport, this is fully featured. For HTTP transport, some bidirectional features like elicitation are not available during reload mode.
+
+
+SSE transport does not support auto-reload due to session limitations. Use HTTP transport instead if you need both network access and auto-reload.
+
+### Async Usage
+
+FastMCP servers are built on async Python, but the framework provides both synchronous and asynchronous APIs to fit your application's needs. The `run()` method we've been using is actually a synchronous wrapper around the async server implementation.
+
+For applications that are already running in an async context, FastMCP provides the `run_async()` method:
+
+```python {10-12}
+from fastmcp import FastMCP
+import asyncio
+
+mcp = FastMCP(name="MyServer")
+
+@mcp.tool
+def hello(name: str) -> str:
+ return f"Hello, {name}!"
+
+async def main():
+ # Use run_async() in async contexts
+ await mcp.run_async(transport="http", port=8000)
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+
+The `run()` method cannot be called from inside an async function because it creates its own async event loop internally. If you attempt to call `run()` from inside an async function, you'll get an error about the event loop already running.
+
+Always use `run_async()` inside async functions and `run()` in synchronous contexts.
+
+
+Both `run()` and `run_async()` accept the same transport arguments, so all the examples above apply to both methods.
+
+## Custom Routes
+
+When using HTTP transport, you might want to add custom web endpoints alongside your MCP server. This is useful for health checks, status pages, or simple APIs. FastMCP lets you add custom routes using the `@custom_route` decorator:
+
+```python
+from fastmcp import FastMCP
+from starlette.requests import Request
+from starlette.responses import PlainTextResponse
+
+mcp = FastMCP("MyServer")
+
+@mcp.custom_route("/health", methods=["GET"])
+async def health_check(request: Request) -> PlainTextResponse:
+ return PlainTextResponse("OK")
+
+@mcp.tool
+def process(data: str) -> str:
+ return f"Processed: {data}"
+
+if __name__ == "__main__":
+ mcp.run(transport="http") # Health check at http://localhost:8000/health
+```
+
+Custom routes are served by the same web server as your MCP endpoint. They're available at the root of your domain while the MCP endpoint is at `/mcp/`. For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/deployment/http#integration-with-web-frameworks).
+
+## Alternative Initialization Patterns
+
+The `if __name__ == "__main__"` pattern works well for standalone scripts, but some deployment scenarios require different approaches. FastMCP handles these cases automatically.
+
+### CLI-Only Servers
+
+When using the FastMCP CLI, you don't need the `if __name__` block at all. The CLI will find your FastMCP instance and run it:
+
+```python
+# server.py
+from fastmcp import FastMCP
+
+mcp = FastMCP("MyServer") # CLI looks for 'mcp', 'server', or 'app'
+
+@mcp.tool
+def process(data: str) -> str:
+ return f"Processed: {data}"
+
+# No if __name__ block needed - CLI will find and run 'mcp'
+```
+
+### ASGI Applications
+
+For ASGI deployment (running with Uvicorn or similar), you'll want to create an ASGI application object. This approach is common in production deployments where you need more control over the server configuration:
+
+```python
+# app.py
+from fastmcp import FastMCP
+
+def create_app():
+ mcp = FastMCP("MyServer")
+
+ @mcp.tool
+ def process(data: str) -> str:
+ return f"Processed: {data}"
+
+ return mcp.http_app()
+
+app = create_app() # Uvicorn will use this
+```
+
+See the [HTTP Deployment](/deployment/http) guide for more ASGI deployment patterns.
\ No newline at end of file
diff --git a/docs/v3/deployment/sandboxed-agents.mdx b/docs/v3/deployment/sandboxed-agents.mdx
new file mode 100644
index 000000000..16191affb
--- /dev/null
+++ b/docs/v3/deployment/sandboxed-agents.mdx
@@ -0,0 +1,262 @@
+---
+title: Sandboxed Agents
+sidebarTitle: Sandboxed Agents
+description: Expose MCP tools to isolated agents without giving the sandbox long-lived credentials.
+icon: box-open
+---
+
+This guide is for deployments where an agent runs inside an isolated container, subprocess, or remote worker and still needs MCP access. In that setup, the sandbox itself becomes part of your trust boundary.
+
+The core recommendation is simple: use FastMCP as the capability boundary. Run a remote FastMCP server, authenticate the sandbox with short-lived scoped credentials, and keep privileged credentials on the server side.
+
+## When to Use This Pattern
+
+This pattern is useful when:
+
+- your agent runs in an ephemeral container or subprocess
+- you do not want long-lived credentials inside that sandbox
+- you need per-run, per-tenant, or per-job scoping
+- the sandbox must call internal APIs, databases, or upstream MCP servers indirectly
+
+If you are building a local desktop integration, STDIO and normal local configuration may be enough. This guide is for cases where the sandbox is isolated enough that secret distribution, credential lifetimes, and privilege boundaries become part of the design.
+
+## What Changes in a Sandboxed Deployment
+
+A desktop MCP client usually runs on a developer's machine and launches local servers with configuration the developer controls. A sandboxed agent is different:
+
+- It often runs in an ephemeral container or subprocess.
+- Its filesystem may be inspected after the fact.
+- Its environment variables may be broader than you intend.
+- You may launch many sandboxes concurrently for different users, tenants, or jobs.
+
+That means convenience patterns that are acceptable locally become risky in sandboxes. Passing a GitHub token, database password, or cloud credentials directly into the sandbox creates a secret distribution problem you do not need to have.
+
+The safer approach is to make your FastMCP server the only component with privileged access and let the sandbox call it over MCP.
+
+## Recommended Architecture
+
+Use this shape by default:
+
+```mermaid
+flowchart LR
+ A["Sandboxed agent"] -->|"short-lived token"| B["FastMCP server"]
+ B --> C["internal APIs"]
+ B --> D["databases"]
+ B --> E["other MCP servers"]
+```
+
+The sandbox gets:
+
+- the MCP server URL
+- a short-lived token scoped to its job, tenant, or run
+- no long-lived upstream credentials
+
+The FastMCP server does the privileged work:
+
+- verifies the sandbox token
+- authorizes the request from token claims, scopes, or other server-side policy
+- exposes only the tools that sandbox should see
+- talks to internal APIs, databases, or upstream MCP servers on the sandbox's behalf
+
+The key design rule is simple:
+
+
+Give the sandbox capabilities, not credentials.
+
+
+With that boundary in place, the next questions are how the sandbox connects, how the server verifies and authorizes it, and how you design the tools the sandbox is allowed to call.
+
+## Prefer HTTP for Sandboxed Agents
+
+For sandboxes, prefer a remote HTTP server over a local STDIO server.
+
+STDIO is still excellent for local development, but a remote HTTP server is usually the better production boundary for sandboxed agents because:
+
+- authentication is explicit
+- the server lifecycle is independent from the sandbox lifecycle
+- secrets stay on the server
+- one deployment can safely serve many sandboxes
+- auditing and revocation happen in one place
+
+This means the sandbox should connect as a client:
+
+```python
+from fastmcp import Client
+from fastmcp.client.auth import BearerAuth
+
+client = Client(
+ "https://sandbox-tools.example.com/mcp",
+ auth=BearerAuth("short-lived-sandbox-token"),
+)
+```
+
+And your FastMCP server should run remotely:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("Sandbox Tools")
+
+if __name__ == "__main__":
+ mcp.run(transport="http", host="0.0.0.0", port=8000)
+```
+
+For production transport setup, see [HTTP Deployment](/deployment/http).
+
+## Use Short-Lived, Scoped Credentials
+
+For sandboxed agents, it is usually cleaner to issue credentials for the sandbox session than to place long-lived upstream credentials directly inside the container.
+
+In practice, that usually means issuing a short-lived bearer token for each sandbox, run, or tenant and validating it on your FastMCP server with a token verifier.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+auth = JWTVerifier(
+ jwks_uri="https://auth.example.com/.well-known/jwks.json",
+ issuer="https://auth.example.com",
+ audience="sandbox-mcp",
+)
+
+mcp = FastMCP("Sandbox Tools", auth=auth)
+```
+
+The token should identify the sandbox's scope. Depending on your system, it may represent a job, a tenant, a run, or a user-authorized session. Useful claims often include:
+
+- sandbox or run id
+- tenant or installation id
+- user or actor id when applicable
+- expiration
+- optional capability scopes
+
+Avoid shared static tokens across many sandboxes. If one sandbox token leaks, you want the blast radius to be small and the lifetime to be short.
+
+Token verification is only one half of the boundary. Authorization still belongs on the FastMCP server: use scopes, claims, middleware, or custom auth checks to decide which tools and resources that sandbox can actually access.
+
+For example, you can verify the token globally and still require a narrower scope on a specific tool:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import require_scopes
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+auth = JWTVerifier(
+ jwks_uri="https://auth.example.com/.well-known/jwks.json",
+ issuer="https://auth.example.com",
+ audience="sandbox-mcp",
+)
+
+mcp = FastMCP("Sandbox Tools", auth=auth)
+
+@mcp.tool(auth=require_scopes("write:summary"))
+def write_summary(content: str) -> str:
+ return f"Stored summary with {len(content)} characters"
+```
+
+For validation patterns, see [Token Verification](/servers/auth/token-verification). For policy enforcement, see [Authorization](/servers/authorization).
+
+## Expose Capabilities, Not Raw Access
+
+The sandbox should not need:
+
+- GitHub app private keys
+- database passwords
+- upstream OAuth client secrets
+- cloud provider credentials
+
+Instead, expose MCP tools that perform privileged work on the server side.
+
+Good sandbox-facing tools tend to look like this:
+
+- `get_recent_updates`
+- `write_summary`
+- `fetch_repo_context`
+- `publish_review_comment`
+
+These tools describe the capability the sandbox needs, not the low-level credentialed action required to perform it.
+
+That distinction matters. A tool like `write_summary` lets the server decide where and how to persist the summary. A tool like `run_sql` or `call_internal_api` pushes privilege and policy into the sandbox where they are much harder to control.
+
+Sandboxed agents behave best when those tools are narrow and structured:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("Sandbox Tools")
+
+@mcp.tool
+def write_summary(content: str) -> str:
+ """Store the final summary for the current run."""
+ return f"Stored summary with {len(content)} characters"
+
+@mcp.tool
+def publish_review_comment(pr_number: int, body: str) -> str:
+ """Queue a review comment for a specific pull request."""
+ return f"Queued comment for PR #{pr_number}"
+```
+
+These are easier to audit, easier to authorize, and easier for agents to use reliably than a broad catch-all tool like `mutate_state(kind: str, payload: dict)`.
+
+Narrow tools also let you express different policies per tool instead of creating one large privileged escape hatch.
+
+## Use a Proxy When Upstream Systems Are More Privileged
+
+If the sandbox needs access to other MCP servers or internal systems, put FastMCP in front of them instead of forwarding secrets into the sandbox.
+
+This is where proxying becomes useful. Your public-facing FastMCP server can authenticate the sandbox, then forward allowed capabilities to upstream systems with stronger credentials.
+
+Typical examples:
+
+- a sandbox-safe MCP gateway in front of internal MCP servers
+- a FastMCP layer in front of internal HTTP APIs
+- a job-scoped server that fronts a Git provider, issue tracker, or storage system
+
+If the upstream system is itself an MCP server, FastMCP's proxy support is a natural fit. See [MCP Proxy](/servers/providers/proxy).
+
+## mcp.json for Sandboxed Clients
+
+If your sandboxed agent is configured through `mcp.json`, keep that configuration minimal. Point it at the remote FastMCP server and pass only the values the sandbox actually needs.
+
+```json
+{
+ "mcpServers": {
+ "sandbox-tools": {
+ "url": "https://sandbox-tools.example.com/mcp",
+ "transport": "http"
+ }
+ }
+}
+```
+
+In many systems, authentication is injected by the launcher or environment rather than hardcoded in `mcp.json`. That is usually the right tradeoff for sandboxes. Avoid baking long-lived credentials directly into generated config files, and avoid treating `mcp.json` as the place where secret material should live.
+
+That is all this section needs to do: tell the sandbox where the server lives. Keep auth and secret handling elsewhere.
+
+For configuration details, see [MCP.json](/integrations/mcp-json-configuration).
+
+## Common Mistakes
+
+The same few mistakes show up again and again in sandboxed deployments:
+
+- passing long-lived API keys directly into the sandbox
+- treating helper scripts in the sandbox as a security boundary
+- exposing broad mutation tools instead of narrow capabilities
+- using one shared token for every sandbox
+- relying on STDIO inheritance for configuration in production
+
+Each of these works at first. Each becomes painful once you have multiple tenants, multiple jobs, or an incident that requires revoking access quickly.
+
+## Production Checklist
+
+Before shipping a sandbox-facing FastMCP server, check these:
+
+- The sandbox connects over HTTP, not with privileged local credentials.
+- Tokens are short-lived and scoped to a run, tenant, or job.
+- The FastMCP server verifies tokens on every request.
+- Long-lived secrets stay on the server side.
+- Tools are narrow, explicit, and structured.
+- Upstream privileged systems sit behind the FastMCP server or proxy.
+- Revocation and audit live at the server boundary, not inside the sandbox.
+
+If you adopt those defaults, sandbox support stops being a special case and becomes a normal deployment pattern: isolated workers talk to a constrained FastMCP surface, and the server handles the privileged parts centrally.
diff --git a/docs/v3/deployment/server-configuration.mdx b/docs/v3/deployment/server-configuration.mdx
new file mode 100644
index 000000000..f9b0e4781
--- /dev/null
+++ b/docs/v3/deployment/server-configuration.mdx
@@ -0,0 +1,640 @@
+---
+title: "Project Configuration"
+sidebarTitle: "Project Configuration"
+description: Use fastmcp.json for portable, declarative project configuration
+icon: file-code
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+FastMCP supports declarative configuration through `fastmcp.json` files. This is the canonical and preferred way to configure FastMCP projects, providing a single source of truth for server settings, dependencies, and deployment options that replaces complex command-line arguments.
+
+The `fastmcp.json` file is designed to be a portable description of your server configuration that can be shared across environments and teams. When running from a `fastmcp.json` file, you can override any configuration values using CLI arguments.
+
+## Overview
+
+The `fastmcp.json` configuration file allows you to define all aspects of your FastMCP server in a structured, shareable format. Instead of remembering command-line arguments or writing shell scripts, you declare your server's configuration once and use it everywhere.
+
+When you have a `fastmcp.json` file, running your server becomes as simple as:
+
+```bash
+# Run the server using the configuration
+fastmcp run fastmcp.json
+
+# Or if fastmcp.json exists in the current directory
+fastmcp run
+```
+
+This configuration approach ensures reproducible deployments across different environments, from local development to production servers. It works seamlessly with Claude Desktop, VS Code extensions, and any MCP-compatible client.
+
+## File Structure
+
+The `fastmcp.json` configuration answers three fundamental questions about your server:
+
+- **Source** = WHERE does your server code live?
+- **Environment** = WHAT environment setup does it require?
+- **Deployment** = HOW should the server run?
+
+This conceptual model helps you understand the purpose of each configuration section and organize your settings effectively. The configuration file maps directly to these three concerns:
+
+```json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ // WHERE: Location of your server code
+ "type": "filesystem", // Optional, defaults to "filesystem"
+ "path": "server.py",
+ "entrypoint": "mcp"
+ },
+ "environment": {
+ // WHAT: Environment setup and dependencies
+ "type": "uv", // Optional, defaults to "uv"
+ "python": ">=3.10",
+ "dependencies": ["pandas", "numpy"]
+ },
+ "deployment": {
+ // HOW: Runtime configuration
+ "transport": "stdio",
+ "log_level": "INFO"
+ }
+}
+```
+
+Only the `source` field is required. The `environment` and `deployment` sections are optional and provide additional configuration when needed.
+
+### JSON Schema Support
+
+FastMCP provides JSON schemas for IDE autocomplete and validation. Add the schema reference to your `fastmcp.json` for enhanced developer experience:
+
+```json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py",
+ "entrypoint": "mcp"
+ }
+}
+```
+
+Two schema URLs are available:
+- **Version-specific**: `https://gofastmcp.com/public/schemas/fastmcp.json/v1.json`
+- **Latest version**: `https://gofastmcp.com/public/schemas/fastmcp.json/latest.json`
+
+Modern IDEs like VS Code will automatically provide autocomplete suggestions, validation, and inline documentation when the schema is specified.
+
+### Source Configuration
+
+The source configuration determines **WHERE** your server code lives. It tells FastMCP how to find and load your server, whether it's a local Python file, a remote repository, or hosted in the cloud. This section is required and forms the foundation of your configuration.
+
+
+
+ The server source configuration that determines where your server code lives.
+
+
+ The source type identifier that determines which implementation to use. Currently supports `"filesystem"` for local files. Future releases will add support for `"git"` and `"cloud"` source types.
+
+
+
+ When `type` is `"filesystem"` (or omitted), the source points to a local Python file containing your FastMCP server:
+
+
+ Path to the Python file containing your FastMCP server.
+
+
+
+ Name of the server instance or factory function within the module:
+ - Can be a FastMCP server instance (e.g., `mcp = FastMCP("MyServer")`)
+ - Can be a function with no arguments that returns a FastMCP server
+ - If not specified, FastMCP searches for common names: `mcp`, `server`, or `app`
+
+
+ **Example:**
+ ```json
+ "source": {
+ "type": "filesystem",
+ "path": "src/server.py",
+ "entrypoint": "mcp"
+ }
+ ```
+
+ Note: File paths are resolved relative to the configuration file's location.
+
+
+
+
+
+**Future Source Types**
+
+Future releases will support additional source types:
+- **Git repositories** (`type: "git"`) for loading server code directly from version control
+- **Prefect Horizon** (`type: "cloud"`) for hosted servers with automatic scaling and management
+
+
+### Environment Configuration
+
+The environment configuration determines **WHAT** environment setup your server requires. It controls the build-time setup of your Python environment, ensuring your server runs with the exact Python version and dependencies it requires. This section creates isolated, reproducible environments across different systems.
+
+FastMCP uses an extensible environment system with a base `Environment` class that can be implemented by different environment providers. Currently, FastMCP supports the `UVEnvironment` for Python environment management using `uv`'s powerful dependency resolver.
+
+
+
+ Optional environment configuration. When specified, FastMCP uses the appropriate environment implementation to set up your server's runtime.
+
+
+ The environment type identifier that determines which implementation to use. Currently supports `"uv"` for Python environments managed by uv. If omitted, defaults to `"uv"`.
+
+
+
+ When `type` is `"uv"` (or omitted), the environment uses uv to manage Python dependencies:
+
+
+ Python version constraint. Examples:
+ - Exact version: `"3.12"`
+ - Minimum version: `">=3.10"`
+ - Version range: `">=3.10,<3.13"`
+
+
+
+ List of pip packages with optional version specifiers (PEP 508 format).
+ ```json
+ "dependencies": ["pandas>=2.0", "requests", "httpx"]
+ ```
+
+
+
+ Path to a requirements.txt file, resolved relative to the config file location.
+ ```json
+ "requirements": "requirements.txt"
+ ```
+
+
+
+ Path to a project directory containing pyproject.toml for uv project management.
+ ```json
+ "project": "."
+ ```
+
+
+
+ List of paths to packages to install in editable/development mode. Useful for local development when you want changes to be reflected immediately. Supports multiple packages for monorepo setups or shared libraries.
+ ```json
+ "editable": ["."]
+ ```
+ Or with multiple packages:
+ ```json
+ "editable": [".", "../shared-lib", "/path/to/another-package"]
+ ```
+
+
+ **Example:**
+ ```json
+ "environment": {
+ "type": "uv",
+ "python": ">=3.10",
+ "dependencies": ["pandas", "numpy"],
+ "editable": ["."]
+ }
+ ```
+
+ Note: When any UVEnvironment field is specified, FastMCP automatically creates an isolated environment using `uv` before running your server.
+
+
+
+
+When environment configuration is provided, FastMCP:
+1. Detects the environment type (defaults to `"uv"` if not specified)
+2. Creates an isolated environment using the appropriate provider
+3. Installs the specified dependencies
+4. Runs your server in this clean environment
+
+This build-time setup ensures your server always has the dependencies it needs, without polluting your system Python or conflicting with other projects.
+
+
+**Future Environment Types**
+
+Similar to source types, future releases may support additional environment types for different runtime requirements, such as Docker containers or language-specific environments beyond Python.
+
+
+### Deployment Configuration
+
+The deployment configuration controls **HOW** your server runs. It defines the runtime behavior including network settings, environment variables, and execution context. These settings determine how your server operates when it executes, from transport protocols to logging levels.
+
+Environment variables are included in this section because they're runtime configuration that affects how your server behaves when it executes, not how its environment is built. The deployment configuration is applied every time your server starts, controlling its operational characteristics.
+
+
+
+ Optional runtime configuration for the server.
+
+
+
+ Protocol for client communication:
+ - `"stdio"`: Standard input/output for desktop clients
+ - `"http"`: Network-accessible HTTP server
+ - `"sse"`: Server-sent events
+
+
+
+ Network interface to bind (HTTP transport only):
+ - `"127.0.0.1"`: Local connections only
+ - `"0.0.0.0"`: All network interfaces
+
+
+
+ Port number for HTTP transport.
+
+
+
+ URL path for the MCP endpoint when using HTTP transport.
+
+
+
+ Server logging verbosity. Options:
+ - `"DEBUG"`: Detailed debugging information
+ - `"INFO"`: General informational messages
+ - `"WARNING"`: Warning messages
+ - `"ERROR"`: Error messages only
+ - `"CRITICAL"`: Critical errors only
+
+
+
+ Environment variables to set when running the server. Supports `${VAR_NAME}` syntax for runtime interpolation.
+ ```json
+ "env": {
+ "API_KEY": "secret-key",
+ "DATABASE_URL": "postgres://${DB_USER}@${DB_HOST}/mydb"
+ }
+ ```
+
+
+
+ Working directory for the server process. Relative paths are resolved from the config file location.
+
+
+
+ Command-line arguments to pass to the server, passed after `--` to the server's argument parser.
+ ```json
+ "args": ["--config", "server-config.json"]
+ ```
+
+
+
+
+
+#### Environment Variable Interpolation
+
+The `env` field in deployment configuration supports runtime interpolation of environment variables using `${VAR_NAME}` syntax. This enables dynamic configuration based on your deployment environment:
+
+```json
+{
+ "deployment": {
+ "env": {
+ "API_URL": "https://api.${ENVIRONMENT}.example.com",
+ "DATABASE_URL": "postgres://${DB_USER}:${DB_PASS}@${DB_HOST}/myapp",
+ "CACHE_KEY": "myapp_${ENVIRONMENT}_${VERSION}"
+ }
+ }
+}
+```
+
+When the server starts, FastMCP replaces `${ENVIRONMENT}`, `${DB_USER}`, etc. with values from your system's environment variables. If a variable doesn't exist, the placeholder is preserved as-is.
+
+**Example**: If your system has `ENVIRONMENT=production` and `DB_HOST=db.example.com`:
+```json
+// Configuration
+{
+ "deployment": {
+ "env": {
+ "API_URL": "https://api.${ENVIRONMENT}.example.com",
+ "DB_HOST": "${DB_HOST}"
+ }
+ }
+}
+
+// Result at runtime
+{
+ "API_URL": "https://api.production.example.com",
+ "DB_HOST": "db.example.com"
+}
+```
+
+This feature is particularly useful for:
+- Deploying the same configuration across development, staging, and production
+- Keeping sensitive values out of configuration files
+- Building dynamic URLs and connection strings
+- Creating environment-specific prefixes or suffixes
+
+## Usage with CLI Commands
+
+FastMCP automatically detects and uses a file specifically named `fastmcp.json` in the current directory, making server execution simple and consistent. Files with FastMCP configuration format but different names are not auto-detected and must be specified explicitly:
+
+```bash
+# Auto-detect fastmcp.json in current directory
+cd my-project
+fastmcp run # No arguments needed!
+
+# Or specify a configuration file explicitly
+fastmcp run prod.fastmcp.json
+
+# Skip environment setup when already in a uv environment
+fastmcp run fastmcp.json --skip-env
+
+# Skip source preparation when source is already prepared
+fastmcp run fastmcp.json --skip-source
+
+# Skip both environment and source preparation
+fastmcp run fastmcp.json --skip-env --skip-source
+```
+
+### Pre-building Environments
+
+You can use `fastmcp project prepare` to create a persistent uv project with all dependencies pre-installed:
+
+```bash
+# Create a persistent environment
+fastmcp project prepare fastmcp.json --output-dir ./env
+
+# Use the pre-built environment to run the server
+fastmcp run fastmcp.json --project ./env
+```
+
+This pattern separates environment setup (slow) from server execution (fast), useful for deployment scenarios.
+
+### Using an Existing Environment
+
+By default, FastMCP creates an isolated environment with `uv` based on your configuration. When you already have a suitable Python environment, use the `--skip-env` flag to skip environment creation:
+
+```bash
+fastmcp run fastmcp.json --skip-env
+```
+
+**When you already have an environment:**
+- You're in an activated virtual environment with all dependencies installed
+- You're inside a Docker container with pre-installed dependencies
+- You're in a CI/CD pipeline that pre-builds the environment
+- You're using a system-wide installation with all required packages
+- You're in a uv-managed environment (prevents infinite recursion)
+
+This flag tells FastMCP: "I already have everything installed, just run the server."
+
+### Using an Existing Source
+
+When working with source types that require preparation (future support for git repositories or cloud sources), use the `--skip-source` flag when you already have the source code available:
+
+```bash
+fastmcp run fastmcp.json --skip-source
+```
+
+**When you already have the source:**
+- You've previously cloned a git repository and don't need to re-fetch
+- You have a cached copy of a cloud-hosted server
+- You're in a CI/CD pipeline where source checkout is a separate step
+- You're iterating locally on already-downloaded code
+
+This flag tells FastMCP: "I already have the source code, skip any download/clone steps."
+
+Note: For filesystem sources (local Python files), this flag has no effect since they don't require preparation.
+
+The configuration file works with all FastMCP commands:
+- **`run`** - Start the server in production mode
+- **`dev`** - Launch with the Inspector UI for development
+- **`inspect`** - View server capabilities and configuration
+- **`install`** - Install to Claude Desktop, Cursor, or other MCP clients
+
+When no file argument is provided, FastMCP searches the current directory for `fastmcp.json`. This means you can simply navigate to your project directory and run `fastmcp run` to start your server with all its configured settings.
+
+### CLI Override Behavior
+
+Command-line arguments take precedence over configuration file values, allowing ad-hoc adjustments without modifying the file:
+
+```bash
+# Config specifies port 3000, CLI overrides to 8080
+fastmcp run fastmcp.json --port 8080
+
+# Config specifies stdio, CLI overrides to HTTP
+fastmcp run fastmcp.json --transport http
+
+# Add extra dependencies not in config
+fastmcp run fastmcp.json --with requests --with httpx
+```
+
+This precedence order enables:
+- Quick testing of different settings
+- Environment-specific overrides in deployment scripts
+- Debugging with increased log levels
+- Temporary configuration changes
+
+### Custom Naming Patterns
+
+You can use different configuration files for different environments:
+
+- `fastmcp.json` - Default configuration
+- `dev.fastmcp.json` - Development settings
+- `prod.fastmcp.json` - Production settings
+- `test_fastmcp.json` - Test configuration
+
+Any file with "fastmcp.json" in the name is recognized as a configuration file.
+
+## Examples
+
+
+
+
+A minimal configuration for a simple server:
+
+```json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py",
+ "entrypoint": "mcp"
+ }
+}
+```
+This configuration explicitly specifies the server entrypoint (`mcp`), making it clear which server instance or factory function to use. Uses all defaults: STDIO transport, no special dependencies, standard logging.
+
+
+
+A configuration optimized for local development:
+
+```json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ // WHERE does the server live?
+ "source": {
+ "path": "src/server.py",
+ "entrypoint": "app"
+ },
+ // WHAT dependencies does it need?
+ "environment": {
+ "type": "uv",
+ "python": "3.12",
+ "dependencies": ["fastmcp[dev]"],
+ "editable": "."
+ },
+ // HOW should it run?
+ "deployment": {
+ "transport": "http",
+ "host": "127.0.0.1",
+ "port": 8000,
+ "log_level": "DEBUG",
+ "env": {
+ "DEBUG": "true",
+ "ENV": "development"
+ }
+ }
+}
+```
+
+
+
+A production-ready configuration with full dependency management:
+
+```json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ // WHERE does the server live?
+ "source": {
+ "path": "app/main.py",
+ "entrypoint": "mcp_server"
+ },
+ // WHAT dependencies does it need?
+ "environment": {
+ "python": "3.11",
+ "requirements": "requirements/production.txt",
+ "project": "."
+ },
+ // HOW should it run?
+ "deployment": {
+ "transport": "http",
+ "host": "0.0.0.0",
+ "port": 3000,
+ "path": "/api/mcp/",
+ "log_level": "INFO",
+ "env": {
+ "ENV": "production",
+ "API_BASE_URL": "https://api.example.com",
+ "DATABASE_URL": "postgresql://user:pass@db.example.com/prod"
+ },
+ "cwd": "/app",
+ "args": ["--workers", "4"]
+ }
+}
+```
+
+
+
+Configuration for a data analysis server with scientific packages:
+
+```json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "analysis_server.py",
+ "entrypoint": "mcp"
+ },
+ "environment": {
+ "python": "3.11",
+ "dependencies": [
+ "pandas>=2.0",
+ "numpy",
+ "scikit-learn",
+ "matplotlib",
+ "jupyterlab"
+ ]
+ },
+ "deployment": {
+ "transport": "stdio",
+ "env": {
+ "MATPLOTLIB_BACKEND": "Agg",
+ "DATA_PATH": "./datasets"
+ }
+ }
+}
+```
+
+
+
+You can maintain multiple configuration files for different environments:
+
+**dev.fastmcp.json**:
+```json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py",
+ "entrypoint": "mcp"
+ },
+ "deployment": {
+ "transport": "http",
+ "log_level": "DEBUG"
+ }
+}
+```
+
+**prod.fastmcp.json**:
+```json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py",
+ "entrypoint": "mcp"
+ },
+ "environment": {
+ "requirements": "requirements/production.txt"
+ },
+ "deployment": {
+ "transport": "http",
+ "host": "0.0.0.0",
+ "log_level": "WARNING"
+ }
+}
+```
+
+Run different configurations:
+```bash
+fastmcp run dev.fastmcp.json # Development
+fastmcp run prod.fastmcp.json # Production
+```
+
+
+
+## Migrating from CLI Arguments
+
+If you're currently using command-line arguments or shell scripts, migrating to `fastmcp.json` simplifies your workflow. Here's how common CLI patterns map to configuration:
+
+**CLI Command**:
+```bash
+uv run --with pandas --with requests \
+ fastmcp run server.py \
+ --transport http \
+ --port 8000 \
+ --log-level INFO
+```
+
+**Equivalent fastmcp.json**:
+```json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py",
+ "entrypoint": "mcp"
+ },
+ "environment": {
+ "dependencies": ["pandas", "requests"]
+ },
+ "deployment": {
+ "transport": "http",
+ "port": 8000,
+ "log_level": "INFO"
+ }
+}
+```
+
+Now simply run:
+```bash
+fastmcp run # Automatically finds and uses fastmcp.json
+```
+
+The configuration file approach provides better documentation, easier sharing, and consistent execution across different environments while maintaining the flexibility to override settings when needed.
\ No newline at end of file
diff --git a/docs/v3/development/contributing.mdx b/docs/v3/development/contributing.mdx
new file mode 100644
index 000000000..c8772765a
--- /dev/null
+++ b/docs/v3/development/contributing.mdx
@@ -0,0 +1,198 @@
+---
+title: "Contributing"
+description: "Development workflow for FastMCP contributors"
+icon: code-pull-request
+---
+
+Contributing to FastMCP means joining a community that values clean, maintainable code and thoughtful API design. All contributions are valued - from fixing typos in documentation to implementing major features.
+
+## Design Principles
+
+Every contribution should advance these principles:
+
+- 🚀 **Fast** — High-level interfaces mean less code and faster development
+- 🍀 **Simple** — Minimal boilerplate; the obvious way should be the right way
+- 🐍 **Pythonic** — Feels natural to Python developers; no surprising patterns
+- 🔍 **Complete** — Everything needed for production: auth, testing, deployment, observability
+
+PRs are evaluated against these principles. Code that makes FastMCP slower, harder to reason about, less Pythonic, or less complete will be rejected.
+
+## Issues
+
+### Issue First, Code Second
+
+**Every pull request requires a corresponding issue - no exceptions.** This requirement creates a collaborative space where approach, scope, and alignment are established before code is written. Issues serve as design documents where maintainers and contributors discuss implementation strategy, identify potential conflicts with existing patterns, and ensure proposed changes advance FastMCP's vision.
+
+**FastMCP is an opinionated framework, not a kitchen sink.** The maintainers have strong beliefs about what FastMCP should and shouldn't do. Just because something takes N lines of code and you want it in fewer lines doesn't mean FastMCP should take on the maintenance burden or endorse that pattern. This is judged at the maintainers' discretion.
+
+Use issues to understand scope BEFORE opening PRs. The issue discussion determines whether a feature belongs in core, contrib, or not at all.
+
+### Writing Good Issues
+
+FastMCP is an extremely highly-trafficked repository maintained by a very small team. Issues that appear to transfer burden to maintainers without any effort to validate the problem will be closed. Please help the maintainers help you by always providing a minimal reproducible example and clearly describing the problem.
+
+**LLM-generated issues will be closed immediately.** Issues that contain paragraphs of unnecessary explanation, verbose problem descriptions, or obvious LLM authorship patterns obfuscate the actual problem and transfer burden to maintainers.
+
+Write clear, concise issues that:
+- State the problem directly
+- Provide a minimal reproducible example
+- Skip unnecessary background or context
+- Take responsibility for clear communication
+
+Issues may be labeled "Invalid" simply due to confusion caused by verbosity or not adhering to the guidelines outlined here.
+
+## Pull Requests
+
+PRs that deviate from FastMCP's core principles will be rejected regardless of implementation quality. **PRs are NOT for iterating on ideas** - they should only be opened for ideas that already have a bias toward acceptance based on issue discussion.
+
+
+### Development Environment
+
+#### Installation
+
+To contribute to FastMCP, you'll need to set up a development environment with all necessary tools and dependencies.
+
+```bash
+# Clone the repository
+git clone https://github.com/PrefectHQ/fastmcp.git
+cd fastmcp
+
+# Install all dependencies including dev tools
+uv sync
+
+# Install prek hooks
+uv run prek install
+```
+
+In addition, some development commands require [just](https://github.com/casey/just) to be installed.
+
+Prek hooks will run automatically on every commit to catch issues before they reach CI. If you see failures, fix them before committing - never commit broken code expecting to fix it later.
+
+### Development Standards
+
+#### Scope
+
+Large pull requests create review bottlenecks and quality risks. Unless you're fixing a discrete bug or making an incredibly well-scoped change, keep PRs small and focused.
+
+A PR that changes 50 lines across 3 files can be thoroughly reviewed in minutes. A PR that changes 500 lines across 20 files requires hours of careful analysis and often hides subtle issues.
+
+Breaking large features into smaller PRs:
+- Creates better review experiences
+- Makes git history clear
+- Simplifies debugging with bisect
+- Reduces merge conflicts
+- Gets your code merged faster
+
+#### Code Quality
+
+FastMCP values clarity over cleverness. Every line you write will be maintained by someone else - possibly years from now, possibly without context about your decisions.
+
+**PRs can be rejected for two opposing reasons:**
+1. **Insufficient quality** - Code that doesn't meet our standards for clarity, maintainability, or idiomaticity
+2. **Overengineering** - Code that is overbearing, unnecessarily complex, or tries to be too clever
+
+The focus is on idiomatic, high-quality Python. FastMCP uses patterns like `NotSet` type as an alternative to `None` in certain situations - follow existing patterns.
+
+#### Required Practices
+
+**Full type annotations** on all functions and methods. They catch bugs before runtime and serve as inline documentation.
+
+**Async/await patterns** for all I/O operations. Even if your specific use case doesn't need concurrency, consistency means users can compose features without worrying about blocking operations.
+
+**Descriptive names** make code self-documenting. `auth_token` is clear; `tok` requires mental translation.
+
+**Specific exception types** make error handling predictable. Catching `ValueError` tells readers exactly what error you expect. Never use bare `except` clauses.
+
+#### Anti-Patterns to Avoid
+
+**Complex one-liners** are hard to debug and modify. Break operations into clear steps.
+
+**Mutable default arguments** cause subtle bugs. Use `None` as the default and create the mutable object inside the function.
+
+**Breaking established patterns** confuses readers. If you must deviate, discuss in the issue first.
+
+### Prek Checks
+
+```bash
+# Runs automatically on commit, or manually:
+uv run prek run --all-files
+```
+
+This runs three critical tools:
+- **Ruff**: Linting and formatting
+- **Prettier**: Code formatting
+- **ty**: Static type checking
+
+Pytest runs separately as a distinct workflow step after prek checks pass. CI will reject PRs that fail these checks. Always run them locally first.
+
+### Testing
+
+Tests are documentation that shows how features work. Good tests give reviewers confidence and help future maintainers understand intent.
+
+```bash
+# Run specific test directory
+uv run pytest tests/server/ -v
+
+# Run all tests before submitting PR
+uv run pytest
+```
+
+Every new feature needs tests. See the [Testing Guide](/development/tests) for patterns and requirements.
+
+### Documentation
+
+A feature doesn't exist unless it's documented. Note that FastMCP's hosted documentation always tracks the main branch - users who want historical documentation can clone the repo, checkout a specific tag, and host it themselves.
+
+```bash
+# Preview documentation locally
+just docs
+```
+
+Documentation requirements:
+- **Explain concepts in prose first** - Code without context is just syntax
+- **Complete, runnable examples** - Every code block should be copy-pasteable
+- **Register in docs.json** - Makes pages appear in navigation
+- **Version badges** - Mark when features were added using ` `
+
+#### SDK Documentation
+
+FastMCP's SDK documentation is auto-generated from the source code docstrings and type annotations. It is automatically updated on every merge to main by a GitHub Actions workflow, so users are *not* responsible for keeping the documentation up to date. However, to generate it proactively, you can use the following command:
+
+```bash
+just api-ref-all
+```
+
+### Submitting Your PR
+
+#### Before Submitting
+
+1. **Run all checks**: `uv run prek run --all-files && uv run pytest`
+2. **Keep scope small**: One feature or fix per PR
+3. **Write clear description**: Your PR description becomes permanent documentation
+4. **Update docs**: Include documentation for API changes
+
+#### PR Description
+
+Write PR descriptions that explain:
+- What problem you're solving
+- Why you chose this approach
+- Any trade-offs or alternatives considered
+- Migration path for breaking changes
+
+Focus on the "why" - the code shows the "what". Keep it concise but complete.
+
+#### What We Look For
+
+**Framework Philosophy**: FastMCP is NOT trying to do all things or provide all shortcuts. Features are rejected when they don't align with the framework's vision, even if perfectly implemented. The burden of proof is on the PR to demonstrate value.
+
+**Code Quality**: We verify code follows existing patterns. Consistency reduces cognitive load. When every module works similarly, developers understand new code quickly.
+
+**Test Coverage**: Not every line needs testing, but every behavior does. Tests document intent and protect against regressions.
+
+**Breaking Changes**: May be acceptable in minor versions but must be clearly documented. See the [versioning policy](/development/releases#versioning-policy).
+
+## Special Modules
+
+**`contrib`**: Community-maintained patterns and utilities. Original authors maintain their contributions. Not representative of the core framework.
+
+**`experimental`**: Maintainer-developed features that may preview future functionality. Can break or be deleted at any time without notice. Pin your FastMCP version when using these features.
\ No newline at end of file
diff --git a/docs/v3/development/releases.mdx b/docs/v3/development/releases.mdx
new file mode 100644
index 000000000..346462736
--- /dev/null
+++ b/docs/v3/development/releases.mdx
@@ -0,0 +1,79 @@
+---
+title: "Releases"
+description: "FastMCP versioning and release process"
+icon: "truck-fast"
+---
+
+FastMCP releases frequently to deliver features quickly in the rapidly evolving MCP ecosystem. We use semantic versioning pragmatically - the Model Context Protocol is young, patterns are still emerging, and waiting for perfect stability would mean missing opportunities to empower developers with better tools.
+
+## Versioning Policy
+
+### Semantic Versioning
+
+**Major (x.0.0)**: Complete API redesigns
+
+Major versions represent fundamental shifts. FastMCP 2.x is entirely different from 1.x in both implementation and design philosophy.
+
+**Minor (2.x.0)**: New features and evolution
+
+
+Unlike traditional semantic versioning, minor versions **may** include [breaking changes](#breaking-changes) when necessary for the ecosystem's evolution. This flexibility is essential in a young ecosystem where perfect backwards compatibility would prevent important improvements.
+
+
+FastMCP always targets the most current MCP Protocol version. Breaking changes in the MCP spec or MCP SDK automatically flow through to FastMCP - we prioritize staying current with the latest features and conventions over maintaining compatibility with older protocol versions.
+
+**Patch (2.0.x)**: Bug fixes and refinements
+
+Patch versions contain only bug fixes without breaking changes. These are safe updates you can apply with confidence.
+
+### Breaking Changes
+
+We permit breaking changes in minor versions because the MCP ecosystem is rapidly evolving. Refusing to break problematic APIs would accumulate design debt that eventually makes the framework unusable. Each breaking change represents a deliberate decision to keep FastMCP aligned with the ecosystem's evolution.
+
+When breaking changes occur:
+- They only happen in minor versions (e.g., 2.3.x to 2.4.0)
+- Release notes explain what changed and how to migrate
+- We provide deprecation warnings at least 1 minor version in advance when possible
+- Changes must substantially benefit users to justify disruption
+
+The public API is what's covered by our compatibility guarantees - these are the parts of FastMCP you can rely on to remain stable within a minor version. The public API consists of:
+- `FastMCP` server class, `Client` class, and FastMCP `Context`
+- Core MCP components: `Tool`, `Prompt`, `Resource`, `ResourceTemplate`, and transports
+- Their public methods and documented behaviors
+
+Everything else (utilities, private methods, internal modules) may change without notice. This boundary lets us refactor internals and improve implementation details without breaking your code. For production stability, pin to specific versions.
+
+
+The `fastmcp.server.auth` module was introduced in 2.12.0 and is exempted from this policy temporarily, meaning it is *expected* to have breaking changes even on patch versions. This is because auth is a rapidly evolving part of the MCP spec and it would be dangerous to be beholden to old decisions. Please pin your FastMCP version if using authentication in production.
+
+We expect this exemption to last through at least the 2.12.x and 2.13.x release series.
+
+
+### Production Use
+
+Pin to exact versions:
+```
+fastmcp==2.11.0 # Good
+fastmcp>=2.11.0 # Bad - will install breaking changes
+```
+
+## Creating Releases
+
+Our release process is intentionally simple:
+
+1. Create GitHub release with tag `vMAJOR.MINOR.PATCH` (e.g., `v2.11.0`)
+2. Generate release notes automatically, and curate or add additional editorial information as needed
+3. GitHub releases automatically trigger PyPI deployments
+
+This automation lets maintainers focus on code quality rather than release mechanics.
+
+### Release Cadence
+
+We follow a feature-driven release cadence rather than a fixed schedule. Minor versions ship approximately every 3-4 weeks when significant functionality is ready.
+
+Patch releases ship promptly for:
+- Critical bug fixes
+- Security updates (immediate release)
+- Regression fixes
+
+This approach means you get improvements as soon as they're ready rather than waiting for arbitrary release dates.
diff --git a/docs/v3/development/tests.mdx b/docs/v3/development/tests.mdx
new file mode 100644
index 000000000..4653368be
--- /dev/null
+++ b/docs/v3/development/tests.mdx
@@ -0,0 +1,396 @@
+---
+title: "Tests"
+description: "Testing patterns and requirements for FastMCP"
+icon: vial
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+Good tests are the foundation of reliable software. In FastMCP, we treat tests as first-class documentation that demonstrates how features work while protecting against regressions. Every new capability needs comprehensive tests that demonstrate correctness.
+
+## FastMCP Tests
+
+### Running Tests
+
+```bash
+# Run all tests
+uv run pytest
+
+# Run specific test file
+uv run pytest tests/server/test_auth.py
+
+# Run with coverage
+uv run pytest --cov=fastmcp
+
+# Skip integration tests for faster runs
+uv run pytest -m "not integration"
+
+# Skip tests that spawn processes
+uv run pytest -m "not integration and not client_process"
+```
+
+Tests should complete in under 1 second unless marked as integration tests. This speed encourages running them frequently, catching issues early.
+
+### Test Organization
+
+Our test organization mirrors the source package structure, creating a predictable mapping between code and tests. When you're working on `fastmcp_slim/fastmcp/server/auth.py`, you'll find its tests in `tests/server/test_auth.py`. In rare cases tests are split further - for example, the OpenAPI tests are so comprehensive they're split across multiple files.
+
+### Test Markers
+
+We use pytest markers to categorize tests that require special resources or take longer to run:
+
+```python
+@pytest.mark.integration
+async def test_github_api_integration():
+ """Test GitHub API integration with real service."""
+ token = os.getenv("FASTMCP_GITHUB_TOKEN")
+ if not token:
+ pytest.skip("FASTMCP_GITHUB_TOKEN not available")
+
+ # Test against real GitHub API
+ client = GitHubClient(token)
+ repos = await client.list_repos("prefecthq")
+ assert "fastmcp" in [repo.name for repo in repos]
+
+@pytest.mark.client_process
+async def test_stdio_transport():
+ """Test STDIO transport with separate process."""
+ # This spawns a subprocess
+ async with Client("python examples/simple_echo.py") as client:
+ result = await client.call_tool("echo", {"message": "test"})
+ assert result.content[0].text == "test"
+```
+
+## Writing Tests
+
+
+### Test Requirements
+
+Following these practices creates maintainable, debuggable test suites that serve as both documentation and regression protection.
+
+#### Single Behavior Per Test
+
+Each test should verify exactly one behavior. When it fails, you need to know immediately what broke. A test that checks five things gives you five potential failure points to investigate. A test that checks one thing points directly to the problem.
+
+
+
+```python Good: Atomic Test
+async def test_tool_registration():
+ """Test that tools are properly registered with the server."""
+ mcp = FastMCP("test-server")
+
+ @mcp.tool
+ def add(a: int, b: int) -> int:
+ return a + b
+
+ tools = mcp.list_tools()
+ assert len(tools) == 1
+ assert tools[0].name == "add"
+```
+
+```python Bad: Multi-Behavior Test
+async def test_server_functionality():
+ """Test multiple server features at once."""
+ mcp = FastMCP("test-server")
+
+ # Tool registration
+ @mcp.tool
+ def add(a: int, b: int) -> int:
+ return a + b
+
+ # Resource creation
+ @mcp.resource("config://app")
+ def get_config():
+ return {"version": "1.0"}
+
+ # Authentication setup
+ mcp.auth = BearerTokenProvider({"token": "user"})
+
+ # What exactly are we testing? If this fails, what broke?
+ assert mcp.list_tools()
+ assert mcp.list_resources()
+ assert mcp.auth is not None
+```
+
+
+
+#### Self-Contained Setup
+
+Every test must create its own setup. Tests should be runnable in any order, in parallel, or in isolation. When a test fails, you should be able to run just that test to reproduce the issue.
+
+
+
+```python Good: Self-Contained
+async def test_tool_execution_with_error():
+ """Test that tool errors are properly handled."""
+ mcp = FastMCP("test-server")
+
+ @mcp.tool
+ def divide(a: int, b: int) -> float:
+ if b == 0:
+ raise ValueError("Cannot divide by zero")
+ return a / b
+
+ async with Client(mcp) as client:
+ with pytest.raises(Exception):
+ await client.call_tool("divide", {"a": 10, "b": 0})
+```
+
+```python Bad: Test Dependencies
+# Global state that tests depend on
+test_server = None
+
+def test_setup_server():
+ """Setup for other tests."""
+ global test_server
+ test_server = FastMCP("shared-server")
+
+def test_server_works():
+ """Test server functionality."""
+ # Depends on test_setup_server running first
+ assert test_server is not None
+```
+
+
+
+#### Clear Intent
+
+Test names and assertions should make the verified behavior obvious. A developer reading your test should understand what feature it validates and how that feature should behave.
+
+```python
+async def test_authenticated_tool_requires_valid_token():
+ """Test that authenticated users can access protected tools."""
+ mcp = FastMCP("test-server")
+ mcp.auth = BearerTokenProvider({"secret-token": "test-user"})
+
+ @mcp.tool
+ def protected_action() -> str:
+ return "success"
+
+ async with Client(mcp, auth=BearerAuth("secret-token")) as client:
+ result = await client.call_tool("protected_action", {})
+ assert result.content[0].text == "success"
+```
+
+#### Using Fixtures
+
+Use fixtures to create reusable data, server configurations, or other resources for your tests. Note that you should **not** open FastMCP clients in your fixtures as it can create hard-to-diagnose issues with event loops.
+
+```python
+import pytest
+from fastmcp import FastMCP, Client
+
+@pytest.fixture
+def weather_server():
+ server = FastMCP("WeatherServer")
+
+ @server.tool
+ def get_temperature(city: str) -> dict:
+ temps = {"NYC": 72, "LA": 85, "Chicago": 68}
+ return {"city": city, "temp": temps.get(city, 70)}
+
+ return server
+
+async def test_temperature_tool(weather_server):
+ async with Client(weather_server) as client:
+ result = await client.call_tool("get_temperature", {"city": "LA"})
+ assert result.data == {"city": "LA", "temp": 85}
+```
+
+#### Effective Assertions
+
+Assertions should be specific and provide context on failure. When a test fails during CI, the assertion message should tell you exactly what went wrong.
+
+```python
+# Basic assertion - minimal context on failure
+assert result.status == "success"
+
+# Better - explains what was expected
+assert result.status == "success", f"Expected successful operation, got {result.status}: {result.error}"
+```
+
+Try not to have too many assertions in a single test unless you truly need to check various aspects of the same behavior. In general, assertions of different behaviors should be in separate tests.
+
+#### Inline Snapshots
+
+FastMCP uses `inline-snapshot` for testing complex data structures. On first run of `pytest --inline-snapshot=create` with an empty `snapshot()`, pytest will auto-populate the expected value. To update snapshots after intentional changes, run `pytest --inline-snapshot=fix`. This is particularly useful for testing JSON schemas and API responses.
+
+```python
+from inline_snapshot import snapshot
+
+async def test_tool_schema_generation():
+ """Test that tool schemas are generated correctly."""
+ mcp = FastMCP("test-server")
+
+ @mcp.tool
+ def calculate_tax(amount: float, rate: float = 0.1) -> dict:
+ """Calculate tax on an amount."""
+ return {"amount": amount, "tax": amount * rate, "total": amount * (1 + rate)}
+
+ tools = mcp.list_tools()
+ schema = tools[0].inputSchema
+
+ # First run: snapshot() is empty, gets auto-populated
+ # Subsequent runs: compares against stored snapshot
+ assert schema == snapshot({
+ "type": "object",
+ "properties": {
+ "amount": {"type": "number"},
+ "rate": {"type": "number", "default": 0.1}
+ },
+ "required": ["amount"]
+ })
+```
+
+### In-Memory Testing
+
+FastMCP uses in-memory transport for testing, where servers and clients communicate directly. The majority of functionality can be tested in a deterministic fashion this way. We use more complex setups only when testing transports themselves.
+
+The in-memory transport runs the real MCP protocol implementation without network overhead. Instead of deploying your server or managing network connections, you pass your server instance directly to the client. Everything runs in the same Python process - you can set breakpoints anywhere and step through with your debugger.
+
+```python
+from fastmcp import FastMCP, Client
+
+# Create your server
+server = FastMCP("WeatherServer")
+
+@server.tool
+def get_temperature(city: str) -> dict:
+ """Get current temperature for a city"""
+ temps = {"NYC": 72, "LA": 85, "Chicago": 68}
+ return {"city": city, "temp": temps.get(city, 70)}
+
+async def test_weather_operations():
+ # Pass server directly - no deployment needed
+ async with Client(server) as client:
+ result = await client.call_tool("get_temperature", {"city": "NYC"})
+ assert result.data == {"city": "NYC", "temp": 72}
+```
+
+This pattern makes tests deterministic and fast - typically completing in milliseconds rather than seconds.
+
+### Mocking External Dependencies
+
+FastMCP servers are standard Python objects, so you can mock external dependencies using your preferred approach:
+
+```python
+from unittest.mock import AsyncMock
+
+async def test_database_tool():
+ server = FastMCP("DataServer")
+
+ # Mock the database
+ mock_db = AsyncMock()
+ mock_db.fetch_users.return_value = [
+ {"id": 1, "name": "Alice"},
+ {"id": 2, "name": "Bob"}
+ ]
+
+ @server.tool
+ async def list_users() -> list:
+ return await mock_db.fetch_users()
+
+ async with Client(server) as client:
+ result = await client.call_tool("list_users", {})
+ assert len(result.data) == 2
+ assert result.data[0]["name"] == "Alice"
+ mock_db.fetch_users.assert_called_once()
+```
+
+### Testing Network Transports
+
+While in-memory testing covers most unit testing needs, you'll occasionally need to test actual network transports like HTTP or SSE. FastMCP provides two approaches: in-process async servers (preferred), and separate subprocess servers (for special cases).
+
+#### In-Process Network Testing (Preferred)
+
+
+
+For most network transport tests, use `run_server_async` as an async context manager. This runs the server as a task in the same process, providing fast, deterministic tests with full debugger support:
+
+```python
+import pytest
+from fastmcp import FastMCP, Client
+from fastmcp.client.transports import StreamableHttpTransport
+from fastmcp.utilities.tests import run_server_async
+
+def create_test_server() -> FastMCP:
+ """Create a test server instance."""
+ server = FastMCP("TestServer")
+
+ @server.tool
+ def greet(name: str) -> str:
+ return f"Hello, {name}!"
+
+ return server
+
+@pytest.fixture
+async def http_server() -> str:
+ """Start server in-process for testing."""
+ server = create_test_server()
+ async with run_server_async(server) as url:
+ yield url
+
+async def test_http_transport(http_server: str):
+ """Test actual HTTP transport behavior."""
+ async with Client(
+ transport=StreamableHttpTransport(http_server)
+ ) as client:
+ result = await client.ping()
+ assert result is True
+
+ greeting = await client.call_tool("greet", {"name": "World"})
+ assert greeting.data == "Hello, World!"
+```
+
+The `run_server_async` context manager automatically handles server lifecycle and cleanup. This approach is faster than subprocess-based testing and provides better error messages.
+
+#### Subprocess Testing (Special Cases)
+
+For tests that require complete process isolation (like STDIO transport or testing subprocess behavior), use `run_server_in_process`:
+
+```python
+import pytest
+from fastmcp.utilities.tests import run_server_in_process
+from fastmcp import FastMCP, Client
+from fastmcp.client.transports import StreamableHttpTransport
+
+def run_server(host: str, port: int) -> None:
+ """Function to run in subprocess."""
+ server = FastMCP("TestServer")
+
+ @server.tool
+ def greet(name: str) -> str:
+ return f"Hello, {name}!"
+
+ server.run(host=host, port=port)
+
+@pytest.fixture
+async def http_server():
+ """Fixture that runs server in subprocess."""
+ with run_server_in_process(run_server, transport="http") as url:
+ yield f"{url}/mcp"
+
+async def test_http_transport(http_server: str):
+ """Test actual HTTP transport behavior."""
+ async with Client(
+ transport=StreamableHttpTransport(http_server)
+ ) as client:
+ result = await client.ping()
+ assert result is True
+```
+
+The `run_server_in_process` utility handles server lifecycle, port allocation, and cleanup automatically. Use this only when subprocess isolation is truly necessary, as it's slower and harder to debug than in-process testing. FastMCP uses the `client_process` marker to isolate these tests in CI.
+
+### Documentation Testing
+
+Documentation requires the same validation as code. The `just docs` command launches a local Mintlify server that renders your documentation exactly as users will see it:
+
+```bash
+# Start local documentation server with hot reload
+just docs
+
+# Or run Mintlify directly
+mintlify dev
+```
+
+The local server watches for changes and automatically refreshes. This preview catches formatting issues and helps you see documentation as users will experience it.
diff --git a/docs/v3/development/v3-notes/auth-provider-env-vars.mdx b/docs/v3/development/v3-notes/auth-provider-env-vars.mdx
new file mode 100644
index 000000000..c61f61cbe
--- /dev/null
+++ b/docs/v3/development/v3-notes/auth-provider-env-vars.mdx
@@ -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__` 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.
diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/v3/development/v3-notes/v3-features.mdx
similarity index 96%
rename from docs/development/v3-notes/v3-features.mdx
rename to docs/v3/development/v3-notes/v3-features.mdx
index da9fc87db..3d656248a 100644
--- a/docs/development/v3-notes/v3-features.mdx
+++ b/docs/v3/development/v3-notes/v3-features.mdx
@@ -29,7 +29,7 @@ FastMCP now includes a sampling handler for Google's Gemini models ([#2977](http
```python
from fastmcp import Client
-from fastmcp.client.sampling.handlers import GoogleGenaiSamplingHandler
+from fastmcp.client.sampling.handlers.google_genai import GoogleGenaiSamplingHandler
from google.genai import Client as GoogleGenaiClient
# Initialize the handler
@@ -386,7 +386,7 @@ Support for [MCP Apps](https://modelcontextprotocol.io/specification/2025-06-18/
```python
from fastmcp import FastMCP
-from fastmcp.server.apps import AppConfig, ResourceCSP, ResourcePermissions
+from fastmcp.apps import AppConfig, ResourceCSP, ResourcePermissions
mcp = FastMCP("My Server")
@@ -421,7 +421,7 @@ The `app=` parameter accepts `True` (enable with defaults), an `AppConfig` insta
```python
from fastmcp import Context
-from fastmcp.server.apps import AppConfig, UI_EXTENSION_ID
+from fastmcp.apps import AppConfig, UI_EXTENSION_ID
@mcp.tool(app=AppConfig(resource_uri="ui://dashboard"))
async def dashboard(ctx: Context) -> dict:
@@ -444,7 +444,7 @@ async def dashboard(ctx: Context) -> dict:
**Future phases** will add a component DSL for building UIs declaratively, an in-repo renderer, and a `FastMCPApp` class.
-Implementation: `src/fastmcp/server/apps.py` (models and constants), with integration points in `server.py` (decorator parameters), `low_level.py` (extension advertisement), and `context.py` (`client_supports_extension` method).
+Implementation: `fastmcp_slim/fastmcp/server/apps.py` (models and constants), with integration points in `server.py` (decorator parameters), `low_level.py` (extension advertisement), and `context.py` (`client_supports_extension` method).
---
@@ -454,7 +454,7 @@ Implementation: `src/fastmcp/server/apps.py` (models and constants), with integr
v3.0 introduces a provider-based component system that replaces v2's static-only registration ([#2622](https://github.com/PrefectHQ/fastmcp/pull/2622)). Providers dynamically source tools, resources, templates, and prompts at runtime.
-**Core abstraction** (`src/fastmcp/server/providers/base.py`):
+**Core abstraction** (`fastmcp_slim/fastmcp/server/providers/base.py`):
```python
class Provider:
async def list_tools(self) -> Sequence[Tool]: ...
@@ -474,7 +474,7 @@ Providers support:
### LocalProvider
-`LocalProvider` (`src/fastmcp/server/providers/local_provider.py`) manages components registered via decorators. Can be used standalone and attached to multiple servers:
+`LocalProvider` (`fastmcp_slim/fastmcp/server/providers/local_provider.py`) manages components registered via decorators. Can be used standalone and attached to multiple servers:
```python
from fastmcp.server.providers import LocalProvider
@@ -492,7 +492,7 @@ server2 = FastMCP("Server2", providers=[provider])
### ProxyProvider
-`ProxyProvider` (`src/fastmcp/server/providers/proxy.py`) proxies components from remote MCP servers via a client factory. Used by `create_proxy()` and `FastMCP.mount()` for remote server integration.
+`ProxyProvider` (`fastmcp_slim/fastmcp/server/providers/proxy.py`) proxies components from remote MCP servers via a client factory. Used by `create_proxy()` and `FastMCP.mount()` for remote server integration.
```python
from fastmcp.server import create_proxy
@@ -503,7 +503,7 @@ server = create_proxy("http://remote-server/mcp")
### OpenAPIProvider
-`OpenAPIProvider` (`src/fastmcp/server/providers/openapi/provider.py`) creates MCP components from OpenAPI specifications. Routes map HTTP operations to tools, resources, or templates based on configurable rules.
+`OpenAPIProvider` (`fastmcp_slim/fastmcp/server/providers/openapi/provider.py`) creates MCP components from OpenAPI specifications. Routes map HTTP operations to tools, resources, or templates based on configurable rules.
```python
from fastmcp.server.providers.openapi import OpenAPIProvider
@@ -523,7 +523,7 @@ Features:
### FastMCPProvider
-`FastMCPProvider` (`src/fastmcp/server/providers/fastmcp_provider.py`) wraps a FastMCP server to enable mounting one server onto another. Components delegate execution through the wrapped server's middleware chain.
+`FastMCPProvider` (`fastmcp_slim/fastmcp/server/providers/fastmcp_provider.py`) wraps a FastMCP server to enable mounting one server onto another. Components delegate execution through the wrapped server's middleware chain.
```python
from fastmcp import FastMCP
@@ -548,7 +548,7 @@ main.add_provider(provider)
Transforms modify components (tools, resources, prompts) as they flow from providers to clients ([#2836](https://github.com/PrefectHQ/fastmcp/pull/2836)). They use a middleware pattern where each transform receives a `call_next` callable to continue the chain.
-**Built-in transforms** (`src/fastmcp/server/transforms/`):
+**Built-in transforms** (`fastmcp_slim/fastmcp/server/transforms/`):
- `Namespace` - adds prefixes to names (`tool` → `api_tool`) and path segments to URIs (`data://x` → `data://api/x`)
- `ToolTransform` - modifies tool schemas (rename, description, tags, argument transforms)
@@ -575,15 +575,14 @@ provider.add_transform(ToolTransform({
```python
from collections.abc import Sequence
-from fastmcp.server.transforms import Transform, ListToolsNext, GetToolNext
+from fastmcp.server.transforms import Transform, GetToolNext
from fastmcp.tools import Tool
class TagFilter(Transform):
def __init__(self, required_tags: set[str]):
self.required_tags = required_tags
- async def list_tools(self, call_next: ListToolsNext) -> Sequence[Tool]:
- tools = await call_next() # Get tools from downstream
+ async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
return [t for t in tools if t.tags & self.required_tags]
async def get_tool(self, name: str, call_next: GetToolNext) -> Tool | None:
@@ -874,7 +873,7 @@ v3.0 introduces type-safe result classes that provide explicit control over comp
#### ToolResult
-`ToolResult` (`src/fastmcp/tools/tool.py:79`) provides structured tool responses:
+`ToolResult` (`fastmcp_slim/fastmcp/tools/tool.py:79`) provides structured tool responses:
```python
from fastmcp.tools import ToolResult
@@ -895,7 +894,7 @@ Fields:
#### ResourceResult
-`ResourceResult` (`src/fastmcp/resources/resource.py:117`) provides structured resource responses:
+`ResourceResult` (`fastmcp_slim/fastmcp/resources/resource.py:117`) provides structured resource responses:
```python
from fastmcp.resources import ResourceResult, ResourceContent
@@ -915,7 +914,7 @@ Accepts strings, bytes, or `list[ResourceContent]` for flexible content handling
#### PromptResult
-`PromptResult` (`src/fastmcp/prompts/prompt.py:109`) provides structured prompt responses:
+`PromptResult` (`fastmcp_slim/fastmcp/prompts/prompt.py:109`) provides structured prompt responses:
```python
from fastmcp.prompts import PromptResult, Message
@@ -937,7 +936,7 @@ def conversation() -> PromptResult:
v3.0 implements MCP SEP-1686 for background task execution via Docket integration.
-**Configuration** (`src/fastmcp/server/tasks/config.py`):
+**Configuration** (`fastmcp_slim/fastmcp/server/tasks/config.py`):
```python
from fastmcp.server.tasks import TaskConfig
@@ -1013,7 +1012,7 @@ fastmcp run server.py --reload --reload-dir ./src --reload-dir ./lib
fastmcp run server.py --reload --transport http --port 8080
```
-Implementation (`src/fastmcp/cli/run.py`):
+Implementation (`fastmcp_slim/fastmcp/cli/run.py`):
- Uses `watchfiles` for efficient file monitoring
- Runs server as subprocess for clean restarts
- Stateless mode for seamless reconnection after restart
diff --git a/docs/v3/getting-started/installation.mdx b/docs/v3/getting-started/installation.mdx
new file mode 100644
index 000000000..4dae8e9b7
--- /dev/null
+++ b/docs/v3/getting-started/installation.mdx
@@ -0,0 +1,118 @@
+---
+title: Installation
+description: Install FastMCP and verify your setup
+icon: arrow-down-to-line
+---
+## Install FastMCP
+
+We recommend using [uv](https://docs.astral.sh/uv/getting-started/installation/) to install and manage FastMCP.
+
+```bash
+pip install fastmcp
+```
+
+Or with uv:
+
+```bash
+uv add fastmcp
+```
+
+### Optional Dependencies
+
+FastMCP provides optional extras for specific features. For example, to install the background tasks extra:
+
+```bash
+pip install "fastmcp[tasks]"
+```
+
+See [Background Tasks](/servers/tasks) for details on the task system.
+
+### Verify Installation
+
+To verify that FastMCP is installed correctly, you can run the following command:
+
+```bash
+fastmcp version
+```
+
+You should see output like the following:
+
+```bash
+$ fastmcp version
+
+FastMCP version: 3.0.0
+MCP version: 1.25.0
+Python version: 3.12.2
+Platform: macOS-15.3.1-arm64-arm-64bit
+FastMCP root path: ~/Developer/fastmcp
+```
+
+### Dependency Licensing
+
+
+FastMCP depends on Cyclopts for CLI functionality. Cyclopts v4 includes docutils as a transitive dependency, which has complex licensing that may trigger compliance reviews in some organizations.
+
+If this is a concern, you can install Cyclopts v5 alpha which removes this dependency:
+
+```bash
+pip install "cyclopts>=5.0.0a1"
+```
+
+Alternatively, wait for the stable v5 release. See [this issue](https://github.com/BrianPugh/cyclopts/issues/672) for details.
+
+## Upgrading
+
+### From FastMCP 2.0
+
+See the [Upgrade Guide](/getting-started/upgrading/from-fastmcp-2) for a complete list of breaking changes and migration steps.
+
+### From the MCP SDK
+
+#### From FastMCP 1.0
+
+If you're using FastMCP 1.0 via the `mcp` package (meaning you import FastMCP as `from mcp.server.fastmcp import FastMCP`), upgrading is straightforward — for most servers, it's a single import change. See the [full upgrade guide](/getting-started/upgrading/from-mcp-sdk) for details.
+
+#### From the Low-Level Server API
+
+If you built your server directly on the `mcp` package's `Server` class — with `list_tools()`/`call_tool()` handlers and hand-written JSON Schema — see the [migration guide](/getting-started/upgrading/from-low-level-sdk) for a full walkthrough.
+
+## Troubleshooting
+
+### `import fastmcp` fails after a pip upgrade
+
+This affects one specific case: upgrading to FastMCP 3.3 or later from FastMCP 3.2 or earlier with `pip`. Fresh installs and `uv` upgrades are unaffected, so you can skip this unless you did exactly that.
+
+If `import fastmcp` raises `ModuleNotFoundError`, or `from fastmcp import FastMCP` raises `ImportError`, immediately after the upgrade, your install is in a half-removed state. Reinstall in a single step:
+
+```bash
+pip install --force-reinstall fastmcp
+```
+
+If that doesn't resolve it, remove both distributions and reinstall from a clean state:
+
+```bash
+pip uninstall -y fastmcp fastmcp-slim
+pip install fastmcp
+```
+
+FastMCP 3.3 moved the importable code from the `fastmcp` distribution into `fastmcp-slim`. During a single-command `pip` upgrade, pip can install the new files and then delete them while uninstalling the old `fastmcp` distribution, whose file manifest still lists those paths. `uv` uninstalls before it installs, so it is unaffected.
+
+## Versioning Policy
+
+FastMCP follows semantic versioning with pragmatic adaptations for the rapidly evolving MCP ecosystem. Breaking changes may occur in minor versions (e.g., 2.3.x to 2.4.0) when necessary to stay current with the MCP Protocol.
+
+For production use, always pin to exact versions:
+```
+fastmcp==3.0.0 # Good
+fastmcp>=3.0.0 # Bad - may install breaking changes
+```
+
+See the full [versioning and release policy](/development/releases#versioning-policy) for details on our public API, deprecation practices, and breaking change philosophy.
+
+## Contributing to FastMCP
+
+Interested in contributing to FastMCP? See the [Contributing Guide](/development/contributing) for details on:
+- Setting up your development environment
+- Running tests and pre-commit hooks
+- Submitting issues and pull requests
+- Code standards and review process
diff --git a/docs/v3/getting-started/quickstart.mdx b/docs/v3/getting-started/quickstart.mdx
new file mode 100644
index 000000000..97d9f3c79
--- /dev/null
+++ b/docs/v3/getting-started/quickstart.mdx
@@ -0,0 +1,164 @@
+---
+title: Quickstart
+icon: rocket-launch
+---
+
+Welcome! This guide will help you quickly set up FastMCP, run your first MCP server, give it a visual UI, and deploy it to Prefect Horizon.
+
+If you haven't already installed FastMCP, follow the [installation instructions](/getting-started/installation).
+
+## Create a FastMCP Server
+
+A FastMCP server is a collection of tools, resources, and other MCP components. To create a server, start by instantiating the `FastMCP` class.
+
+Create a new file called `my_server.py` and add the following code:
+
+```python my_server.py
+from fastmcp import FastMCP
+
+mcp = FastMCP("My MCP Server")
+```
+
+
+That's it! You've created a FastMCP server, albeit a very boring one. Let's add a tool to make it more interesting.
+
+
+## Add a Tool
+
+To add a tool that returns a simple greeting, write a function and decorate it with `@mcp.tool` to register it with the server:
+
+```python my_server.py {5-7}
+from fastmcp import FastMCP
+
+mcp = FastMCP("My MCP Server")
+
+@mcp.tool
+def greet(name: str) -> str:
+ return f"Hello, {name}!"
+```
+
+
+## Run the Server
+
+The simplest way to run your FastMCP server is to call its `run()` method. You can choose between different transports, like `stdio` for local servers, or `http` for remote access:
+
+
+
+```python my_server.py (stdio) {9, 10}
+from fastmcp import FastMCP
+
+mcp = FastMCP("My MCP Server")
+
+@mcp.tool
+def greet(name: str) -> str:
+ return f"Hello, {name}!"
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+```python my_server.py (HTTP) {9, 10}
+from fastmcp import FastMCP
+
+mcp = FastMCP("My MCP Server")
+
+@mcp.tool
+def greet(name: str) -> str:
+ return f"Hello, {name}!"
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
+```
+
+
+
+This lets us run the server with `python my_server.py`. The stdio transport is the traditional way to connect MCP servers to clients, while the HTTP transport enables remote connections.
+
+
+Why do we need the `if __name__ == "__main__":` block?
+
+The `__main__` block is recommended for consistency and compatibility, ensuring your server works with all MCP clients that execute your server file as a script. Users who will exclusively run their server with the FastMCP CLI can omit it, as the CLI imports the server object directly.
+
+
+### Using the FastMCP CLI
+
+You can also use the `fastmcp run` command to start your server. Note that the FastMCP CLI **does not** execute the `__main__` block of your server file. Instead, it imports your server object and runs it with whatever transport and options you provide.
+
+For example, to run this server with the default stdio transport (no matter how you called `mcp.run()`), you can use the following command:
+```bash
+fastmcp run my_server.py:mcp
+```
+
+To run this server with the HTTP transport, you can use the following command:
+```bash
+fastmcp run my_server.py:mcp --transport http --port 8000
+```
+
+## Call Your Server
+
+Once your server is running with HTTP transport, you can connect to it with a FastMCP client or any LLM client that supports the MCP protocol:
+
+```python my_client.py
+import asyncio
+from fastmcp import Client
+
+client = Client("http://localhost:8000/mcp")
+
+async def call_tool(name: str):
+ async with client:
+ result = await client.call_tool("greet", {"name": name})
+ print(result)
+
+asyncio.run(call_tool("Ford"))
+```
+
+Note that:
+- FastMCP clients are asynchronous, so we need to use `asyncio.run` to run the client
+- We must enter a client context (`async with client:`) before using the client
+- You can make multiple client calls within the same context
+
+## Give Your Tool a UI
+
+Tools normally return text, but any tool can return an interactive UI instead. Add `app=True` to your tool decorator and return a [Prefab](https://prefab.prefect.io) component — the host renders it as a chart, table, form, or any other visual element right in the conversation. This requires the `apps` extra (`pip install "fastmcp[apps]"`).
+
+The `app=True` flag tells FastMCP to wire up the renderer and protocol metadata automatically. The tool still works like any other MCP tool — it receives arguments and returns a result — but the result is a component tree that the host displays visually instead of as plain text.
+
+```python my_server.py
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column, Heading, Text, Badge, Row
+from fastmcp import FastMCP
+
+mcp = FastMCP("My MCP Server")
+
+
+@mcp.tool(app=True)
+def greet(name: str) -> PrefabApp:
+ """Greet someone with a visual card."""
+ with Column(gap=4, css_class="p-6") as view:
+ Heading(f"Hello, {name}!")
+ with Row(gap=2, align="center"):
+ Text("Status")
+ Badge("Greeted", variant="success")
+
+ return PrefabApp(view=view)
+```
+
+You can preview app tools locally with `fastmcp dev apps my_server.py` — no MCP host required. See the [Apps overview](/apps/overview) for the full guide, including state management, forms, charts, and server-connected interactivity.
+
+## Deploy to Prefect Horizon
+
+[Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides managed hosting, authentication, access control, and observability for MCP servers.
+
+
+Horizon is **free for personal projects** and offers enterprise governance for teams.
+
+
+To deploy your server, you'll need a [GitHub account](https://github.com). Once you have one, you can deploy your server in three steps:
+
+1. Push your `my_server.py` file to a GitHub repository
+2. Sign in to [Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) with your GitHub account
+3. Create a new project from your repository and enter `my_server.py:mcp` as the server entrypoint
+
+That's it! Horizon will build and deploy your server, making it available at a URL like `https://your-project.fastmcp.app/mcp`. You can chat with it to test its functionality, or connect to it from any LLM client that supports the MCP protocol.
+
+For more details, see the [Prefect Horizon guide](/deployment/prefect-horizon).
diff --git a/docs/v3/getting-started/upgrading/from-fastmcp-2.mdx b/docs/v3/getting-started/upgrading/from-fastmcp-2.mdx
new file mode 100644
index 000000000..1e659e76a
--- /dev/null
+++ b/docs/v3/getting-started/upgrading/from-fastmcp-2.mdx
@@ -0,0 +1,444 @@
+---
+title: Upgrading from FastMCP 2
+sidebarTitle: "From FastMCP 2"
+description: Migration instructions for upgrading between FastMCP versions
+icon: up
+---
+
+This guide covers breaking changes and migration steps when upgrading FastMCP.
+
+## v3.0.0
+
+For most servers, upgrading to v3 is straightforward. The breaking changes below affect deprecated constructor kwargs, sync-to-async shifts, a few renamed methods, and some less commonly used features.
+
+### Install
+
+Since you already have `fastmcp` installed, you need to explicitly request the new version — `pip install fastmcp` won't upgrade an existing installation:
+
+```bash
+pip install --upgrade fastmcp
+# or
+uv add --upgrade fastmcp
+```
+
+If you pin versions in a requirements file or `pyproject.toml`, update your pin to `fastmcp>=3.0.0,<4`.
+
+
+**New repository home.** As part of the v3 release, FastMCP's GitHub repository has moved from `jlowin/fastmcp` to [`PrefectHQ/fastmcp`](https://github.com/PrefectHQ/fastmcp) under [Prefect](https://prefect.io)'s stewardship. GitHub automatically redirects existing clones and bookmarks, so nothing breaks — but you can update your local remote whenever convenient:
+
+```bash
+git remote set-url origin https://github.com/PrefectHQ/fastmcp.git
+```
+
+If you reference the repository URL in dependency specifications (e.g., `git+https://github.com/jlowin/fastmcp.git`), update those to the new location.
+
+
+
+You are upgrading a FastMCP v2 server to FastMCP v3.0. Analyze the provided code and identify every change needed. The full upgrade guide is at https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2 and the complete FastMCP documentation is at https://gofastmcp.com — fetch these for complete context.
+
+BREAKING CHANGES (will crash at import or runtime):
+
+1. CONSTRUCTOR KWARGS REMOVED: FastMCP() no longer accepts these kwargs (raises TypeError):
+ - Transport settings: host, port, log_level, debug, sse_path, streamable_http_path, json_response, stateless_http
+ Fix: pass to run() or run_http_async() instead, e.g. mcp.run(transport="http", host="0.0.0.0", port=8080)
+ - message_path: set via environment variable FASTMCP_MESSAGE_PATH only (not a run() kwarg)
+ - Duplicate handling: on_duplicate_tools, on_duplicate_resources, on_duplicate_prompts
+ Fix: use unified on_duplicate= parameter
+ - Tool settings: tool_serializer, include_tags, exclude_tags, tool_transformations
+ Fix: use ToolResult returns, server.enable()/disable(), server.add_transform()
+
+2. COMPONENT METHODS REMOVED:
+ - tool.enable()/disable() raises NotImplementedError
+ Fix: server.disable(names={"tool_name"}, components={"tool"}) or server.disable(tags={"tag"})
+ - get_tools()/get_resources()/get_prompts()/get_resource_templates() removed
+ Fix: use list_tools()/list_resources()/list_prompts()/list_resource_templates() — these return lists, not dicts
+
+3. ASYNC STATE: ctx.set_state() and ctx.get_state() are now async (must be awaited).
+ State values must be JSON-serializable unless serializable=False is passed.
+ Each FastMCP instance has its own state store, so serializable state set by parent middleware isn't visible to mounted tools by default.
+ Fix: pass the same session_state_store to both servers, or use serializable=False (request-scoped state is always shared).
+
+4. PROMPTS: mcp.types.PromptMessage replaced by fastmcp.prompts.Message.
+ Before: PromptMessage(role="user", content=TextContent(type="text", text="Hello"))
+ After: Message("Hello") # role defaults to "user", accepts plain strings
+ Also: if prompts return raw dicts like `{"role": "user", "content": "..."}`, these must become Message objects.
+ v2 silently coerced dicts; v3 requires typed Message objects or plain strings.
+
+5. AUTH PROVIDERS: No longer auto-load from env vars. Pass client_id, client_secret explicitly via os.environ.
+
+6. WSTRANSPORT: Removed. Use StreamableHttpTransport.
+
+7. OPENAPI: timeout parameter removed from OpenAPIProvider. Set timeout on the httpx.AsyncClient instead.
+
+8. METADATA: Namespace changed from "_fastmcp" to "fastmcp" in tool.meta. The include_fastmcp_meta parameter is removed (always included).
+
+9. ENV VAR: FASTMCP_SHOW_CLI_BANNER renamed to FASTMCP_SHOW_SERVER_BANNER.
+
+10. DECORATORS: @mcp.tool, @mcp.resource, @mcp.prompt now return the original function, not a component object. Code that accesses .name, .description, or other component attributes on the decorated result will crash with AttributeError.
+ Fix: set FASTMCP_DECORATOR_MODE=object for v2 compat (itself deprecated).
+
+11. OAUTH STORAGE: Default OAuth client storage changed from DiskStore to FileTreeStore due to pickle deserialization vulnerability in diskcache (CVE-2025-69872). Clients using default storage will re-register automatically on first connection. If using DiskStore explicitly, switch to FileTreeStore (with key/collection sanitization strategies) or add pip install 'py-key-value-aio[disk]'.
+
+12. REPO MOVE: GitHub repository moved from jlowin/fastmcp to PrefectHQ/fastmcp. Update git remotes and dependency URLs that reference the old location.
+
+13. BACKGROUND TASKS: FastMCP's background task system (SEP-1686) is now an optional dependency. If the code uses task=True or TaskConfig, add pip install "fastmcp[tasks]".
+
+DEPRECATIONS (still work but emit warnings):
+
+- mount(prefix="x") -> mount(namespace="x")
+- import_server(sub) -> mount(sub)
+- FastMCP.as_proxy(url) -> from fastmcp.server import create_proxy; create_proxy(url)
+- from fastmcp.server.proxy -> from fastmcp.server.providers.proxy
+- from fastmcp.server.openapi import FastMCPOpenAPI -> from fastmcp.server.providers.openapi import OpenAPIProvider; use FastMCP("name", providers=[OpenAPIProvider(...)])
+- mcp.add_tool_transformation(name, cfg) -> from fastmcp.server.transforms import ToolTransform; mcp.add_transform(ToolTransform(...))
+
+For each issue found, show the original line, explain why it breaks, and provide the corrected code.
+
+
+### Breaking Changes
+
+**Transport and server settings removed from constructor**
+
+In v2, you could configure transport settings directly in the `FastMCP()` constructor. In v3, `FastMCP()` is purely about your server's identity and behavior — transport configuration happens when you actually start serving. Passing any of the old kwargs now raises `TypeError` with a migration hint.
+
+```python
+# Before
+mcp = FastMCP("server", host="0.0.0.0", port=8080)
+mcp.run()
+
+# After
+mcp = FastMCP("server")
+mcp.run(transport="http", host="0.0.0.0", port=8080)
+```
+
+The full list of removed kwargs and their replacements:
+
+- `host`, `port`, `log_level`, `debug`, `sse_path`, `streamable_http_path`, `json_response`, `stateless_http` — pass to `run()`, `run_http_async()`, or `http_app()`, or set via environment variables (e.g. `FASTMCP_HOST`)
+- `message_path` — set via environment variable `FASTMCP_MESSAGE_PATH` only (not a `run()` kwarg)
+- `on_duplicate_tools`, `on_duplicate_resources`, `on_duplicate_prompts` — consolidated into a single `on_duplicate=` parameter
+- `tool_serializer` — return [`ToolResult`](/servers/tools#custom-serialization) from your tools instead
+- `include_tags` / `exclude_tags` — use `server.enable(tags=..., only=True)` / `server.disable(tags=...)` after construction
+- `tool_transformations` — use `server.add_transform(ToolTransform(...))` after construction
+
+**OAuth storage backend changed (diskcache CVE)**
+
+The default OAuth client storage has moved from `DiskStore` to `FileTreeStore` to address a pickle deserialization vulnerability in diskcache ([CVE-2025-69872](https://github.com/PrefectHQ/fastmcp/issues/3166)).
+
+If you were using the default storage (i.e., not passing an explicit `client_storage`), clients will need to re-register on their first connection after upgrading. This happens automatically — no user action required, and it's the same flow that already occurs whenever a server restarts with in-memory storage.
+
+If you were passing a `DiskStore` explicitly, you can either [switch to `FileTreeStore`](/servers/storage-backends) (recommended) or keep using `DiskStore` by adding the dependency yourself.
+
+
+When switching to `FileTreeStore`, you **must** configure key and collection sanitization strategies. Without them, keys containing special characters (such as URL-based OAuth client IDs) will cause filesystem errors. See the [File Storage](/servers/storage-backends#file-storage) section for the recommended setup.
+
+
+
+Keeping `DiskStore` requires `pip install 'py-key-value-aio[disk]'`, which re-introduces the vulnerable `diskcache` package into your dependency tree.
+
+
+**Component enable()/disable() moved to server**
+
+In v2, you could enable or disable individual components by calling methods on the component object itself. In v3, visibility is controlled through the server (or provider), which lets you target components by name, tag, or type without needing a reference to the object:
+
+```python
+# Before
+tool = await server.get_tool("my_tool")
+tool.disable()
+
+# After
+server.disable(names={"my_tool"}, components={"tool"})
+```
+
+Calling `.enable()` or `.disable()` on a component object now raises `NotImplementedError`. See [Visibility](/servers/visibility) for the full API, including tag-based filtering and per-session visibility.
+
+**Listing methods renamed and return lists**
+
+The `get_tools()`, `get_resources()`, `get_prompts()`, and `get_resource_templates()` methods have been renamed to `list_tools()`, `list_resources()`, `list_prompts()`, and `list_resource_templates()`. More importantly, they now return lists instead of dicts — so code that indexes by name needs to change:
+
+```python
+# Before
+tools = await server.get_tools()
+tool = tools["my_tool"]
+
+# After
+tools = await server.list_tools()
+tool = next((t for t in tools if t.name == "my_tool"), None)
+```
+
+**Prompts use Message class**
+
+Prompt functions now use FastMCP's `Message` class instead of `mcp.types.PromptMessage`. The new class is simpler — it accepts a plain string and defaults to `role="user"`, so most prompts become one-liners:
+
+```python
+# Before
+from mcp.types import PromptMessage, TextContent
+
+@mcp.prompt
+def my_prompt() -> PromptMessage:
+ return PromptMessage(role="user", content=TextContent(type="text", text="Hello"))
+
+# After
+from fastmcp.prompts import Message
+
+@mcp.prompt
+def my_prompt() -> Message:
+ return Message("Hello")
+```
+
+If your prompt functions return raw dicts with `role` and `content` keys, those also need to change. v2 silently coerced dicts into prompt messages, but v3 requires typed `Message` objects (or plain strings for single user messages):
+
+```python
+# Before (v2 accepted this)
+@mcp.prompt
+def my_prompt():
+ return [
+ {"role": "user", "content": "Hello"},
+ {"role": "assistant", "content": "How can I help?"},
+ ]
+
+# After
+from fastmcp.prompts import Message
+
+@mcp.prompt
+def my_prompt() -> list[Message]:
+ return [
+ Message("Hello"),
+ Message("How can I help?", role="assistant"),
+ ]
+```
+
+**Context state methods are async**
+
+`ctx.set_state()` and `ctx.get_state()` are now async because state in v3 is session-scoped and backed by a pluggable storage backend (rather than a simple dict). This means state persists across multiple tool calls within the same session:
+
+```python
+# Before
+ctx.set_state("key", "value")
+value = ctx.get_state("key")
+
+# After
+await ctx.set_state("key", "value")
+value = await ctx.get_state("key")
+```
+
+State values must also be JSON-serializable by default (dicts, lists, strings, numbers, etc.). If you need to store non-serializable values like an HTTP client, pass `serializable=False` — these values are request-scoped and only available during the current tool call:
+
+```python
+await ctx.set_state("client", my_http_client, serializable=False)
+```
+
+**Mounted servers have isolated state stores**
+
+Each `FastMCP` instance has its own state store. In v2 this wasn't noticeable because mounted tools ran in the parent's context, but in v3's provider architecture each server is isolated. Non-serializable state (`serializable=False`) is request-scoped and automatically shared across mount boundaries. For serializable state, pass the same `session_state_store` to both servers:
+
+```python
+from fastmcp import FastMCP
+from key_value.aio.stores.memory import MemoryStore
+
+store = MemoryStore()
+parent = FastMCP("Parent", session_state_store=store)
+child = FastMCP("Child", session_state_store=store)
+parent.mount(child, namespace="child")
+```
+
+**Auth provider environment variables removed**
+
+In v2, auth providers like `GitHubProvider` could auto-load configuration from environment variables with a `FASTMCP_SERVER_AUTH_*` prefix. This magic has been removed — pass values explicitly:
+
+```python
+# Before (v2) — client_id and client_secret loaded automatically
+# from FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID, etc.
+auth = GitHubProvider()
+
+# After (v3) — pass values explicitly
+import os
+from fastmcp.server.auth.providers.github import GitHubProvider
+
+auth = GitHubProvider(
+ client_id=os.environ["GITHUB_CLIENT_ID"],
+ client_secret=os.environ["GITHUB_CLIENT_SECRET"],
+)
+```
+
+**WSTransport removed**
+
+The deprecated WebSocket client transport has been removed. Use `StreamableHttpTransport` instead:
+
+```python test="skip"
+# Before
+from fastmcp.client.transports import WSTransport
+transport = WSTransport("ws://localhost:8000/ws")
+
+# After
+from fastmcp.client.transports import StreamableHttpTransport
+transport = StreamableHttpTransport("http://localhost:8000/mcp")
+```
+
+**OpenAPI `timeout` parameter removed**
+
+`OpenAPIProvider` no longer accepts a `timeout` parameter. Configure timeout on the httpx client directly. The `client` parameter is also now optional — when omitted, a default client is created from the spec's `servers` URL with a 30-second timeout:
+
+```python
+# Before
+provider = OpenAPIProvider(spec, client, timeout=60)
+
+# After
+client = httpx.AsyncClient(base_url="https://api.example.com", timeout=60)
+provider = OpenAPIProvider(spec, client)
+```
+
+**Metadata namespace renamed**
+
+The FastMCP metadata key in component `meta` dicts changed from `_fastmcp` to `fastmcp`. If you read metadata from tool or resource objects, update the key:
+
+```python
+# Before
+tags = tool.meta.get("_fastmcp", {}).get("tags", [])
+
+# After
+tags = tool.meta.get("fastmcp", {}).get("tags", [])
+```
+
+Metadata is now always included — the `include_fastmcp_meta` parameter has been removed from `FastMCP()` and `to_mcp_tool()`, so there is no way to suppress it.
+
+**Server banner environment variable renamed**
+
+`FASTMCP_SHOW_CLI_BANNER` is now `FASTMCP_SHOW_SERVER_BANNER`.
+
+**Decorators return functions**
+
+In v2, `@mcp.tool` transformed your function into a `FunctionTool` object. In v3, decorators return your original function unchanged — which means decorated functions stay callable for testing, reuse, and composition:
+
+```python
+@mcp.tool
+def greet(name: str) -> str:
+ return f"Hello, {name}!"
+
+greet("World") # Works! Returns "Hello, World!"
+```
+
+If you have code that treats the decorated result as a `FunctionTool` (e.g., accessing `.name` or `.description`), set `FASTMCP_DECORATOR_MODE=object` for v2 compatibility. This escape hatch is itself deprecated and will be removed in a future release.
+
+**Background tasks require optional dependency**
+
+FastMCP's background task system (SEP-1686) is now behind an optional extra. If your server uses background tasks, install with:
+
+```bash
+pip install "fastmcp[tasks]"
+```
+
+Without the extra, configuring a tool with `task=True` or `TaskConfig` will raise an import error at runtime. See [Background Tasks](/servers/tasks) for details.
+
+### Deprecated Features
+
+These still work but emit warnings. Update when convenient.
+
+**mount() prefix → namespace**
+
+```python
+# Deprecated
+main.mount(subserver, prefix="api")
+
+# New
+main.mount(subserver, namespace="api")
+```
+
+**import_server() → mount()**
+
+```python
+# Deprecated
+main.import_server(subserver)
+
+# New
+main.mount(subserver)
+```
+
+**Module import paths for proxy and OpenAPI**
+
+The proxy and OpenAPI modules have moved under `providers` to reflect v3's provider-based architecture:
+
+```python test="skip"
+# Deprecated
+from fastmcp.server.proxy import FastMCPProxy
+from fastmcp.server.openapi import FastMCPOpenAPI
+
+# New
+from fastmcp.server.providers.proxy import FastMCPProxy
+from fastmcp.server.providers.openapi import OpenAPIProvider
+```
+
+`FastMCPOpenAPI` itself is deprecated — use `FastMCP` with an `OpenAPIProvider` instead:
+
+```python test="skip"
+# Deprecated
+from fastmcp.server.openapi import FastMCPOpenAPI
+server = FastMCPOpenAPI(spec, client)
+
+# New
+from fastmcp import FastMCP
+from fastmcp.server.providers.openapi import OpenAPIProvider
+server = FastMCP("my_api", providers=[OpenAPIProvider(spec, client)])
+```
+
+**add_tool_transformation() → add_transform()**
+
+```python
+# Deprecated
+mcp.add_tool_transformation("name", config)
+
+# New
+from fastmcp.server.transforms import ToolTransform
+mcp.add_transform(ToolTransform({"name": config}))
+```
+
+**FastMCP.as_proxy() → create_proxy()**
+
+```python
+# Deprecated
+proxy = FastMCP.as_proxy("http://example.com/mcp")
+
+# New
+from fastmcp.server import create_proxy
+proxy = create_proxy("http://example.com/mcp")
+```
+
+## v2.14.0
+
+### OpenAPI Parser Promotion
+
+The experimental OpenAPI parser is now standard. Update imports:
+
+```python test="skip"
+# Before
+from fastmcp.experimental.server.openapi import FastMCPOpenAPI
+
+# After
+from fastmcp.server.openapi import FastMCPOpenAPI
+```
+
+### Removed Deprecated Features
+
+- `BearerAuthProvider` → use `JWTVerifier`
+- `Context.get_http_request()` → use `get_http_request()` from dependencies
+- `from fastmcp import Image` → use `from fastmcp.utilities.types import Image`
+- `FastMCP(dependencies=[...])` → use `fastmcp.json` configuration
+- `FastMCPProxy(client=...)` → use `client_factory=lambda: ...`
+- `output_schema=False` → use `output_schema=None`
+
+## v2.13.0
+
+### OAuth Token Key Management
+
+The OAuth proxy now issues its own JWT tokens. For production, provide explicit keys:
+
+```python
+auth = GitHubProvider(
+ client_id=os.environ["GITHUB_CLIENT_ID"],
+ client_secret=os.environ["GITHUB_CLIENT_SECRET"],
+ base_url="https://your-server.com",
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ client_storage=RedisStore(host="redis.example.com"),
+)
+```
+
+See [OAuth Token Security](/deployment/http#oauth-token-security) for details.
diff --git a/docs/getting-started/upgrading/from-low-level-sdk.mdx b/docs/v3/getting-started/upgrading/from-low-level-sdk.mdx
similarity index 100%
rename from docs/getting-started/upgrading/from-low-level-sdk.mdx
rename to docs/v3/getting-started/upgrading/from-low-level-sdk.mdx
diff --git a/docs/getting-started/upgrading/from-mcp-sdk.mdx b/docs/v3/getting-started/upgrading/from-mcp-sdk.mdx
similarity index 100%
rename from docs/getting-started/upgrading/from-mcp-sdk.mdx
rename to docs/v3/getting-started/upgrading/from-mcp-sdk.mdx
diff --git a/docs/v3/getting-started/welcome.mdx b/docs/v3/getting-started/welcome.mdx
new file mode 100644
index 000000000..d42dc39b7
--- /dev/null
+++ b/docs/v3/getting-started/welcome.mdx
@@ -0,0 +1,134 @@
+---
+title: "Welcome to FastMCP"
+sidebarTitle: "Welcome!"
+description: The fast, Pythonic way to build MCP servers, clients, and applications.
+icon: hand-wave
+mode: center
+---
+{/*
+
+
+
+ */}
+
+
+
+
+**FastMCP is the standard framework for building MCP applications.** The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) connects LLMs to tools and data. FastMCP gives you everything you need to go from prototype to production — build servers that expose capabilities, connect clients to any MCP service, and give your tools interactive UIs:
+
+```python {1}
+from fastmcp import FastMCP
+
+mcp = FastMCP("Demo 🚀")
+
+@mcp.tool
+def add(a: int, b: int) -> int:
+ """Add two numbers"""
+ return a + b
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+
+## Move Fast and Make Things
+
+The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) lets you give agents access to your tools and data. But building an effective MCP application is harder than it looks.
+
+FastMCP 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:
+
+
+
+ Expose tools, resources, and prompts to LLMs.
+
+
+ Give your tools interactive UIs rendered directly in the conversation.
+
+
+ Connect to any MCP server — local or remote, programmatic or CLI.
+
+
+
+**[Servers](/servers/server)** wrap your Python functions into MCP-compliant tools, resources, and prompts. **[Clients](/clients/client)** connect to any server with full protocol support. And **[Apps](/apps/overview)** give your tools interactive UIs rendered directly in the conversation.
+
+Ready to build? Start with the [installation guide](/getting-started/installation) or jump straight to the [quickstart](/getting-started/quickstart).
+
+FastMCP is made with 💙 by [Prefect](https://www.prefect.io/).
+
+## Run FastMCP in production with Horizon
+
+FastMCP is the standard way to build MCP servers. **[Prefect Horizon](https://www.prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_welcome&utm_content=welcome_body)** is the enterprise MCP gateway for running them safely.
+
+Built by the FastMCP team, Horizon packages the best practices we've learned shipping the world's most popular MCP framework.
+
+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=gofastmcp&utm_medium=docs&utm_campaign=docs_welcome&utm_content=welcome_cta)
+
+
+**This documentation reflects FastMCP's `main` branch**, meaning it always reflects the latest development version. Features are generally marked with version badges (e.g. `New in version: 3.0.0`) to indicate when they were introduced. Note that this may include features that are not yet released.
+
+
+## LLM-Friendly Docs
+
+The FastMCP documentation is available in multiple LLM-friendly formats:
+
+### MCP Server
+
+The FastMCP docs are accessible via MCP! The server URL is `https://gofastmcp.com/mcp`.
+
+In fact, you can use FastMCP to search the FastMCP docs:
+
+```python
+import asyncio
+from fastmcp import Client
+
+async def main():
+ async with Client("https://gofastmcp.com/mcp") as client:
+ result = await client.call_tool(
+ name="search_fast_mcp",
+ arguments={"query": "deploy a FastMCP server"}
+ )
+ print(result)
+
+asyncio.run(main())
+```
+
+### Text Formats
+
+The docs are also available in [llms.txt format](https://llmstxt.org/):
+- [llms.txt](https://gofastmcp.com/llms.txt) - A sitemap listing all documentation pages
+- [llms-full.txt](https://gofastmcp.com/llms-full.txt) - The entire documentation in one file (may exceed context windows)
+
+Any page can be accessed as markdown by appending `.md` to the URL. For example, this page becomes `https://gofastmcp.com/getting-started/welcome.md`.
+
+You can also copy any page as markdown by pressing "Cmd+C" (or "Ctrl+C" on Windows) on your keyboard.
diff --git a/docs/v3/integrations/anthropic.mdx b/docs/v3/integrations/anthropic.mdx
new file mode 100644
index 000000000..08b9b2c9c
--- /dev/null
+++ b/docs/v3/integrations/anthropic.mdx
@@ -0,0 +1,228 @@
+---
+title: Anthropic API 🤝 FastMCP
+sidebarTitle: Anthropic API
+description: Connect FastMCP servers to the Anthropic API
+icon: message-code
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+Anthropic's [Messages API](https://docs.anthropic.com/en/api/messages) supports MCP servers as remote tool sources. This tutorial will show you how to create a FastMCP server and deploy it to a public URL, then how to call it from the Messages API.
+
+
+Currently, the MCP connector only accesses **tools** from MCP servers—it queries the `list_tools` endpoint and exposes those functions to Claude. Other MCP features like resources and prompts are not currently supported. You can read more about the MCP connector in the [Anthropic documentation](https://docs.anthropic.com/en/docs/agents-and-tools/mcp-connector).
+
+
+## Create a Server
+
+First, create a FastMCP server with the tools you want to expose. For this example, we'll create a server with a single tool that rolls dice.
+
+```python server.py
+import random
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="Dice Roller")
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ """Roll `n_dice` 6-sided dice and return the results."""
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
+```
+
+## Deploy the Server
+
+Your server must be deployed to a public URL in order for Anthropic to access it. The MCP connector supports both SSE and Streamable HTTP transports.
+
+For development, you can use tools like `ngrok` to temporarily expose a locally-running server to the internet. We'll do that for this example (you may need to install `ngrok` and create a free account), but you can use any other method to deploy your server.
+
+Assuming you saved the above code as `server.py`, you can run the following two commands in two separate terminals to deploy your server and expose it to the internet:
+
+
+```bash FastMCP server
+python server.py
+```
+
+```bash ngrok
+ngrok http 8000
+```
+
+
+
+This exposes your unauthenticated server to the internet. Only run this command in a safe environment if you understand the risks.
+
+
+## Call the Server
+
+To use the Messages API with MCP servers, you'll need to install the Anthropic Python SDK (not included with FastMCP):
+
+```bash
+pip install anthropic
+```
+
+You'll also need to authenticate with Anthropic. You can do this by setting the `ANTHROPIC_API_KEY` environment variable. Consult the Anthropic SDK documentation for more information.
+
+```bash
+export ANTHROPIC_API_KEY="your-api-key"
+```
+
+Here is an example of how to call your server from Python. Note that you'll need to replace `https://your-server-url.com` with the actual URL of your server. In addition, we use `/mcp/` as the endpoint because we deployed a streamable-HTTP server with the default path; you may need to use a different endpoint if you customized your server's deployment. **At this time you must also include the `extra_headers` parameter with the `anthropic-beta` header.**
+
+```python {5, 13-22}
+import anthropic
+from rich import print
+
+# Your server URL (replace with your actual URL)
+url = 'https://your-server-url.com'
+
+client = anthropic.Anthropic()
+
+response = client.beta.messages.create(
+ model="claude-sonnet-4-20250514",
+ max_tokens=1000,
+ messages=[{"role": "user", "content": "Roll a few dice!"}],
+ mcp_servers=[
+ {
+ "type": "url",
+ "url": f"{url}/mcp/",
+ "name": "dice-server",
+ }
+ ],
+ extra_headers={
+ "anthropic-beta": "mcp-client-2025-04-04"
+ }
+)
+
+print(response.content)
+```
+
+If you run this code, you'll see something like the following output:
+
+```text
+I'll roll some dice for you! Let me use the dice rolling tool.
+
+I rolled 3 dice and got: 4, 2, 6
+
+The results were 4, 2, and 6. Would you like me to roll again or roll a different number of dice?
+```
+
+
+## Authentication
+
+
+
+The MCP connector supports OAuth authentication through authorization tokens, which means you can secure your server while still allowing Anthropic to access it.
+
+### Server Authentication
+
+The simplest way to add authentication to the server is to use a bearer token scheme.
+
+For this example, we'll quickly generate our own tokens with FastMCP's `RSAKeyPair` utility, but this may not be appropriate for production use. For more details, see the complete server-side [Token Verification](/servers/auth/token-verification) documentation.
+
+We'll start by creating an RSA key pair to sign and verify tokens.
+
+```python
+from fastmcp.server.auth.providers.jwt import RSAKeyPair
+
+key_pair = RSAKeyPair.generate()
+access_token = key_pair.create_token(audience="dice-server")
+```
+
+
+FastMCP's `RSAKeyPair` utility is for development and testing only.
+
+
+Next, we'll create a `JWTVerifier` to authenticate the server.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import JWTVerifier
+
+auth = JWTVerifier(
+ public_key=key_pair.public_key,
+ audience="dice-server",
+)
+
+mcp = FastMCP(name="Dice Roller", auth=auth)
+```
+
+Here is a complete example that you can copy/paste. For simplicity and the purposes of this example only, it will print the token to the console. **Do NOT do this in production!**
+
+```python server.py [expandable]
+from fastmcp import FastMCP
+from fastmcp.server.auth import JWTVerifier
+from fastmcp.server.auth.providers.jwt import RSAKeyPair
+import random
+
+key_pair = RSAKeyPair.generate()
+access_token = key_pair.create_token(audience="dice-server")
+
+auth = JWTVerifier(
+ public_key=key_pair.public_key,
+ audience="dice-server",
+)
+
+mcp = FastMCP(name="Dice Roller", auth=auth)
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ """Roll `n_dice` 6-sided dice and return the results."""
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+if __name__ == "__main__":
+ print(f"\n---\n\n🔑 Dice Roller access token:\n\n{access_token}\n\n---\n")
+ mcp.run(transport="http", port=8000)
+```
+
+### Client Authentication
+
+If you try to call the authenticated server with the same Anthropic code we wrote earlier, you'll get an error indicating that the server rejected the request because it's not authenticated.
+
+```text
+Error code: 400 - {
+ "type": "error",
+ "error": {
+ "type": "invalid_request_error",
+ "message": "MCP server 'dice-server' requires authentication. Please provide an authorization_token.",
+ },
+}
+```
+
+To authenticate the client, you can pass the token using the `authorization_token` parameter in your MCP server configuration:
+
+```python {8, 21}
+import anthropic
+from rich import print
+
+# Your server URL (replace with your actual URL)
+url = 'https://your-server-url.com'
+
+# Your access token (replace with your actual token)
+access_token = 'your-access-token'
+
+client = anthropic.Anthropic()
+
+response = client.beta.messages.create(
+ model="claude-sonnet-4-20250514",
+ max_tokens=1000,
+ messages=[{"role": "user", "content": "Roll a few dice!"}],
+ mcp_servers=[
+ {
+ "type": "url",
+ "url": f"{url}/mcp/",
+ "name": "dice-server",
+ "authorization_token": access_token
+ }
+ ],
+ extra_headers={
+ "anthropic-beta": "mcp-client-2025-04-04"
+ }
+)
+
+print(response.content)
+```
+
+You should now see the dice roll results in the output.
diff --git a/docs/v3/integrations/auth0.mdx b/docs/v3/integrations/auth0.mdx
new file mode 100644
index 000000000..65f9d3873
--- /dev/null
+++ b/docs/v3/integrations/auth0.mdx
@@ -0,0 +1,195 @@
+---
+title: Auth0 OAuth 🤝 FastMCP
+sidebarTitle: Auth0
+description: Secure your FastMCP server with Auth0 OAuth
+icon: shield-check
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+This guide shows you how to secure your FastMCP server using **Auth0 OAuth**. While Auth0 does have support for Dynamic Client Registration, it is not enabled by default so this integration uses the [**OIDC Proxy**](/servers/auth/oidc-proxy) pattern to bridge Auth0's dynamic OIDC configuration with MCP's authentication requirements.
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+1. An **[Auth0 Account](https://auth0.com/)** with access to create Applications
+2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
+
+### Step 1: Create an Auth0 Application
+
+Create an Application in your Auth0 settings to get the credentials needed for authentication:
+
+
+
+ Go to **Applications → Applications** in your Auth0 account.
+
+ Click **"+ Create Application"** to create a new application.
+
+
+
+ - **Name**: Choose a name users will recognize (e.g., "My FastMCP Server")
+ - **Choose an application type**: Choose "Single Page Web Applications"
+ - Click **Create** to create the application
+
+
+
+ Select the "Settings" tab for your application, then find the "Application URIs" section.
+
+ - **Allowed Callback URLs**: Your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)
+ - Click **Save** to save your changes
+
+
+ The callback URL must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter.
+
+
+
+ If you want to use a custom callback path (e.g., `/auth/auth0/callback`), make sure to set the same path in both your Auth0 Application settings and the `redirect_path` parameter when configuring the Auth0Provider.
+
+
+
+
+ After creating the app, in the "Basic Information" section you'll see:
+
+ - **Client ID**: A public identifier like `tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB`
+ - **Client Secret**: A private hidden value that should always be stored securely
+
+
+ Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.
+
+
+
+
+ Go to **Applications → APIs** in your Auth0 account.
+
+ - Find the API that you want to use for your application
+ - **API Audience**: A URL that uniquely identifies the API
+
+
+ Store this along with of the credentials above. Never commit this to version control. Use environment variables or a secrets manager in production.
+
+
+
+
+### Step 2: FastMCP Configuration
+
+Create your FastMCP server using the `Auth0Provider`.
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.auth0 import Auth0Provider
+
+# The Auth0Provider utilizes Auth0 OIDC configuration
+auth_provider = Auth0Provider(
+ config_url="https://.../.well-known/openid-configuration", # Your Auth0 configuration URL
+ client_id="tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB", # Your Auth0 application Client ID
+ client_secret="vPYqbjemq...", # Your Auth0 application Client Secret
+ audience="https://...", # Your Auth0 API audience
+ base_url="http://localhost:8000", # Must match your application configuration
+ # redirect_path="/auth/callback" # Default value, customize if needed
+)
+
+mcp = FastMCP(name="Auth0 Secured App", auth=auth_provider)
+
+# Add a protected tool to test authentication
+@mcp.tool
+async def get_token_info() -> dict:
+ """Returns information about the Auth0 token."""
+ from fastmcp.server.dependencies import get_access_token
+
+ token = get_access_token()
+
+ return {
+ "issuer": token.claims.get("iss"),
+ "audience": token.claims.get("aud"),
+ "scope": token.claims.get("scope")
+ }
+```
+
+## Testing
+
+### Running the Server
+
+Start your FastMCP server with HTTP transport to enable OAuth flows:
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+Your server is now running and protected by Auth0 authentication.
+
+### Testing with a Client
+
+Create a test client that authenticates with your Auth0-protected server:
+
+```python test_client.py
+from fastmcp import Client
+import asyncio
+
+async def main():
+ # The client will automatically handle Auth0 OAuth flows
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ # First-time connection will open Auth0 login in your browser
+ print("✓ Authenticated with Auth0!")
+
+ # Test the protected tool
+ result = await client.call_tool("get_token_info")
+ print(f"Auth0 audience: {result['audience']}")
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+When you run the client for the first time:
+1. Your browser will open to Auth0's authorization page
+2. After you authorize the app, you'll be redirected back
+3. The client receives the token and can make authenticated requests
+
+## Production Configuration
+
+
+
+For production deployments with persistent token management across server restarts, configure `jwt_signing_key`, and `client_storage`:
+
+```python server.py
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.auth0 import Auth0Provider
+from key_value.aio.stores.redis import RedisStore
+from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
+from cryptography.fernet import Fernet
+
+# Production setup with encrypted persistent token storage
+auth_provider = Auth0Provider(
+ config_url="https://.../.well-known/openid-configuration",
+ client_id="tv2ObNgaZAWWhhycr7Bz1LU2mxlnsmsB",
+ client_secret="vPYqbjemq...",
+ audience="https://...",
+ base_url="https://your-production-domain.com",
+
+ # Production token management
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ client_storage=FernetEncryptionWrapper(
+ key_value=RedisStore(
+ host=os.environ["REDIS_HOST"],
+ port=int(os.environ["REDIS_PORT"])
+ ),
+ fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
+ )
+)
+
+mcp = FastMCP(name="Production Auth0 App", auth=auth_provider)
+```
+
+
+Parameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.
+
+For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
+
+
+
+The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.
+
diff --git a/docs/v3/integrations/authkit.mdx b/docs/v3/integrations/authkit.mdx
new file mode 100644
index 000000000..c77175201
--- /dev/null
+++ b/docs/v3/integrations/authkit.mdx
@@ -0,0 +1,106 @@
+---
+title: AuthKit 🤝 FastMCP
+sidebarTitle: AuthKit
+description: Secure your FastMCP server with AuthKit by WorkOS
+icon: shield-check
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+This guide shows you how to secure your FastMCP server using WorkOS's **AuthKit**, a complete authentication and user management solution. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern with [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html) resource indicators: AuthKit issues tokens whose `aud` claim is bound to your server's resource URL, and FastMCP validates that claim automatically.
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+1. A **[WorkOS Account](https://workos.com/)** and a new **Project**.
+2. An **[AuthKit](https://www.authkit.com/)** instance configured within your WorkOS project.
+3. Your FastMCP server's URL (can be localhost for development, e.g., `http://127.0.0.1:8000`).
+
+### Step 1: WorkOS Dashboard
+
+In the WorkOS Dashboard, go to **Connect → Configuration** and configure:
+
+
+
+ Enable **Dynamic Client Registration** (DCR) so MCP clients can register themselves. Alternatively, enable **Client ID Metadata Document** (CIMD) if your clients support it.
+
+
+
+ Add your FastMCP server's resource URL (e.g., `http://127.0.0.1:8000/mcp`) as a valid resource indicator.
+
+ This must exactly match what FastMCP advertises in its protected resource metadata. Start your server first and it will log the correct URL on startup — copy that value.
+
+ Without this step, AuthKit falls back to a default environment-scoped audience and audience validation will fail with a 401.
+
+
+
+ Find your **AuthKit Domain** on the configuration page. It will look like `https://your-project-12345.authkit.app`. You'll need this for your FastMCP server configuration.
+
+
+
+### Step 2: FastMCP Configuration
+
+Create your FastMCP server file and use the `AuthKitProvider` to handle all the OAuth integration automatically:
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.workos import AuthKitProvider
+
+# AuthKitProvider automatically discovers WorkOS endpoints, configures JWT
+# validation, and binds the token audience to this server's resource URL.
+auth_provider = AuthKitProvider(
+ authkit_domain="https://your-project-12345.authkit.app",
+ base_url="http://127.0.0.1:8000", # Use your actual server URL
+)
+
+mcp = FastMCP(name="AuthKit Secured App", auth=auth_provider)
+```
+
+When the server starts, it logs the resource URL it is validating against. Paste that URL into your Dashboard's **MCP resource indicators** list.
+
+## Testing
+
+To test your server, you can use the `fastmcp` CLI to run it locally. Assuming you've saved the above code to `server.py` (after replacing the `authkit_domain` and `base_url` with your actual values!), you can run the following command:
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+AuthKit defaults DCR clients to `client_secret_basic` for token exchange, which conflicts with how some MCP clients send credentials. To avoid token exchange errors, register as a public client by setting `token_endpoint_auth_method` to `"none"`:
+
+```python client.py
+from fastmcp import Client
+from fastmcp.client.auth import OAuth
+import asyncio
+
+auth = OAuth(additional_client_metadata={"token_endpoint_auth_method": "none"})
+
+async def main():
+ async with Client("http://127.0.0.1:8000/mcp", auth=auth) as client:
+ assert await client.ping()
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+## Production Configuration
+
+For production deployments, load sensitive configuration from environment variables:
+
+```python server.py
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.workos import AuthKitProvider
+
+# Load configuration from environment variables
+auth = AuthKitProvider(
+ authkit_domain=os.environ.get("AUTHKIT_DOMAIN"),
+ base_url=os.environ.get("BASE_URL", "https://your-server.com"),
+)
+
+mcp = FastMCP(name="AuthKit Secured App", auth=auth)
+```
diff --git a/docs/v3/integrations/aws-cognito.mdx b/docs/v3/integrations/aws-cognito.mdx
new file mode 100644
index 000000000..b7df29222
--- /dev/null
+++ b/docs/v3/integrations/aws-cognito.mdx
@@ -0,0 +1,278 @@
+---
+title: AWS Cognito OAuth 🤝 FastMCP
+sidebarTitle: AWS Cognito
+description: Secure your FastMCP server with AWS Cognito user pools
+icon: aws
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+This guide shows you how to secure your FastMCP server using **AWS Cognito user pools**. Since AWS Cognito doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/servers/auth/oauth-proxy) pattern to bridge AWS Cognito's traditional OAuth with MCP's authentication requirements. It also includes robust JWT token validation, ensuring enterprise-grade authentication.
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+1. An **[AWS Account](https://aws.amazon.com/)** with access to create AWS Cognito user pools
+2. Basic familiarity with AWS Cognito concepts (user pools, app clients)
+3. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
+
+### Step 1: Create an AWS Cognito User Pool and App Client
+
+Set up AWS Cognito user pool with an app client to get the credentials needed for authentication:
+
+
+
+ Go to the **[AWS Cognito Console](https://console.aws.amazon.com/cognito/)** and ensure you're in your desired AWS region.
+
+ Select **"User pools"** from the side navigation (click on the hamburger icon at the top left in case you don't see any), and click **"Create user pool"** to create a new user pool.
+
+
+
+ AWS Cognito now provides a streamlined setup experience:
+
+ 1. **Application type**: Select **"Traditional web application"** (this is the correct choice for FastMCP server-side authentication)
+ 2. **Name your application**: Enter a descriptive name (e.g., `FastMCP Server`)
+
+ The traditional web application type automatically configures:
+ - Server-side authentication with client secrets
+ - Authorization code grant flow
+ - Appropriate security settings for confidential clients
+
+
+ Choose "Traditional web application" rather than SPA, Mobile app, or Machine-to-machine options. This ensures proper OAuth 2.0 configuration for FastMCP.
+
+
+
+
+ AWS will guide you through configuration options:
+
+ - **Sign-in identifiers**: Choose how users will sign in (email, username, or phone)
+ - **Required attributes**: Select any additional user information you need
+ - **Return URL**: Add your callback URL (e.g., `http://localhost:8000/auth/callback` for development)
+
+
+ The simplified interface handles most OAuth security settings automatically based on your application type selection.
+
+
+
+
+ Review your configuration and click **"Create user pool"**.
+
+ After creation, you'll see your user pool details. Save these important values:
+ - **User pool ID** (format: `eu-central-1_XXXXXXXXX`)
+ - **Client ID** (found under → "Applications" → "App clients" in the side navigation → \ → "App client information")
+ - **Client Secret** (found under → "Applications" → "App clients" in the side navigation → \ → "App client information")
+
+
+ The user pool ID and app client credentials are all you need for FastMCP configuration.
+
+
+
+
+ Under "Login pages" in your app client's settings, you can double check and adjust the OAuth configuration:
+
+ - **Allowed callback URLs**: Add your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)
+ - **Allowed sign-out URLs**: Optional, for logout functionality
+ - **OAuth 2.0 grant types**: Ensure "Authorization code grant" is selected
+ - **OpenID Connect scopes**: Select scopes your application needs (e.g., `openid`, `email`, `profile`)
+
+
+ For local development, you can use `http://localhost` URLs. For production, you must use HTTPS.
+
+
+
+
+ AWS Cognito requires a resource server entry to support OAuth with protected resources. Without this, token exchange will fail with an `invalid_grant` error.
+
+ Navigate to **"Branding" → "Domain"** in the side navigation, then:
+
+ 1. Click **"Create resource server"**
+ 2. **Resource server name**: Enter a descriptive name (e.g., `My MCP Server`)
+ 3. **Resource server identifier**: Enter your MCP endpoint URL exactly as it will be accessed (e.g., `http://localhost:8000/mcp` for development, or `https://your-server.com/mcp` for production)
+ 4. Click **"Create resource server"**
+
+
+ The resource server identifier must exactly match your `base_url + mcp_path`. For the default configuration with `base_url="http://localhost:8000"` and `path="/mcp"`, use `http://localhost:8000/mcp`.
+
+
+
+
+ After setup, you'll have:
+
+ - **User Pool ID**: Format like `eu-central-1_XXXXXXXXX`
+ - **Client ID**: Your application's client identifier
+ - **Client Secret**: Generated client secret (keep secure)
+ - **AWS Region**: Where Your AWS Cognito user pool is located
+
+
+ Store these credentials securely. Never commit them to version control. Use environment variables or AWS Secrets Manager in production.
+
+
+
+
+### Step 2: FastMCP Configuration
+
+Create your FastMCP server using the `AWSCognitoProvider`, which handles AWS Cognito's JWT tokens and user claims automatically:
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.aws import AWSCognitoProvider
+from fastmcp.server.dependencies import get_access_token
+
+# The AWSCognitoProvider handles JWT validation and user claims
+auth_provider = AWSCognitoProvider(
+ user_pool_id="eu-central-1_XXXXXXXXX", # Your AWS Cognito user pool ID
+ aws_region="eu-central-1", # AWS region (defaults to eu-central-1)
+ client_id="your-app-client-id", # Your app client ID
+ client_secret="your-app-client-secret", # Your app client Secret
+ base_url="http://localhost:8000", # Must match your callback URL
+ # redirect_path="/auth/callback" # Default value, customize if needed
+)
+
+mcp = FastMCP(name="AWS Cognito Secured App", auth=auth_provider)
+
+# Add a protected tool to test authentication
+@mcp.tool
+async def get_access_token_claims() -> dict:
+ """Get the authenticated user's access token claims."""
+ token = get_access_token()
+ return {
+ "sub": token.claims.get("sub"),
+ "username": token.claims.get("username"),
+ "cognito:groups": token.claims.get("cognito:groups", []),
+ }
+```
+
+## Testing
+
+### Running the Server
+
+Start your FastMCP server with HTTP transport to enable OAuth flows:
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+Your server is now running and protected by AWS Cognito OAuth authentication.
+
+### Testing with a Client
+
+Create a test client that authenticates with Your AWS Cognito-protected server:
+
+```python test_client.py
+from fastmcp import Client
+import asyncio
+
+async def main():
+ # The client will automatically handle AWS Cognito OAuth
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ # First-time connection will open AWS Cognito login in your browser
+ print("✓ Authenticated with AWS Cognito!")
+
+ # Test the protected tool
+ print("Calling protected tool: get_access_token_claims")
+ result = await client.call_tool("get_access_token_claims")
+ user_data = result.data
+ print("Available access token claims:")
+ print(f"- sub: {user_data.get('sub', 'N/A')}")
+ print(f"- username: {user_data.get('username', 'N/A')}")
+ print(f"- cognito:groups: {user_data.get('cognito:groups', [])}")
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+When you run the client for the first time:
+1. Your browser will open to AWS Cognito's hosted UI login page
+2. After you sign in (or sign up), you'll be redirected back to your MCP server
+3. The client receives the JWT token and can make authenticated requests
+
+
+The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.
+
+
+## Production Configuration
+
+
+
+For production deployments with persistent token management across server restarts, configure `jwt_signing_key`, and `client_storage`:
+
+```python server.py
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.aws import AWSCognitoProvider
+from key_value.aio.stores.redis import RedisStore
+from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
+from cryptography.fernet import Fernet
+
+# Production setup with encrypted persistent token storage
+auth_provider = AWSCognitoProvider(
+ user_pool_id="eu-central-1_XXXXXXXXX",
+ aws_region="eu-central-1",
+ client_id="your-app-client-id",
+ client_secret="your-app-client-secret",
+ base_url="https://your-production-domain.com",
+
+ # Production token management
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ client_storage=FernetEncryptionWrapper(
+ key_value=RedisStore(
+ host=os.environ["REDIS_HOST"],
+ port=int(os.environ["REDIS_PORT"])
+ ),
+ fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
+ )
+)
+
+mcp = FastMCP(name="Production AWS Cognito App", auth=auth_provider)
+```
+
+
+Parameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.
+
+For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
+
+
+## Features
+
+### JWT Token Validation
+
+The AWS Cognito provider includes robust JWT token validation:
+
+- **Signature Verification**: Validates tokens against AWS Cognito's public keys (JWKS)
+- **Expiration Checking**: Automatically rejects expired tokens
+- **Issuer Validation**: Ensures tokens come from your specific AWS Cognito user pool
+- **Scope Enforcement**: Verifies required OAuth scopes are present
+
+### User Claims and Groups
+
+Access rich user information from AWS Cognito JWT tokens:
+
+```python
+from fastmcp.server.dependencies import get_access_token
+
+@mcp.tool
+async def admin_only_tool() -> str:
+ """A tool only available to admin users."""
+ token = get_access_token()
+ user_groups = token.claims.get("cognito:groups", [])
+
+ if "admin" not in user_groups:
+ raise ValueError("This tool requires admin access")
+
+ return "Admin access granted!"
+```
+
+### Enterprise Integration
+
+Perfect for enterprise environments with:
+
+- **Single Sign-On (SSO)**: Integrate with corporate identity providers
+- **Multi-Factor Authentication (MFA)**: Leverage AWS Cognito's built-in MFA
+- **User Groups**: Role-based access control through AWS Cognito groups
+- **Custom Attributes**: Access custom user attributes defined in your AWS Cognito user pool
+- **Compliance**: Meet enterprise security and compliance requirements
\ No newline at end of file
diff --git a/docs/v3/integrations/azure.mdx b/docs/v3/integrations/azure.mdx
new file mode 100644
index 000000000..cba92349e
--- /dev/null
+++ b/docs/v3/integrations/azure.mdx
@@ -0,0 +1,542 @@
+---
+title: Azure (Microsoft Entra ID) OAuth 🤝 FastMCP
+sidebarTitle: Azure (Entra ID)
+description: Secure your FastMCP server with Azure/Microsoft Entra OAuth
+icon: microsoft
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+This guide shows you how to secure your FastMCP server using **Azure OAuth** (Microsoft Entra ID). Since Azure doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/servers/auth/oauth-proxy) pattern to bridge Azure's traditional OAuth with MCP's authentication requirements. FastMCP validates Azure JWTs against your application's client_id.
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+1. An **[Azure Account](https://portal.azure.com/)** with access to create App registrations
+2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
+3. Your Azure tenant ID (found in Azure Portal under Microsoft Entra ID)
+
+### Step 1: Create an Azure App Registration
+
+Create an App registration in Azure Portal to get the credentials needed for authentication:
+
+
+
+ Go to the [Azure Portal](https://portal.azure.com) and navigate to **Microsoft Entra ID → App registrations**.
+
+ Click **"New registration"** to create a new application.
+
+
+
+ Fill in the application details:
+
+ - **Name**: Choose a name users will recognize (e.g., "My FastMCP Server")
+ - **Supported account types**: Choose based on your needs:
+ - **Single tenant**: Only users in your organization
+ - **Multitenant**: Users in any Microsoft Entra directory
+ - **Multitenant + personal accounts**: Any Microsoft account
+ - **Redirect URI**: Select "Web" and enter your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)
+
+
+ The redirect URI must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. For local development, Azure allows `http://localhost` URLs. For production, you must use HTTPS.
+
+
+
+ If you want to use a custom callback path (e.g., `/auth/azure/callback`), make sure to set the same path in both your Azure App registration and the `redirect_path` parameter when configuring the AzureProvider.
+
+
+ - **Expose an API**: Configure your Application ID URI and define scopes
+ - Go to **Expose an API** in the App registration sidebar.
+ - Click **Set** next to "Application ID URI" and choose one of:
+ - Keep the default `api://{client_id}`
+ - Set a custom value, following the supported formats (see [Identifier URI restrictions](https://learn.microsoft.com/en-us/entra/identity-platform/identifier-uri-restrictions))
+ - Click **Add a scope** and create a scope your app will require, for example:
+ - Scope name: `read` (or `write`, etc.)
+ - Admin consent display name/description: as appropriate for your org
+ - Who can consent: as needed (Admins only or Admins and users)
+
+ - **Configure Access Token Version**: Ensure your app uses access token v2
+ - Go to **Manifest** in the App registration sidebar.
+ - Find the `requestedAccessTokenVersion` property and set it to `2`:
+ ```json
+ "api": {
+ "requestedAccessTokenVersion": 2
+ }
+ ```
+ - Click **Save** at the top of the manifest editor.
+
+
+ Access token v2 is required for FastMCP's Azure integration to work correctly. If this is not set, you may encounter authentication errors.
+
+
+
+ In FastMCP's `AzureProvider`, set `identifier_uri` to your Application ID URI (optional; defaults to `api://{client_id}`) and set `required_scopes` to the unprefixed scope names (e.g., `read`, `write`). During authorization, FastMCP automatically prefixes scopes with your `identifier_uri`.
+
+
+
+
+
+
+
+ After registration, navigate to **Certificates & secrets** in your app's settings.
+
+ - Click **"New client secret"**
+ - Add a description (e.g., "FastMCP Server")
+ - Choose an expiration period
+ - Click **"Add"**
+
+
+ Copy the secret value immediately - it won't be shown again! You'll need to create a new secret if you lose it.
+
+
+
+
+ From the **Overview** page of your app registration, note:
+
+ - **Application (client) ID**: A UUID like `835f09b6-0f0f-40cc-85cb-f32c5829a149`
+ - **Directory (tenant) ID**: A UUID like `08541b6e-646d-43de-a0eb-834e6713d6d5`
+ - **Client Secret**: The value you copied in the previous step
+
+
+ Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.
+
+
+
+
+### Step 2: FastMCP Configuration
+
+Create your FastMCP server using the `AzureProvider`, which handles Azure's OAuth flow automatically:
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.azure import AzureProvider
+
+# The AzureProvider handles Azure's token format and validation
+auth_provider = AzureProvider(
+ client_id="835f09b6-0f0f-40cc-85cb-f32c5829a149", # Your Azure App Client ID
+ client_secret="your-client-secret", # Your Azure App Client Secret
+ tenant_id="08541b6e-646d-43de-a0eb-834e6713d6d5", # Your Azure Tenant ID (REQUIRED)
+ base_url="http://localhost:8000", # Must match your App registration
+ required_scopes=["your-scope"], # At least one scope REQUIRED - name of scope from your App
+ # identifier_uri defaults to api://{client_id}
+ # identifier_uri="api://your-api-id",
+ # Optional: request additional upstream scopes in the authorize request
+ # additional_authorize_scopes=["User.Read", "openid", "email"],
+ # redirect_path="/auth/callback" # Default value, customize if needed
+ # base_authority="login.microsoftonline.us" # For Azure Government (default: login.microsoftonline.com)
+)
+
+mcp = FastMCP(name="Azure Secured App", auth=auth_provider)
+
+# Add a protected tool to test authentication
+@mcp.tool
+async def get_user_info() -> dict:
+ """Returns information about the authenticated Azure user."""
+ from fastmcp.server.dependencies import get_access_token
+
+ token = get_access_token()
+ # The AzureProvider stores user data in token claims
+ return {
+ "azure_id": token.claims.get("sub"),
+ "email": token.claims.get("email"),
+ "name": token.claims.get("name"),
+ "job_title": token.claims.get("job_title"),
+ "office_location": token.claims.get("office_location")
+ }
+```
+
+
+**Important**: The `tenant_id` parameter is **REQUIRED**. Azure no longer supports using "common" for new applications due to security requirements. You must use one of:
+
+- **Your specific tenant ID**: Found in Azure Portal (e.g., `08541b6e-646d-43de-a0eb-834e6713d6d5`)
+- **"organizations"**: For work and school accounts only
+- **"consumers"**: For personal Microsoft accounts only
+
+Using your specific tenant ID is recommended for better security and control.
+
+
+
+**Important**: The `required_scopes` parameter is **REQUIRED** and must include at least one scope. Azure's OAuth API requires the `scope` parameter in all authorization requests - you cannot authenticate without specifying at least one scope. Use the unprefixed scope names from your Azure App registration (e.g., `["read", "write"]`). These scopes must be created under **Expose an API** in your App registration.
+
+
+### Scope Handling
+
+FastMCP automatically prefixes `required_scopes` with your `identifier_uri` (e.g., `api://your-client-id`) since these are your custom API scopes. Scopes in `additional_authorize_scopes` are sent as-is since they target external resources like Microsoft Graph.
+
+**`required_scopes`** — Your custom API scopes, defined in Azure "Expose an API":
+
+| You write | Sent to Azure | Validated on tokens |
+|-----------|---------------|---------------------|
+| `mcp-read` | `api://xxx/mcp-read` | ✓ |
+| `my.scope` | `api://xxx/my.scope` | ✓ |
+| `openid` | `openid` | ✗ (OIDC scope) |
+| `api://xxx/read` | `api://xxx/read` | ✓ |
+
+**`additional_authorize_scopes`** — External scopes (e.g., Microsoft Graph) for server-side use:
+
+| You write | Sent to Azure | Validated on tokens |
+|-----------|---------------|---------------------|
+| `User.Read` | `User.Read` | ✗ |
+| `Mail.Send` | `Mail.Send` | ✗ |
+
+
+`offline_access` is automatically included to obtain refresh tokens. FastMCP manages token refreshing automatically.
+
+
+
+**Why aren't `additional_authorize_scopes` validated?** Azure issues separate tokens per resource. The access token FastMCP receives is for *your API*—Graph scopes aren't in its `scp` claim. To call Graph APIs, your server uses the upstream Azure token in an on-behalf-of (OBO) flow.
+
+
+
+OIDC scopes (`openid`, `profile`, `email`, `offline_access`) are never prefixed and excluded from validation because Azure doesn't include them in access token `scp` claims.
+
+
+## Testing
+
+### Running the Server
+
+Start your FastMCP server with HTTP transport to enable OAuth flows:
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+Your server is now running and protected by Azure OAuth authentication.
+
+### Testing with a Client
+
+Create a test client that authenticates with your Azure-protected server:
+
+```python test_client.py
+from fastmcp import Client
+import asyncio
+
+async def main():
+ # The client will automatically handle Azure OAuth
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ # First-time connection will open Azure login in your browser
+ print("✓ Authenticated with Azure!")
+
+ # Test the protected tool
+ result = await client.call_tool("get_user_info")
+ print(f"Azure user: {result['email']}")
+ print(f"Name: {result['name']}")
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+When you run the client for the first time:
+1. Your browser will open to Microsoft's authorization page
+2. Sign in with your Microsoft account (work, school, or personal based on your tenant configuration)
+3. Grant the requested permissions
+4. After authorization, you'll be redirected back
+5. The client receives the token and can make authenticated requests
+
+
+The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.
+
+
+## Production Configuration
+
+
+
+For production deployments with persistent token management across server restarts, configure `jwt_signing_key` and `client_storage`:
+
+```python server.py
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.azure import AzureProvider
+from key_value.aio.stores.redis import RedisStore
+from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
+from cryptography.fernet import Fernet
+
+# Production setup with encrypted persistent token storage
+auth_provider = AzureProvider(
+ client_id="835f09b6-0f0f-40cc-85cb-f32c5829a149",
+ client_secret="your-client-secret",
+ tenant_id="08541b6e-646d-43de-a0eb-834e6713d6d5",
+ base_url="https://your-production-domain.com",
+ required_scopes=["your-scope"],
+
+ # Production token management
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ client_storage=FernetEncryptionWrapper(
+ key_value=RedisStore(
+ host=os.environ["REDIS_HOST"],
+ port=int(os.environ["REDIS_PORT"])
+ ),
+ fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
+ )
+)
+
+mcp = FastMCP(name="Production Azure App", auth=auth_provider)
+```
+
+
+Parameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.
+
+For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
+
+
+## Token Verification Only (Managed Identity)
+
+
+
+For deployments where your server only needs to **validate incoming tokens** — such as Azure Container Apps with Managed Identity — use `AzureJWTVerifier` with `RemoteAuthProvider` instead of the full `AzureProvider`.
+
+This pattern is ideal when:
+- Your infrastructure handles authentication (e.g., Managed Identity)
+- You don't need the OAuth proxy flow (no `client_secret` required)
+- You just need to verify that incoming Azure AD tokens are valid
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth import RemoteAuthProvider
+from fastmcp.server.auth.providers.azure import AzureJWTVerifier
+from pydantic import AnyHttpUrl
+
+tenant_id = "your-tenant-id"
+client_id = "your-client-id"
+
+# AzureJWTVerifier auto-configures JWKS, issuer, and audience
+verifier = AzureJWTVerifier(
+ client_id=client_id,
+ tenant_id=tenant_id,
+ required_scopes=["access_as_user"], # Scope names from Azure Portal
+)
+
+auth = RemoteAuthProvider(
+ token_verifier=verifier,
+ authorization_servers=[
+ AnyHttpUrl(f"https://login.microsoftonline.com/{tenant_id}/v2.0")
+ ],
+ base_url="https://your-container-app.azurecontainerapps.io",
+)
+
+mcp = FastMCP(name="Azure MI App", auth=auth)
+```
+
+`AzureJWTVerifier` handles Azure's scope format automatically. You write scope names exactly as they appear in Azure Portal under **Expose an API** (e.g., `access_as_user`). The verifier validates tokens using the short-form scopes that Azure puts in the `scp` claim, while advertising the full URI scopes (e.g., `api://your-client-id/access_as_user`) in OAuth metadata so MCP clients know what to request.
+
+
+For Azure Government, pass `base_authority="login.microsoftonline.us"` to `AzureJWTVerifier`.
+
+
+## On-Behalf-Of (OBO)
+
+
+
+The On-Behalf-Of (OBO) flow allows your FastMCP server to call downstream Microsoft APIs—like Microsoft Graph—using the authenticated user's identity. When a user authenticates to your MCP server, you receive a token for your API. OBO exchanges that token for a new token that can call other services, maintaining the user's identity and permissions throughout the chain.
+
+This pattern is useful when your tools need to access user-specific data from Microsoft services: reading emails, accessing calendar events, querying SharePoint, or any other Graph API operation that requires user context.
+
+
+OBO features require the `azure` extra:
+
+```bash
+pip install 'fastmcp[azure]'
+```
+
+
+### Azure Portal Setup
+
+OBO requires additional configuration in your Azure App registration beyond basic authentication.
+
+
+
+ In your App registration, navigate to **API permissions** and add the Microsoft Graph permissions your tools will need.
+
+ - Click **Add a permission** → **Microsoft Graph** → **Delegated permissions**
+ - Select the permissions required for your use case (e.g., `Mail.Read`, `Calendars.Read`, `User.Read`)
+ - Repeat for any other APIs you need to call
+
+
+ Only add delegated permissions for OBO. Application permissions bypass user context entirely and are inappropriate for the OBO flow.
+
+
+
+
+ OBO requires admin consent for the permissions you've added. In the **API permissions** page, click **Grant admin consent for [Your Organization]**.
+
+ Without admin consent, OBO token exchanges will fail with an `AADSTS65001` error indicating the user or administrator hasn't consented to use the application.
+
+
+ For development, you can grant consent for just your own account. For production, an Azure AD administrator must grant tenant-wide consent.
+
+
+
+
+### Configure AzureProvider for OBO
+
+The `additional_authorize_scopes` parameter tells Azure which downstream API permissions to include during the initial authorization. These scopes establish what your server can request through OBO later.
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.azure import AzureProvider
+
+auth_provider = AzureProvider(
+ client_id="your-client-id",
+ client_secret="your-client-secret",
+ tenant_id="your-tenant-id",
+ base_url="http://localhost:8000",
+ required_scopes=["mcp-access"], # Your API scope
+ # Include Graph scopes for OBO
+ additional_authorize_scopes=[
+ "https://graph.microsoft.com/Mail.Read",
+ "https://graph.microsoft.com/User.Read",
+ "offline_access", # Enables refresh tokens
+ ],
+)
+
+mcp = FastMCP(name="Graph-Enabled Server", auth=auth_provider)
+```
+
+Scopes listed in `additional_authorize_scopes` are requested during the initial OAuth flow but aren't validated on incoming tokens. They establish permission for your server to later exchange the user's token for downstream API access.
+
+
+Use fully-qualified scope URIs for downstream APIs (e.g., `https://graph.microsoft.com/Mail.Read`). Short forms like `Mail.Read` work for authorization requests, but fully-qualified URIs are clearer and avoid ambiguity.
+
+
+### EntraOBOToken Dependency
+
+The `EntraOBOToken` dependency handles the complete OBO flow automatically. Declare it as a parameter default with the scopes you need, and FastMCP exchanges the user's token for a downstream API token before your function runs.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.azure import AzureProvider, EntraOBOToken
+import httpx
+
+auth_provider = AzureProvider(
+ client_id="your-client-id",
+ client_secret="your-client-secret",
+ tenant_id="your-tenant-id",
+ base_url="http://localhost:8000",
+ required_scopes=["mcp-access"],
+ additional_authorize_scopes=[
+ "https://graph.microsoft.com/Mail.Read",
+ "https://graph.microsoft.com/User.Read",
+ ],
+)
+
+mcp = FastMCP(name="Email Reader", auth=auth_provider)
+
+@mcp.tool
+async def get_recent_emails(
+ count: int = 10,
+ graph_token: str = EntraOBOToken(["https://graph.microsoft.com/Mail.Read"]),
+) -> list[dict]:
+ """Get the user's recent emails from Microsoft Graph."""
+ async with httpx.AsyncClient() as client:
+ response = await client.get(
+ f"https://graph.microsoft.com/v1.0/me/messages?$top={count}",
+ headers={"Authorization": f"Bearer {graph_token}"},
+ )
+ response.raise_for_status()
+ data = response.json()
+
+ return [
+ {"subject": msg["subject"], "from": msg["from"]["emailAddress"]["address"]}
+ for msg in data.get("value", [])
+ ]
+```
+
+The `graph_token` parameter receives a ready-to-use access token for Microsoft Graph. FastMCP handles the OBO exchange transparently—your function just uses the token to call the API.
+
+
+**Scope alignment is critical.** The scopes passed to `EntraOBOToken` must be a subset of the scopes in `additional_authorize_scopes`. If you request a scope during OBO that wasn't included in the initial authorization, the exchange will fail.
+
+
+
+For advanced OBO scenarios, use `CurrentAccessToken()` to get the user's token, then construct an `azure.identity.aio.OnBehalfOfCredential` directly with your Azure credentials.
+
+
+
+For a complete working example of Azure OBO with FastMCP, see [Pamela Fox's blog post on OBO flow for Entra-based MCP servers](https://blog.pamelafox.org/2026/01/using-on-behalf-of-flow-for-entra-based.html).
+
+
+## Azure AD B2C
+
+
+
+Azure AD B2C (Business-to-Consumer) uses different endpoints, scope URIs, and
+token issuers than standard Microsoft Entra ID. The `AzureProvider.from_b2c()`
+factory handles all of these differences automatically.
+
+
+Azure AD B2C does **not** support the On-Behalf-Of (OBO) flow. If you need
+OBO for downstream API calls, use `AzureProvider` with standard Entra ID
+instead.
+
+
+### Quick Start
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.azure import AzureProvider
+
+auth = AzureProvider.from_b2c(
+ tenant_name="mytenant",
+ policy_name="B2C_1_susi",
+ client_id="00000000-0000-0000-0000-000000000000",
+ client_secret="my-secret",
+ required_scopes=["mcp-access"],
+ base_url="https://myserver.com",
+)
+
+mcp = FastMCP("My App", auth=auth)
+```
+
+`from_b2c()` derives the following values automatically:
+
+| Derived value | Formula |
+|---|---|
+| Authority host | `{tenant_name}.b2clogin.com` |
+| Authorization endpoint | `https://{tenant_name}.b2clogin.com/{tenant_name}.onmicrosoft.com/{policy_name}/oauth2/v2.0/authorize` |
+| Token endpoint | `https://{tenant_name}.b2clogin.com/{tenant_name}.onmicrosoft.com/{policy_name}/oauth2/v2.0/token` |
+| Scope identifier URI | `https://{tenant_name}.onmicrosoft.com/{client_id}` |
+
+### Token Issuer Validation
+
+B2C access tokens carry the **tenant GUID** (not the `.onmicrosoft.com` name)
+in the `iss` claim, and the exact format varies by policy and custom-domain
+configuration. `from_b2c()` therefore **disables issuer validation by
+default**; **audience validation still enforces that tokens target the correct
+application**.
+
+Once you have confirmed a successful end-to-end login, read the actual `iss`
+value from the decoded claims and enable strict validation:
+
+```python
+auth = AzureProvider.from_b2c(
+ tenant_name="mytenant",
+ policy_name="B2C_1_susi",
+ client_id="00000000-0000-0000-0000-000000000000",
+ client_secret="my-secret",
+ required_scopes=["mcp-access"],
+ base_url="https://myserver.com",
+ token_issuer="https://mytenant.b2clogin.com/11111111-2222-3333-4444-555555555555/v2.0/",
+)
+```
+
+### Custom Domains
+
+If your B2C tenant uses a [custom domain](https://learn.microsoft.com/en-us/azure/active-directory-b2c/custom-domain)
+(e.g. `auth.mycompany.com` instead of `mytenant.b2clogin.com`), pass it via
+`custom_domain`:
+
+```python
+auth = AzureProvider.from_b2c(
+ tenant_name="mytenant",
+ policy_name="B2C_1_susi",
+ client_id="00000000-0000-0000-0000-000000000000",
+ client_secret="my-secret",
+ required_scopes=["mcp-access"],
+ base_url="https://myserver.com",
+ custom_domain="auth.mycompany.com",
+)
+```
diff --git a/docs/v3/integrations/chatgpt.mdx b/docs/v3/integrations/chatgpt.mdx
new file mode 100644
index 000000000..23249f92c
--- /dev/null
+++ b/docs/v3/integrations/chatgpt.mdx
@@ -0,0 +1,157 @@
+---
+title: ChatGPT 🤝 FastMCP
+sidebarTitle: ChatGPT
+description: Connect FastMCP servers to ChatGPT in Chat and Deep Research modes
+icon: message-smile
+---
+
+[ChatGPT](https://chatgpt.com/) supports MCP servers through remote HTTP connections in two modes: **Chat mode** for interactive conversations and **Deep Research mode** for comprehensive information retrieval.
+
+
+**Developer Mode Required for Chat Mode**: To use MCP servers in regular ChatGPT conversations, you must first enable Developer Mode in your ChatGPT settings. This feature is available for ChatGPT Pro, Team, Enterprise, and Edu users.
+
+
+
+OpenAI's official MCP documentation and examples are built with **FastMCP v2**! Learn more from their [MCP documentation](https://platform.openai.com/docs/mcp) and [Developer Mode guide](https://platform.openai.com/docs/guides/developer-mode).
+
+
+## Build a Server
+
+First, let's create a simple FastMCP server:
+
+```python server.py
+from fastmcp import FastMCP
+import random
+
+mcp = FastMCP("Demo Server")
+
+@mcp.tool
+def roll_dice(sides: int = 6) -> int:
+ """Roll a dice with the specified number of sides."""
+ return random.randint(1, sides)
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
+```
+
+### Deploy Your Server
+
+Your server must be accessible from the internet. For development, use `ngrok`:
+
+
+```bash Terminal 1
+python server.py
+```
+
+```bash Terminal 2
+ngrok http 8000
+```
+
+
+Note your public URL (e.g., `https://abc123.ngrok.io`) for the next steps.
+
+## Chat Mode
+
+Chat mode lets you use MCP tools directly in ChatGPT conversations. See [OpenAI's Developer Mode guide](https://platform.openai.com/docs/guides/developer-mode) for the latest requirements.
+
+### Add to ChatGPT
+
+#### 1. Enable Developer Mode
+
+1. Open ChatGPT and go to **Settings** → **Connectors**
+2. Under **Advanced**, toggle **Developer Mode** to enabled
+
+#### 2. Create Connector
+
+1. In **Settings** → **Connectors**, click **Create**
+2. Enter:
+ - **Name**: Your server name
+ - **Server URL**: `https://your-server.ngrok.io/mcp/`
+3. Check **I trust this provider**
+4. Add authentication if needed
+5. Click **Create**
+
+
+**Without Developer Mode**: If you don't have search/fetch tools, ChatGPT will reject the server. With Developer Mode enabled, you don't need search/fetch tools for Chat mode.
+
+
+#### 3. Use in Chat
+
+1. Start a new chat
+2. Click the **+** button → **More** → **Developer Mode**
+3. **Enable your MCP server connector** (required - the connector must be explicitly added to each chat)
+4. Now you can use your tools:
+
+Example usage:
+- "Roll a 20-sided dice"
+- "Roll dice" (uses default 6 sides)
+
+
+The connector must be explicitly enabled in each chat session through Developer Mode. Once added, it remains active for the entire conversation.
+
+
+### Skip Confirmations
+
+Use `annotations=ToolAnnotations(readOnlyHint=True)` to skip confirmation prompts for read-only tools:
+
+```python
+from mcp.types import ToolAnnotations
+
+@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
+def get_status() -> str:
+ """Check system status."""
+ return "All systems operational"
+
+@mcp.tool() # No annotation - ChatGPT may ask for confirmation
+def delete_item(id: str) -> str:
+ """Delete an item."""
+ return f"Deleted {id}"
+```
+
+## Deep Research Mode
+
+Deep Research mode provides systematic information retrieval with citations. See [OpenAI's MCP documentation](https://platform.openai.com/docs/mcp) for the latest Deep Research specifications.
+
+
+**Search and Fetch Required**: Without Developer Mode, ChatGPT will reject any server that doesn't have both `search` and `fetch` tools. Even in Developer Mode, Deep Research only uses these two tools.
+
+
+### Tool Implementation
+
+Deep Research tools must follow this pattern:
+
+```python
+@mcp.tool()
+def search(query: str) -> dict:
+ """
+ Search for records matching the query.
+ Must return {"ids": [list of string IDs]}
+ """
+ # Your search logic
+ matching_ids = ["id1", "id2", "id3"]
+ return {"ids": matching_ids}
+
+@mcp.tool()
+def fetch(id: str) -> dict:
+ """
+ Fetch a complete record by ID.
+ Return the full record data for ChatGPT to analyze.
+ """
+ # Your fetch logic
+ return {
+ "id": id,
+ "title": "Record Title",
+ "content": "Full record content...",
+ "metadata": {"author": "Jane Doe", "date": "2024"}
+ }
+```
+
+### Using Deep Research
+
+1. Ensure your server is added to ChatGPT's connectors (same as Chat mode)
+2. Start a new chat
+3. Click **+** → **Deep Research**
+4. Select your MCP server as a source
+5. Ask research questions
+
+ChatGPT will use your `search` and `fetch` tools to find and cite relevant information.
diff --git a/docs/v3/integrations/claude-code.mdx b/docs/v3/integrations/claude-code.mdx
new file mode 100644
index 000000000..8098ff51e
--- /dev/null
+++ b/docs/v3/integrations/claude-code.mdx
@@ -0,0 +1,177 @@
+---
+title: Claude Code 🤝 FastMCP
+sidebarTitle: Claude Code
+description: Install and use FastMCP servers in Claude Code
+icon: message-smile
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+import { LocalFocusTip } from "/snippets/local-focus.mdx"
+
+
+
+[Claude Code](https://docs.anthropic.com/en/docs/claude-code) supports MCP servers through multiple transport methods including STDIO, SSE, and HTTP, allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers.
+
+## Requirements
+
+This integration uses STDIO transport to run your FastMCP server locally. For remote deployments, you can run your FastMCP server with HTTP or SSE transport and configure it directly using Claude Code's built-in MCP management commands.
+
+## Create a Server
+
+The examples in this guide will use the following simple dice-rolling server, saved as `server.py`.
+
+```python server.py
+import random
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="Dice Roller")
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ """Roll `n_dice` 6-sided dice and return the results."""
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+## Install the Server
+
+### FastMCP CLI
+
+
+The easiest way to install a FastMCP server in Claude Code is using the `fastmcp install claude-code` command. This automatically handles the configuration, dependency management, and calls Claude Code's built-in MCP management system.
+
+```bash
+fastmcp install claude-code server.py
+```
+
+The install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:
+
+```bash
+# These are equivalent if your server object is named 'mcp'
+fastmcp install claude-code server.py
+fastmcp install claude-code server.py:mcp
+
+# Use explicit object name if your server has a different name
+fastmcp install claude-code server.py:my_custom_server
+```
+
+The command will automatically configure the server with Claude Code's `claude mcp add` command.
+
+#### Dependencies
+
+FastMCP provides flexible dependency management options for your Claude Code servers:
+
+**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times:
+
+```bash
+fastmcp install claude-code server.py --with pandas --with requests
+```
+
+**Requirements file**: If you maintain a `requirements.txt` file with all your dependencies, use `--with-requirements` to install them:
+
+```bash
+fastmcp install claude-code server.py --with-requirements requirements.txt
+```
+
+**Editable packages**: For local packages under development, use `--with-editable` to install them in editable mode:
+
+```bash
+fastmcp install claude-code server.py --with-editable ./my-local-package
+```
+
+Alternatively, you can use a `fastmcp.json` configuration file (recommended):
+
+```json fastmcp.json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py",
+ "entrypoint": "mcp"
+ },
+ "environment": {
+ "dependencies": ["pandas", "requests"]
+ }
+}
+```
+
+
+#### Python Version and Project Configuration
+
+Control the Python environment for your server with these options:
+
+**Python version**: Use `--python` to specify which Python version your server requires. This ensures compatibility when your server needs specific Python features:
+
+```bash
+fastmcp install claude-code server.py --python 3.11
+```
+
+**Project directory**: Use `--project` to run your server within a specific project context. This tells `uv` to use the project's configuration files and virtual environment:
+
+```bash
+fastmcp install claude-code server.py --project /path/to/my-project
+```
+
+#### Environment Variables
+
+If your server needs environment variables (like API keys), you must include them:
+
+```bash
+fastmcp install claude-code server.py --server-name "Weather Server" \
+ --env API_KEY=your-api-key \
+ --env DEBUG=true
+```
+
+Or load them from a `.env` file:
+
+```bash
+fastmcp install claude-code server.py --server-name "Weather Server" --env-file .env
+```
+
+
+**Claude Code must be installed**. The integration looks for the Claude Code CLI at the default installation location (`~/.claude/local/claude`) and uses the `claude mcp add` command to register servers.
+
+
+### Manual Configuration
+
+For more control over the configuration, you can manually use Claude Code's built-in MCP management commands. This gives you direct control over how your server is launched:
+
+```bash
+# Add a server with custom configuration
+claude mcp add dice-roller -- uv run --with fastmcp fastmcp run server.py
+
+# Add with environment variables
+claude mcp add weather-server -e API_KEY=secret -e DEBUG=true -- uv run --with fastmcp fastmcp run server.py
+
+# Add with specific scope (local, user, or project)
+claude mcp add my-server --scope user -- uv run --with fastmcp fastmcp run server.py
+```
+
+You can also manually specify Python versions and project directories in your Claude Code commands:
+
+```bash
+# With specific Python version
+claude mcp add ml-server -- uv run --python 3.11 --with fastmcp fastmcp run server.py
+
+# Within a project directory
+claude mcp add project-server -- uv run --project /path/to/project --with fastmcp fastmcp run server.py
+```
+
+## Using the Server
+
+Once your server is installed, you can start using your FastMCP server with Claude Code.
+
+Try asking Claude something like:
+
+> "Roll some dice for me"
+
+Claude will automatically detect your `roll_dice` tool and use it to fulfill your request, returning something like:
+
+> I'll roll some dice for you! Here are your results: [4, 2, 6]
+>
+> You rolled three dice and got a 4, a 2, and a 6!
+
+Claude Code can now access all the tools, resources, and prompts you've defined in your FastMCP server.
+
+If your server provides resources, you can reference them with `@` mentions using the format `@server:protocol://resource/path`. If your server provides prompts, you can use them as slash commands with `/mcp__servername__promptname`.
\ No newline at end of file
diff --git a/docs/v3/integrations/claude-desktop.mdx b/docs/v3/integrations/claude-desktop.mdx
new file mode 100644
index 000000000..4478bcc37
--- /dev/null
+++ b/docs/v3/integrations/claude-desktop.mdx
@@ -0,0 +1,299 @@
+---
+title: Claude Desktop 🤝 FastMCP
+sidebarTitle: Claude Desktop
+description: Connect FastMCP servers to Claude Desktop
+icon: message-smile
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+import { LocalFocusTip } from "/snippets/local-focus.mdx"
+
+
+
+[Claude Desktop](https://www.claude.com/download) supports MCP servers through local STDIO connections and remote servers (beta), allowing you to extend Claude's capabilities with custom tools, resources, and prompts from your FastMCP servers.
+
+
+Remote MCP server support is currently in beta and available for users on Claude Pro, Max, Team, and Enterprise plans (as of June 2025). Most users will still need to use local STDIO connections.
+
+
+
+This guide focuses specifically on using FastMCP servers with Claude Desktop. For general Claude Desktop MCP setup and official examples, see the [official Claude Desktop quickstart guide](https://modelcontextprotocol.io/quickstart/user).
+
+
+
+## Requirements
+
+Claude Desktop traditionally requires MCP servers to run locally using STDIO transport, where your server communicates with Claude through standard input/output rather than HTTP. However, users on certain plans now have access to remote server support as well.
+
+
+If you don't have access to remote server support or need to connect to remote servers, you can create a **proxy server** that runs locally via STDIO and forwards requests to remote HTTP servers. See the [Proxy Servers](#proxy-servers) section below.
+
+
+## Create a Server
+
+The examples in this guide will use the following simple dice-rolling server, saved as `server.py`.
+
+```python server.py
+import random
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="Dice Roller")
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ """Roll `n_dice` 6-sided dice and return the results."""
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+## Install the Server
+
+### FastMCP CLI
+
+
+The easiest way to install a FastMCP server in Claude Desktop is using the `fastmcp install claude-desktop` command. This automatically handles the configuration and dependency management.
+
+
+Prior to version 2.10.3, Claude Desktop could be managed by running `fastmcp install ` without specifying the client.
+
+
+```bash
+fastmcp install claude-desktop server.py
+```
+
+The install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:
+
+```bash
+# These are equivalent if your server object is named 'mcp'
+fastmcp install claude-desktop server.py
+fastmcp install claude-desktop server.py:mcp
+
+# Use explicit object name if your server has a different name
+fastmcp install claude-desktop server.py:my_custom_server
+```
+
+After installation, restart Claude Desktop completely. You should see a hammer icon (🔨) in the bottom left of the input box, indicating that MCP tools are available.
+
+#### Dependencies
+
+FastMCP provides several ways to manage your server's dependencies when installing in Claude Desktop:
+
+**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times:
+
+```bash
+fastmcp install claude-desktop server.py --with pandas --with requests
+```
+
+**Requirements file**: If you have a `requirements.txt` file listing all your dependencies, use `--with-requirements` to install them all at once:
+
+```bash
+fastmcp install claude-desktop server.py --with-requirements requirements.txt
+```
+
+**Editable packages**: For local packages in development, use `--with-editable` to install them in editable mode:
+
+```bash
+fastmcp install claude-desktop server.py --with-editable ./my-local-package
+```
+
+Alternatively, you can use a `fastmcp.json` configuration file (recommended):
+
+```json fastmcp.json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py",
+ "entrypoint": "mcp"
+ },
+ "environment": {
+ "dependencies": ["pandas", "requests"]
+ }
+}
+```
+
+
+#### Python Version and Project Directory
+
+FastMCP allows you to control the Python environment for your server:
+
+**Python version**: Use `--python` to specify which Python version your server should run with. This is particularly useful when your server requires a specific Python version:
+
+```bash
+fastmcp install claude-desktop server.py --python 3.11
+```
+
+**Project directory**: Use `--project` to run your server within a specific project directory. This ensures that `uv` will discover all `pyproject.toml`, `uv.toml`, and `.python-version` files from that project:
+
+```bash
+fastmcp install claude-desktop server.py --project /path/to/my-project
+```
+
+When you specify a project directory, all relative paths in your server will be resolved from that directory, and the project's virtual environment will be used.
+
+#### Environment Variables
+
+
+Claude Desktop runs servers in a completely isolated environment with no access to your shell environment or locally installed applications. You must explicitly pass any environment variables your server needs.
+
+
+If your server needs environment variables (like API keys), you must include them:
+
+```bash
+fastmcp install claude-desktop server.py --server-name "Weather Server" \
+ --env API_KEY=your-api-key \
+ --env DEBUG=true
+```
+
+Or load them from a `.env` file:
+
+```bash
+fastmcp install claude-desktop server.py --server-name "Weather Server" --env-file .env
+```
+
+- **`uv` must be installed and available in your system PATH**. Claude Desktop runs in its own isolated environment and needs `uv` to manage dependencies.
+- **On macOS, it is recommended to install `uv` globally with Homebrew** so that Claude Desktop will detect it: `brew install uv`. Installing `uv` with other methods may not make it accessible to Claude Desktop.
+
+
+
+### Manual Configuration
+
+For more control over the configuration, you can manually edit Claude Desktop's configuration file. You can open the configuration file from Claude's developer settings, or find it in the following locations:
+- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
+- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
+
+The configuration file is a JSON object with a `mcpServers` key, which contains the configuration for each MCP server.
+
+```json
+{
+ "mcpServers": {
+ "dice-roller": {
+ "command": "python",
+ "args": ["path/to/your/server.py"]
+ }
+ }
+}
+```
+
+After updating the configuration file, restart Claude Desktop completely. Look for the hammer icon (🔨) to confirm your server is loaded.
+
+#### Dependencies
+
+If your server has dependencies, you can use `uv` or another package manager to set up the environment.
+
+
+When manually configuring dependencies, the recommended approach is to use `uv` with FastMCP. The configuration uses `uv run` to create an isolated environment with your specified packages:
+
+```json
+{
+ "mcpServers": {
+ "dice-roller": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--with", "fastmcp",
+ "--with", "pandas",
+ "--with", "requests",
+ "fastmcp",
+ "run",
+ "path/to/your/server.py"
+ ]
+ }
+ }
+}
+```
+
+You can also manually specify Python versions and project directories in your configuration. Add `--python` to use a specific Python version, or `--project` to run within a project directory:
+
+```json
+{
+ "mcpServers": {
+ "dice-roller": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--python", "3.11",
+ "--project", "/path/to/project",
+ "--with", "fastmcp",
+ "fastmcp",
+ "run",
+ "path/to/your/server.py"
+ ]
+ }
+ }
+}
+```
+
+The order of arguments matters: Python version and project settings come before package specifications, which come before the actual command to run.
+
+
+- **`uv` must be installed and available in your system PATH**. Claude Desktop runs in its own isolated environment and needs `uv` to manage dependencies.
+- **On macOS, it is recommended to install `uv` globally with Homebrew** so that Claude Desktop will detect it: `brew install uv`. Installing `uv` with other methods may not make it accessible to Claude Desktop.
+
+
+#### Environment Variables
+
+You can also specify environment variables in the configuration:
+
+```json
+{
+ "mcpServers": {
+ "weather-server": {
+ "command": "python",
+ "args": ["path/to/weather_server.py"],
+ "env": {
+ "API_KEY": "your-api-key",
+ "DEBUG": "true"
+ }
+ }
+ }
+}
+```
+
+Claude Desktop runs servers in a completely isolated environment with no access to your shell environment or locally installed applications. You must explicitly pass any environment variables your server needs.
+
+
+
+## Remote Servers
+
+
+Users on Claude Pro, Max, Team, and Enterprise plans have first-class remote server support via integrations. For other users, or as an alternative approach, FastMCP can create a proxy server that forwards requests to a remote HTTP server. You can install the proxy server in Claude Desktop.
+
+Create a proxy server that connects to a remote HTTP server:
+
+```python proxy_server.py
+from fastmcp.server import create_proxy
+
+# Create a proxy to a remote server
+proxy = create_proxy(
+ "https://example.com/mcp/sse",
+ name="Remote Server Proxy"
+)
+
+if __name__ == "__main__":
+ proxy.run() # Runs via STDIO for Claude Desktop
+```
+
+### Authentication
+
+For authenticated remote servers, create an authenticated client following the guidance in the [client auth documentation](/clients/auth/bearer) and pass it to the proxy:
+
+```python auth_proxy_server.py {7}
+from fastmcp import Client
+from fastmcp.client.auth import BearerAuth
+from fastmcp.server import create_proxy
+
+# Create authenticated client
+client = Client(
+ "https://api.example.com/mcp/sse",
+ auth=BearerAuth(token="your-access-token")
+)
+
+# Create proxy using the authenticated client
+proxy = create_proxy(client, name="Authenticated Proxy")
+
+if __name__ == "__main__":
+ proxy.run()
+```
+
diff --git a/docs/v3/integrations/cursor-install-mcp.png b/docs/v3/integrations/cursor-install-mcp.png
new file mode 100644
index 000000000..5681d70d7
Binary files /dev/null and b/docs/v3/integrations/cursor-install-mcp.png differ
diff --git a/docs/v3/integrations/cursor.mdx b/docs/v3/integrations/cursor.mdx
new file mode 100644
index 000000000..da0744ee0
--- /dev/null
+++ b/docs/v3/integrations/cursor.mdx
@@ -0,0 +1,284 @@
+---
+title: Cursor 🤝 FastMCP
+sidebarTitle: Cursor
+description: Install and use FastMCP servers in Cursor
+icon: message-smile
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+import { LocalFocusTip } from "/snippets/local-focus.mdx"
+
+
+
+[Cursor](https://www.cursor.com/) supports MCP servers through multiple transport methods including STDIO, SSE, and Streamable HTTP, allowing you to extend Cursor's AI assistant with custom tools, resources, and prompts from your FastMCP servers.
+
+## Requirements
+
+This integration uses STDIO transport to run your FastMCP server locally. For remote deployments, you can run your FastMCP server with HTTP or SSE transport and configure it directly in Cursor's settings.
+
+## Create a Server
+
+The examples in this guide will use the following simple dice-rolling server, saved as `server.py`.
+
+```python server.py
+import random
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="Dice Roller")
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ """Roll `n_dice` 6-sided dice and return the results."""
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+## Install the Server
+
+### FastMCP CLI
+
+
+The easiest way to install a FastMCP server in Cursor is using the `fastmcp install cursor` command. This automatically handles the configuration, dependency management, and opens Cursor with a deeplink to install the server.
+
+```bash
+fastmcp install cursor server.py
+```
+
+#### Workspace Installation
+
+
+By default, FastMCP installs servers globally for Cursor. You can also install servers to project-specific workspaces using the `--workspace` flag:
+
+```bash
+# Install to current directory's .cursor/ folder
+fastmcp install cursor server.py --workspace .
+
+# Install to specific workspace
+fastmcp install cursor server.py --workspace /path/to/project
+```
+
+This creates a `.cursor/mcp.json` configuration file in the specified workspace directory, allowing different projects to have their own MCP server configurations.
+
+The install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:
+
+```bash
+# These are equivalent if your server object is named 'mcp'
+fastmcp install cursor server.py
+fastmcp install cursor server.py:mcp
+
+# Use explicit object name if your server has a different name
+fastmcp install cursor server.py:my_custom_server
+```
+
+After running the command, Cursor will open automatically and prompt you to install the server. The command will be `uv`, which is expected as this is a Python STDIO server. Click "Install" to confirm:
+
+
+
+#### Dependencies
+
+FastMCP offers multiple ways to manage dependencies for your Cursor servers:
+
+**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times:
+
+```bash
+fastmcp install cursor server.py --with pandas --with requests
+```
+
+**Requirements file**: For projects with a `requirements.txt` file, use `--with-requirements` to install all dependencies at once:
+
+```bash
+fastmcp install cursor server.py --with-requirements requirements.txt
+```
+
+**Editable packages**: When developing local packages, use `--with-editable` to install them in editable mode:
+
+```bash
+fastmcp install cursor server.py --with-editable ./my-local-package
+```
+
+Alternatively, you can use a `fastmcp.json` configuration file (recommended):
+
+```json fastmcp.json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py",
+ "entrypoint": "mcp"
+ },
+ "environment": {
+ "dependencies": ["pandas", "requests"]
+ }
+}
+```
+
+
+#### Python Version and Project Configuration
+
+Control your server's Python environment with these options:
+
+**Python version**: Use `--python` to specify which Python version your server should use. This is essential when your server requires specific Python features:
+
+```bash
+fastmcp install cursor server.py --python 3.11
+```
+
+**Project directory**: Use `--project` to run your server within a specific project context. This ensures `uv` discovers all project configuration files and uses the correct virtual environment:
+
+```bash
+fastmcp install cursor server.py --project /path/to/my-project
+```
+
+#### Environment Variables
+
+
+Cursor runs servers in a completely isolated environment with no access to your shell environment or locally installed applications. You must explicitly pass any environment variables your server needs.
+
+
+If your server needs environment variables (like API keys), you must include them:
+
+```bash
+fastmcp install cursor server.py --server-name "Weather Server" \
+ --env API_KEY=your-api-key \
+ --env DEBUG=true
+```
+
+Or load them from a `.env` file:
+
+```bash
+fastmcp install cursor server.py --server-name "Weather Server" --env-file .env
+```
+
+
+**`uv` must be installed and available in your system PATH**. Cursor runs in its own isolated environment and needs `uv` to manage dependencies.
+
+
+### Generate MCP JSON
+
+
+**Use the first-class integration above for the best experience.** The MCP JSON generation is useful for advanced use cases, manual configuration, or integration with other tools.
+
+
+You can generate MCP JSON configuration for manual use:
+
+```bash
+# Generate configuration and output to stdout
+fastmcp install mcp-json server.py --server-name "Dice Roller" --with pandas
+
+# Copy configuration to clipboard for easy pasting
+fastmcp install mcp-json server.py --server-name "Dice Roller" --copy
+```
+
+This generates the standard `mcpServers` configuration format that can be used with any MCP-compatible client.
+
+### Manual Configuration
+
+For more control over the configuration, you can manually edit Cursor's configuration file. The configuration file is located at:
+- **All platforms**: `~/.cursor/mcp.json`
+
+The configuration file is a JSON object with a `mcpServers` key, which contains the configuration for each MCP server.
+
+```json
+{
+ "mcpServers": {
+ "dice-roller": {
+ "command": "python",
+ "args": ["path/to/your/server.py"]
+ }
+ }
+}
+```
+
+After updating the configuration file, your server should be available in Cursor.
+
+#### Dependencies
+
+If your server has dependencies, you can use `uv` or another package manager to set up the environment.
+
+When manually configuring dependencies, the recommended approach is to use `uv` with FastMCP. The configuration should use `uv run` to create an isolated environment with your specified packages:
+
+```json
+{
+ "mcpServers": {
+ "dice-roller": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--with", "fastmcp",
+ "--with", "pandas",
+ "--with", "requests",
+ "fastmcp",
+ "run",
+ "path/to/your/server.py"
+ ]
+ }
+ }
+}
+```
+
+You can also manually specify Python versions and project directories in your configuration:
+
+```json
+{
+ "mcpServers": {
+ "dice-roller": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--python", "3.11",
+ "--project", "/path/to/project",
+ "--with", "fastmcp",
+ "fastmcp",
+ "run",
+ "path/to/your/server.py"
+ ]
+ }
+ }
+}
+```
+
+Note that the order of arguments is important: Python version and project settings should come before package specifications.
+
+
+**`uv` must be installed and available in your system PATH**. Cursor runs in its own isolated environment and needs `uv` to manage dependencies.
+
+
+#### Environment Variables
+
+You can also specify environment variables in the configuration:
+
+```json
+{
+ "mcpServers": {
+ "weather-server": {
+ "command": "python",
+ "args": ["path/to/weather_server.py"],
+ "env": {
+ "API_KEY": "your-api-key",
+ "DEBUG": "true"
+ }
+ }
+ }
+}
+```
+
+
+Cursor runs servers in a completely isolated environment with no access to your shell environment or locally installed applications. You must explicitly pass any environment variables your server needs.
+
+
+## Using the Server
+
+Once your server is installed, you can start using your FastMCP server with Cursor's AI assistant.
+
+Try asking Cursor something like:
+
+> "Roll some dice for me"
+
+Cursor will automatically detect your `roll_dice` tool and use it to fulfill your request, returning something like:
+
+> 🎲 Here are your dice rolls: 4, 6, 4
+>
+> You rolled 3 dice with a total of 14! The 6 was a nice high roll there!
+
+The AI assistant can now access all the tools, resources, and prompts you've defined in your FastMCP server.
diff --git a/docs/v3/integrations/descope.mdx b/docs/v3/integrations/descope.mdx
new file mode 100644
index 000000000..bfb6cd9c8
--- /dev/null
+++ b/docs/v3/integrations/descope.mdx
@@ -0,0 +1,113 @@
+---
+title: Descope 🤝 FastMCP
+sidebarTitle: Descope
+description: Secure your FastMCP server with Descope
+icon: shield-check
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx";
+
+
+
+This guide shows you how to secure your FastMCP server using [**Descope**](https://www.descope.com), a complete authentication and user management solution. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern, where Descope handles user login and your FastMCP server validates the tokens.
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+
+1. To [sign up](https://www.descope.com/sign-up) for a Free Forever Descope account
+2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:3000`)
+
+### Step 1: Configure Descope
+
+
+
+ 1. Go to the [MCP Servers page](https://app.descope.com/mcp-servers) of the Descope Console, and create a new MCP Server.
+ 2. Give the MCP server a name and description.
+ 3. Ensure that **Dynamic Client Registration (DCR)** is enabled. Then click **Create**.
+ 4. Once you've created the MCP Server, note your Well-Known URL.
+
+
+
+ DCR is required for FastMCP clients to automatically register with your authentication server.
+
+
+
+
+ Save your Well-Known URL from [MCP Server Settings](https://app.descope.com/mcp-servers):
+ ```
+ Well-Known URL: https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration
+ ```
+
+
+
+### Step 2: Environment Setup
+
+Create a `.env` file with your Descope configuration:
+
+```bash
+DESCOPE_CONFIG_URL=https://.../v1/apps/agentic/P.../M.../.well-known/openid-configuration # Your Descope Well-Known URL
+SERVER_URL=http://localhost:3000 # Your server's base URL
+```
+
+### Step 3: FastMCP Configuration
+
+Create your FastMCP server file and use the DescopeProvider to handle all the OAuth integration automatically:
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.descope import DescopeProvider
+
+# The DescopeProvider automatically discovers Descope endpoints
+# and configures JWT token validation
+auth_provider = DescopeProvider(
+ config_url="https://.../.well-known/openid-configuration", # Your MCP Server .well-known URL
+ base_url=SERVER_URL, # Your server's public URL
+)
+
+# Create FastMCP server with auth
+mcp = FastMCP(name="My Descope Protected Server", auth=auth_provider)
+
+```
+
+## Testing
+
+To test your server, you can use the `fastmcp` CLI to run it locally. Assuming you've saved the above code to `server.py` (after replacing the environment variables with your actual values!), you can run the following command:
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+Now, you can use a FastMCP client to test that you can reach your server after authenticating:
+
+```python
+from fastmcp import Client
+import asyncio
+
+async def main():
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ assert await client.ping()
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+## Production Configuration
+
+For production deployments, load configuration from environment variables:
+
+```python server.py
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.descope import DescopeProvider
+
+# Load configuration from environment variables
+auth = DescopeProvider(
+ config_url=os.environ.get("DESCOPE_CONFIG_URL"),
+ base_url=os.environ.get("BASE_URL", "https://your-server.com")
+)
+
+mcp = FastMCP(name="My Descope Protected Server", auth=auth)
+```
diff --git a/docs/v3/integrations/discord.mdx b/docs/v3/integrations/discord.mdx
new file mode 100644
index 000000000..5d6c643b7
--- /dev/null
+++ b/docs/v3/integrations/discord.mdx
@@ -0,0 +1,183 @@
+---
+title: Discord OAuth 🤝 FastMCP
+sidebarTitle: Discord
+description: Secure your FastMCP server with Discord OAuth
+icon: discord
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+This guide shows you how to secure your FastMCP server using **Discord OAuth**. Since Discord doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/servers/auth/oauth-proxy) pattern to bridge Discord's traditional OAuth with MCP's authentication requirements.
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+1. A **[Discord Account](https://discord.com/)** with access to create applications
+2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
+
+### Step 1: Create a Discord Application
+
+Create an application in the Discord Developer Portal to get the credentials needed for authentication:
+
+
+
+ Go to the [Discord Developer Portal](https://discord.com/developers/applications).
+
+ Click **"New Application"** and give it a name users will recognize (e.g., "My FastMCP Server").
+
+
+
+ In the left sidebar, click **"OAuth2"**.
+
+ In the **Redirects** section, click **"Add Redirect"** and enter your callback URL:
+ - For development: `http://localhost:8000/auth/callback`
+ - For production: `https://your-domain.com/auth/callback`
+
+
+ The redirect URL must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. Discord allows `http://localhost` URLs for development. For production, use HTTPS.
+
+
+
+
+ On the same OAuth2 page, you'll find:
+
+ - **Client ID**: A numeric string like `12345`
+ - **Client Secret**: Click "Reset Secret" to generate one
+
+
+ Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.
+
+
+
+
+### Step 2: FastMCP Configuration
+
+Create your FastMCP server using the `DiscordProvider`, which handles Discord's OAuth flow automatically:
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.discord import DiscordProvider
+
+auth_provider = DiscordProvider(
+ client_id="12345", # Your Discord Application Client ID
+ client_secret="your-client-secret", # Your Discord OAuth Client Secret
+ base_url="http://localhost:8000", # Must match your OAuth configuration
+)
+
+mcp = FastMCP(name="Discord Secured App", auth=auth_provider)
+
+@mcp.tool
+async def get_user_info() -> dict:
+ """Returns information about the authenticated Discord user."""
+ from fastmcp.server.dependencies import get_access_token
+
+ token = get_access_token()
+ return {
+ "discord_id": token.claims.get("sub"),
+ "username": token.claims.get("username"),
+ "avatar": token.claims.get("avatar"),
+ }
+```
+
+## Testing
+
+### Running the Server
+
+Start your FastMCP server with HTTP transport to enable OAuth flows:
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+Your server is now running and protected by Discord OAuth authentication.
+
+### Testing with a Client
+
+Create a test client that authenticates with your Discord-protected server:
+
+```python test_client.py
+from fastmcp import Client
+import asyncio
+
+async def main():
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ print("✓ Authenticated with Discord!")
+
+ result = await client.call_tool("get_user_info")
+ print(f"Discord user: {result['username']}")
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+When you run the client for the first time:
+1. Your browser will open to Discord's authorization page
+2. Sign in with your Discord account and authorize the app
+3. After authorization, you'll be redirected back
+4. The client receives the token and can make authenticated requests
+
+
+The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.
+
+
+## Discord Scopes
+
+Discord OAuth supports several scopes for accessing different types of user data:
+
+| Scope | Description |
+|-------|-------------|
+| `identify` | Access username, avatar, and discriminator (default) |
+| `email` | Access the user's email address |
+| `guilds` | Access the user's list of servers |
+| `guilds.join` | Ability to add the user to a server |
+
+To request additional scopes:
+
+```python
+auth_provider = DiscordProvider(
+ client_id="...",
+ client_secret="...",
+ base_url="http://localhost:8000",
+ required_scopes=["identify", "email"],
+)
+```
+
+## Production Configuration
+
+For production deployments with persistent token management across server restarts, configure `jwt_signing_key` and `client_storage`:
+
+```python server.py
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.discord import DiscordProvider
+from key_value.aio.stores.redis import RedisStore
+from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
+from cryptography.fernet import Fernet
+
+auth_provider = DiscordProvider(
+ client_id="12345",
+ client_secret=os.environ["DISCORD_CLIENT_SECRET"],
+ base_url="https://your-production-domain.com",
+
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ client_storage=FernetEncryptionWrapper(
+ key_value=RedisStore(
+ host=os.environ["REDIS_HOST"],
+ port=int(os.environ["REDIS_PORT"])
+ ),
+ fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
+ )
+)
+
+mcp = FastMCP(name="Production Discord App", auth=auth_provider)
+```
+
+
+Parameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.
+
+For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
+
diff --git a/docs/v3/integrations/eunomia-authorization.mdx b/docs/v3/integrations/eunomia-authorization.mdx
new file mode 100644
index 000000000..2fd2ca4a5
--- /dev/null
+++ b/docs/v3/integrations/eunomia-authorization.mdx
@@ -0,0 +1,129 @@
+---
+title: Eunomia Authorization 🤝 FastMCP
+sidebarTitle: Eunomia Auth
+description: Add policy-based authorization to your FastMCP servers with Eunomia
+icon: shield-check
+---
+
+Add **policy-based authorization** to your FastMCP servers with one-line code addition with the **[Eunomia][eunomia-github] authorization middleware**.
+
+Control which tools, resources and prompts MCP clients can view and execute on your server. Define dynamic JSON-based policies and obtain a comprehensive audit log of all access attempts and violations.
+
+## How it Works
+
+Exploiting FastMCP's [Middleware][fastmcp-middleware], the Eunomia middleware intercepts all MCP requests to your server and automatically maps MCP methods to authorization checks.
+
+### Listing Operations
+
+The middleware behaves as a filter for listing operations (`tools/list`, `resources/list`, `prompts/list`), hiding to the client components that are not authorized by the defined policies.
+
+```mermaid
+sequenceDiagram
+ participant MCPClient as MCP Client
+ participant EunomiaMiddleware as Eunomia Middleware
+ participant MCPServer as FastMCP Server
+ participant EunomiaServer as Eunomia Server
+
+ MCPClient->>EunomiaMiddleware: MCP Listing Request (e.g., tools/list)
+ EunomiaMiddleware->>MCPServer: MCP Listing Request
+ MCPServer-->>EunomiaMiddleware: MCP Listing Response
+ EunomiaMiddleware->>EunomiaServer: Authorization Checks
+ EunomiaServer->>EunomiaMiddleware: Authorization Decisions
+ EunomiaMiddleware-->>MCPClient: Filtered MCP Listing Response
+```
+
+### Execution Operations
+
+The middleware behaves as a firewall for execution operations (`tools/call`, `resources/read`, `prompts/get`), blocking operations that are not authorized by the defined policies.
+
+```mermaid
+sequenceDiagram
+ participant MCPClient as MCP Client
+ participant EunomiaMiddleware as Eunomia Middleware
+ participant MCPServer as FastMCP Server
+ participant EunomiaServer as Eunomia Server
+
+ MCPClient->>EunomiaMiddleware: MCP Execution Request (e.g., tools/call)
+ EunomiaMiddleware->>EunomiaServer: Authorization Check
+ EunomiaServer->>EunomiaMiddleware: Authorization Decision
+ EunomiaMiddleware-->>MCPClient: MCP Unauthorized Error (if denied)
+ EunomiaMiddleware->>MCPServer: MCP Execution Request (if allowed)
+ MCPServer-->>EunomiaMiddleware: MCP Execution Response (if allowed)
+ EunomiaMiddleware-->>MCPClient: MCP Execution Response (if allowed)
+```
+
+## Add Authorization to Your Server
+
+
+Eunomia is an AI-specific authorization server that handles policy decisions. The server runs embedded within your MCP server by default for a zero-effort configuration, but can alternatively be run remotely for centralized policy decisions.
+
+
+
+### Create a Server with Authorization
+
+First, install the `eunomia-mcp` package:
+
+```bash
+pip install eunomia-mcp
+```
+
+Then create a FastMCP server and add the Eunomia middleware in one line:
+
+```python server.py
+from fastmcp import FastMCP
+from eunomia_mcp import create_eunomia_middleware
+
+# Create your FastMCP server
+mcp = FastMCP("Secure MCP Server 🔒")
+
+@mcp.tool()
+def add(a: int, b: int) -> int:
+ """Add two numbers"""
+ return a + b
+
+# Add middleware to your server
+middleware = create_eunomia_middleware(policy_file="mcp_policies.json")
+mcp.add_middleware(middleware)
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+### Configure Access Policies
+
+Use the `eunomia-mcp` CLI in your terminal to manage your authorization policies:
+
+```bash
+# Create a default policy file
+eunomia-mcp init
+
+# Or create a policy file customized for your FastMCP server
+eunomia-mcp init --custom-mcp "app.server:mcp"
+```
+
+This creates `mcp_policies.json` file that you can further edit to your access control needs.
+
+```bash
+# Once edited, validate your policy file
+eunomia-mcp validate mcp_policies.json
+```
+
+### Run the Server
+
+Start your FastMCP server normally:
+
+```bash
+python server.py
+```
+
+The middleware will now intercept all MCP requests and check them against your policies. Requests include agent identification through headers like `X-Agent-ID`, `X-User-ID`, `User-Agent`, or `Authorization` and an automatic mapping of MCP methods to authorization resources and actions.
+
+
+ For detailed policy configuration, custom authentication, and remote
+ deployments, visit the [Eunomia MCP Middleware
+ repository][eunomia-mcp-github].
+
+
+[eunomia-github]: https://github.com/whataboutyou-ai/eunomia
+[eunomia-mcp-github]: https://github.com/whataboutyou-ai/eunomia/tree/main/pkgs/extensions/mcp
+[fastmcp-middleware]: /servers/middleware
diff --git a/docs/v3/integrations/fastapi.mdx b/docs/v3/integrations/fastapi.mdx
new file mode 100644
index 000000000..83aa924f8
--- /dev/null
+++ b/docs/v3/integrations/fastapi.mdx
@@ -0,0 +1,445 @@
+---
+title: FastAPI 🤝 FastMCP
+sidebarTitle: FastAPI
+description: Integrate FastMCP with FastAPI applications
+icon: bolt
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+FastMCP provides two powerful ways to integrate with FastAPI applications:
+
+1. **[Generate an MCP server FROM your FastAPI app](#generating-an-mcp-server)** - Convert existing API endpoints into MCP tools
+2. **[Mount an MCP server INTO your FastAPI app](#mounting-an-mcp-server)** - Add MCP functionality to your web application
+
+
+When generating an MCP server from FastAPI, FastMCP uses OpenAPIProvider (v3.0.0+) under the hood to source tools from your FastAPI app's OpenAPI spec. See [Providers](/servers/providers/overview) to understand how FastMCP sources components.
+
+
+
+
+Generating MCP servers from OpenAPI is a great way to get started with FastMCP, but in practice LLMs achieve **significantly better performance** with well-designed and curated MCP servers than with auto-converted OpenAPI servers. This is especially true for complex APIs with many endpoints and parameters.
+
+We recommend using the FastAPI integration for bootstrapping and prototyping, not for mirroring your API to LLM clients. See the post [Stop Converting Your REST APIs to MCP](https://www.jlowin.dev/blog/stop-converting-rest-apis-to-mcp) for more details.
+
+
+
+
+FastMCP does *not* include FastAPI as a dependency; you must install it separately to use this integration.
+
+
+## Example FastAPI Application
+
+Throughout this guide, we'll use this e-commerce API as our example (click the `Copy` button to copy it for use with other code blocks):
+
+```python [expandable]
+# Copy this FastAPI server into other code blocks in this guide
+
+from fastapi import FastAPI, HTTPException
+from pydantic import BaseModel
+
+# Models
+class Product(BaseModel):
+ name: str
+ price: float
+ category: str
+ description: str | None = None
+
+class ProductResponse(BaseModel):
+ id: int
+ name: str
+ price: float
+ category: str
+ description: str | None = None
+
+# Create FastAPI app
+app = FastAPI(title="E-commerce API", version="1.0.0")
+
+# In-memory database
+products_db = {
+ 1: ProductResponse(
+ id=1, name="Laptop", price=999.99, category="Electronics"
+ ),
+ 2: ProductResponse(
+ id=2, name="Mouse", price=29.99, category="Electronics"
+ ),
+ 3: ProductResponse(
+ id=3, name="Desk Chair", price=299.99, category="Furniture"
+ ),
+}
+next_id = 4
+
+@app.get("/products", response_model=list[ProductResponse])
+def list_products(
+ category: str | None = None,
+ max_price: float | None = None,
+) -> list[ProductResponse]:
+ """List all products with optional filtering."""
+ products = list(products_db.values())
+ if category:
+ products = [p for p in products if p.category == category]
+ if max_price:
+ products = [p for p in products if p.price <= max_price]
+ return products
+
+@app.get("/products/{product_id}", response_model=ProductResponse)
+def get_product(product_id: int):
+ """Get a specific product by ID."""
+ if product_id not in products_db:
+ raise HTTPException(status_code=404, detail="Product not found")
+ return products_db[product_id]
+
+@app.post("/products", response_model=ProductResponse)
+def create_product(product: Product):
+ """Create a new product."""
+ global next_id
+ product_response = ProductResponse(id=next_id, **product.model_dump())
+ products_db[next_id] = product_response
+ next_id += 1
+ return product_response
+
+@app.put("/products/{product_id}", response_model=ProductResponse)
+def update_product(product_id: int, product: Product):
+ """Update an existing product."""
+ if product_id not in products_db:
+ raise HTTPException(status_code=404, detail="Product not found")
+ products_db[product_id] = ProductResponse(
+ id=product_id,
+ **product.model_dump(),
+ )
+ return products_db[product_id]
+
+@app.delete("/products/{product_id}")
+def delete_product(product_id: int):
+ """Delete a product."""
+ if product_id not in products_db:
+ raise HTTPException(status_code=404, detail="Product not found")
+ del products_db[product_id]
+ return {"message": "Product deleted"}
+```
+
+
+All subsequent code examples in this guide assume you have the above FastAPI application code already defined. Each example builds upon this base application, `app`.
+
+
+## Generating an MCP Server
+
+
+
+One of the most common ways to bootstrap an MCP server is to generate it from an existing FastAPI application. FastMCP will expose your FastAPI endpoints as MCP components (tools, by default) in order to expose your API to LLM clients.
+
+
+
+### Basic Conversion
+
+Convert the FastAPI app to an MCP server with a single line:
+
+```python {5}
+# Assumes the FastAPI app from above is already defined
+from fastmcp import FastMCP
+
+# Convert to MCP server
+mcp = FastMCP.from_fastapi(app=app)
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+### Adding Components
+
+Your converted MCP server is a full FastMCP instance, meaning you can add new tools, resources, and other components to it just like you would with any other FastMCP instance.
+
+```python {8-11}
+# Assumes the FastAPI app from above is already defined
+from fastmcp import FastMCP
+
+# Convert to MCP server
+mcp = FastMCP.from_fastapi(app=app)
+
+# Add a new tool
+@mcp.tool
+def get_product(product_id: int) -> ProductResponse:
+ """Get a product by ID."""
+ return products_db[product_id]
+
+# Run the MCP server
+if __name__ == "__main__":
+ mcp.run()
+```
+
+
+
+
+
+### Interacting with the MCP Server
+
+Once you've converted your FastAPI app to an MCP server, you can interact with it using the FastMCP client to test functionality before deploying it to an LLM-based application.
+
+```python {3, }
+# Assumes the FastAPI app from above is already defined
+from fastmcp import FastMCP
+from fastmcp.client import Client
+import asyncio
+
+# Convert to MCP server
+mcp = FastMCP.from_fastapi(app=app)
+
+async def demo():
+ async with Client(mcp) as client:
+ # List available tools
+ tools = await client.list_tools()
+ print(f"Available tools: {[t.name for t in tools]}")
+
+ # Create a product
+ result = await client.call_tool(
+ "create_product_products_post",
+ {
+ "name": "Wireless Keyboard",
+ "price": 79.99,
+ "category": "Electronics",
+ "description": "Bluetooth mechanical keyboard"
+ }
+ )
+ print(f"Created product: {result.data}")
+
+ # List electronics under $100
+ result = await client.call_tool(
+ "list_products_products_get",
+ {"category": "Electronics", "max_price": 100}
+ )
+ print(f"Affordable electronics: {result.data}")
+
+if __name__ == "__main__":
+ asyncio.run(demo())
+```
+
+### Custom Route Mapping
+
+Because FastMCP's FastAPI integration is based on its [OpenAPI integration](/integrations/openapi), you can customize how endpoints are converted to MCP components in exactly the same way. For example, here we use a `RouteMap` to map all GET requests to MCP resources, and all POST/PUT/DELETE requests to MCP tools:
+
+```python
+# Assumes the FastAPI app from above is already defined
+from fastmcp import FastMCP
+from fastmcp.server.providers.openapi import RouteMap, MCPType
+
+# Custom mapping rules
+mcp = FastMCP.from_fastapi(
+ app=app,
+ route_maps=[
+ # GET with path params → ResourceTemplates
+ RouteMap(
+ methods=["GET"],
+ pattern=r".*\{.*\}.*",
+ mcp_type=MCPType.RESOURCE_TEMPLATE
+ ),
+ # Other GETs → Resources
+ RouteMap(
+ methods=["GET"],
+ pattern=r".*",
+ mcp_type=MCPType.RESOURCE
+ ),
+ # POST/PUT/DELETE → Tools (default)
+ ],
+)
+
+# Now:
+# - GET /products → Resource
+# - GET /products/{id} → ResourceTemplate
+# - POST/PUT/DELETE → Tools
+```
+
+
+To learn more about customizing the conversion process, see the [OpenAPI Integration guide](/integrations/openapi).
+
+
+### Authentication and Headers
+
+You can configure headers and other client options via the `httpx_client_kwargs` parameter. For example, to add authentication to your FastAPI app, you can pass a `headers` dictionary to the `httpx_client_kwargs` parameter:
+
+```python {27-31}
+# Assumes the FastAPI app from above is already defined
+from fastmcp import FastMCP
+
+# Add authentication to your FastAPI app
+from fastapi import Depends, Header
+from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
+
+security = HTTPBearer()
+
+def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
+ if credentials.credentials != "secret-token":
+ raise HTTPException(status_code=401, detail="Invalid authentication")
+ return credentials.credentials
+
+# Add a protected endpoint
+@app.get("/admin/stats", dependencies=[Depends(verify_token)])
+def get_admin_stats():
+ return {
+ "total_products": len(products_db),
+ "categories": list(set(p.category for p in products_db.values()))
+ }
+
+# Create MCP server with authentication headers
+mcp = FastMCP.from_fastapi(
+ app=app,
+ httpx_client_kwargs={
+ "headers": {
+ "Authorization": "Bearer secret-token",
+ }
+ }
+)
+```
+
+## Mounting an MCP Server
+
+
+
+In addition to generating servers, FastMCP can facilitate adding MCP servers to your existing FastAPI application. You can do this by mounting the MCP ASGI application.
+
+### Basic Mounting
+
+To mount an MCP server, you can use the `http_app` method on your FastMCP instance. This will return an ASGI application that can be mounted to your FastAPI application.
+
+```python {23-30}
+from fastmcp import FastMCP
+from fastapi import FastAPI
+
+# Create MCP server
+mcp = FastMCP("Analytics Tools")
+
+@mcp.tool
+def analyze_pricing(category: str) -> dict:
+ """Analyze pricing for a category."""
+ products = [p for p in products_db.values() if p.category == category]
+ if not products:
+ return {"error": f"No products in {category}"}
+
+ prices = [p.price for p in products]
+ return {
+ "category": category,
+ "avg_price": round(sum(prices) / len(prices), 2),
+ "min": min(prices),
+ "max": max(prices),
+ }
+
+# Create ASGI app from MCP server
+mcp_app = mcp.http_app(path='/mcp')
+
+# Key: Pass lifespan to FastAPI
+app = FastAPI(title="E-commerce API", lifespan=mcp_app.lifespan)
+
+# Mount the MCP server
+app.mount("/analytics", mcp_app)
+
+# Now: API at /products/*, MCP at /analytics/mcp/
+```
+
+## Offering an LLM-Friendly API
+
+A common pattern is to generate an MCP server from your FastAPI app and serve both interfaces from the same application. This provides an LLM-optimized interface alongside your regular API:
+
+```python
+# Assumes the FastAPI app from above is already defined
+from fastmcp import FastMCP
+from fastapi import FastAPI
+
+# 1. Generate MCP server from your API
+mcp = FastMCP.from_fastapi(app=app, name="E-commerce MCP")
+
+# 2. Create the MCP's ASGI app
+mcp_app = mcp.http_app(path='/mcp')
+
+# 3. Create a new FastAPI app that combines both sets of routes
+combined_app = FastAPI(
+ title="E-commerce API with MCP",
+ routes=[
+ *mcp_app.routes, # MCP routes
+ *app.routes, # Original API routes
+ ],
+ lifespan=mcp_app.lifespan,
+)
+
+# Now you have:
+# - Regular API: http://localhost:8000/products
+# - LLM-friendly MCP: http://localhost:8000/mcp
+# Both served from the same FastAPI application!
+```
+
+This approach lets you maintain a single codebase while offering both traditional REST endpoints and MCP-compatible endpoints for LLM clients.
+
+## Key Considerations
+
+### Operation IDs
+
+FastAPI operation IDs become MCP component names. Always specify meaningful operation IDs:
+
+```python
+# Good - explicit operation_id
+@app.get("/users/{user_id}", operation_id="get_user_by_id")
+def get_user(user_id: int):
+ return {"id": user_id}
+
+# Less ideal - auto-generated name
+@app.get("/users/{user_id}")
+def get_user(user_id: int):
+ return {"id": user_id}
+```
+
+### Lifespan Management
+
+When mounting MCP servers, always pass the lifespan context:
+
+```python
+# Correct - lifespan passed, path="/" since we mount at /mcp
+mcp_app = mcp.http_app(path="/")
+app = FastAPI(lifespan=mcp_app.lifespan)
+app.mount("/mcp", mcp_app) # MCP endpoint at /mcp
+
+# Incorrect - missing lifespan
+app = FastAPI()
+app.mount("/mcp", mcp.http_app(path="/")) # Session manager won't initialize
+```
+
+If you're mounting an authenticated MCP server under a path prefix, see [Mounting Authenticated Servers](/deployment/http#mounting-authenticated-servers) for important OAuth routing considerations.
+
+### CORS Middleware
+
+If your FastAPI app uses `CORSMiddleware` and you're mounting an OAuth-protected FastMCP server, avoid adding application-wide CORS middleware. FastMCP and the MCP SDK already handle CORS for OAuth routes, and layering CORS middleware can cause conflicts (such as 404 errors on `.well-known` routes or OPTIONS requests).
+
+If you need CORS on your own FastAPI routes, use the sub-app pattern: mount your API and FastMCP as separate apps, each with their own middleware, rather than adding top-level `CORSMiddleware` to the combined application.
+
+### Combining Lifespans
+
+If your FastAPI app already has a lifespan (for database connections, startup tasks, etc.), you can't simply replace it with the MCP lifespan. Use `combine_lifespans` to run both:
+
+```python
+from fastapi import FastAPI
+from fastmcp import FastMCP
+from fastmcp.utilities.lifespan import combine_lifespans
+from contextlib import asynccontextmanager
+
+# Your existing lifespan
+@asynccontextmanager
+async def app_lifespan(app: FastAPI):
+ print("Starting up the app...")
+ yield
+ print("Shutting down the app...")
+
+# Create MCP server
+mcp = FastMCP("Tools")
+mcp_app = mcp.http_app(path="/")
+
+# Combine both lifespans
+app = FastAPI(lifespan=combine_lifespans(app_lifespan, mcp_app.lifespan))
+app.mount("/mcp", mcp_app) # MCP endpoint at /mcp
+```
+
+`combine_lifespans` enters lifespans in order and exits in reverse order.
+
+### Performance Tips
+
+1. **Use in-memory transport for testing** - Pass MCP servers directly to clients
+2. **Design purpose-built MCP tools** - Better than auto-converting complex APIs
+3. **Keep tool parameters simple** - LLMs perform better with focused interfaces
+
+For more details on configuration options, see the [OpenAPI Integration guide](/integrations/openapi).
\ No newline at end of file
diff --git a/docs/v3/integrations/gemini-cli.mdx b/docs/v3/integrations/gemini-cli.mdx
new file mode 100644
index 000000000..10613fb1b
--- /dev/null
+++ b/docs/v3/integrations/gemini-cli.mdx
@@ -0,0 +1,173 @@
+---
+title: Gemini CLI 🤝 FastMCP
+sidebarTitle: Gemini CLI
+description: Install and use FastMCP servers in Gemini CLI
+icon: message-smile
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+import { LocalFocusTip } from "/snippets/local-focus.mdx"
+
+
+
+[Gemini CLI](https://geminicli.com/) supports MCP servers through multiple transport methods including STDIO, SSE, and HTTP, allowing you to extend Gemini's capabilities with custom tools, resources, and prompts from your FastMCP servers.
+
+## Requirements
+
+This integration uses STDIO transport to run your FastMCP server locally. For remote deployments, you can run your FastMCP server with HTTP or SSE transport and configure it directly using Gemini CLI's built-in MCP management commands.
+
+## Create a Server
+
+The examples in this guide will use the following simple dice-rolling server, saved as `server.py`.
+
+```python server.py
+import random
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="Dice Roller")
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ """Roll `n_dice` 6-sided dice and return the results."""
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+## Install the Server
+
+### FastMCP CLI
+
+
+The easiest way to install a FastMCP server in Gemini CLI is using the `fastmcp install gemini-cli` command. This automatically handles the configuration, dependency management, and calls Gemini CLI's built-in MCP management system.
+
+```bash
+fastmcp install gemini-cli server.py
+```
+
+The install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:
+
+```bash
+# These are equivalent if your server object is named 'mcp'
+fastmcp install gemini-cli server.py
+fastmcp install gemini-cli server.py:mcp
+
+# Use explicit object name if your server has a different name
+fastmcp install gemini-cli server.py:my_custom_server
+```
+
+The command will automatically configure the server with Gemini CLI's `gemini mcp add` command.
+
+#### Dependencies
+
+FastMCP provides flexible dependency management options for your Gemini CLI servers:
+
+**Individual packages**: Use the `--with` flag to specify packages your server needs. You can use this flag multiple times:
+
+```bash
+fastmcp install gemini-cli server.py --with pandas --with requests
+```
+
+**Requirements file**: If you maintain a `requirements.txt` file with all your dependencies, use `--with-requirements` to install them:
+
+```bash
+fastmcp install gemini-cli server.py --with-requirements requirements.txt
+```
+
+**Editable packages**: For local packages under development, use `--with-editable` to install them in editable mode:
+
+```bash
+fastmcp install gemini-cli server.py --with-editable ./my-local-package
+```
+
+Alternatively, you can use a `fastmcp.json` configuration file (recommended):
+
+```json fastmcp.json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py",
+ "entrypoint": "mcp"
+ },
+ "environment": {
+ "dependencies": ["pandas", "requests"]
+ }
+}
+```
+
+
+#### Python Version and Project Configuration
+
+Control the Python environment for your server with these options:
+
+**Python version**: Use `--python` to specify which Python version your server requires. This ensures compatibility when your server needs specific Python features:
+
+```bash
+fastmcp install gemini-cli server.py --python 3.11
+```
+
+**Project directory**: Use `--project` to run your server within a specific project context. This tells `uv` to use the project's configuration files and virtual environment:
+
+```bash
+fastmcp install gemini-cli server.py --project /path/to/my-project
+```
+
+#### Environment Variables
+
+If your server needs environment variables (like API keys), you must include them:
+
+```bash
+fastmcp install gemini-cli server.py --server-name "Weather Server" \
+ --env API_KEY=your-api-key \
+ --env DEBUG=true
+```
+
+Or load them from a `.env` file:
+
+```bash
+fastmcp install gemini-cli server.py --server-name "Weather Server" --env-file .env
+```
+
+
+**Gemini CLI must be installed**. The integration looks for the Gemini CLI and uses the `gemini mcp add` command to register servers.
+
+
+### Manual Configuration
+
+For more control over the configuration, you can manually use Gemini CLI's built-in MCP management commands. This gives you direct control over how your server is launched:
+
+```bash
+# Add a server with custom configuration
+gemini mcp add dice-roller uv -- run --with fastmcp fastmcp run server.py
+
+# Add with environment variables
+gemini mcp add weather-server -e API_KEY=secret -e DEBUG=true uv -- run --with fastmcp fastmcp run server.py
+
+# Add with specific scope (user, or project)
+gemini mcp add my-server --scope user uv -- run --with fastmcp fastmcp run server.py
+```
+
+You can also manually specify Python versions and project directories in your Gemini CLI commands:
+
+```bash
+# With specific Python version
+gemini mcp add ml-server uv -- run --python 3.11 --with fastmcp fastmcp run server.py
+
+# Within a project directory
+gemini mcp add project-server uv -- run --project /path/to/project --with fastmcp fastmcp run server.py
+```
+
+## Using the Server
+
+Once your server is installed, you can start using your FastMCP server with Gemini CLI.
+
+Try asking Gemini something like:
+
+> "Roll some dice for me"
+
+Gemini will automatically detect your `roll_dice` tool and use it to fulfill your request.
+
+Gemini CLI can now access all the tools and prompts you've defined in your FastMCP server.
+
+If your server provides prompts, you can use them as slash commands with `/prompt_name`.
diff --git a/docs/v3/integrations/gemini.mdx b/docs/v3/integrations/gemini.mdx
new file mode 100644
index 000000000..1b17ab6ee
--- /dev/null
+++ b/docs/v3/integrations/gemini.mdx
@@ -0,0 +1,108 @@
+---
+title: Gemini SDK 🤝 FastMCP
+sidebarTitle: Gemini SDK
+description: Connect FastMCP servers to the Google Gemini SDK
+icon: message-code
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+Google's Gemini API includes built-in support for MCP servers in their Python and JavaScript SDKs, allowing you to connect directly to MCP servers and use their tools seamlessly with Gemini models.
+
+## Gemini Python SDK
+
+Google's [Gemini Python SDK](https://ai.google.dev/gemini-api/docs) can use FastMCP clients directly.
+
+
+Google's MCP integration is currently experimental and available in the Python and JavaScript SDKs. The API automatically calls MCP tools when needed and can connect to both local and remote MCP servers.
+
+
+
+Currently, Gemini's MCP support only accesses **tools** from MCP servers—it queries the `list_tools` endpoint and exposes those functions to the AI. Other MCP features like resources and prompts are not currently supported.
+
+
+### Create a Server
+
+First, create a FastMCP server with the tools you want to expose. For this example, we'll create a server with a single tool that rolls dice.
+
+```python server.py
+import random
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="Dice Roller")
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ """Roll `n_dice` 6-sided dice and return the results."""
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+### Call the Server
+
+
+To use the Gemini API with MCP, you'll need to install the Google Generative AI SDK:
+
+```bash
+pip install google-genai
+```
+
+You'll also need to authenticate with Google. You can do this by setting the `GEMINI_API_KEY` environment variable. Consult the Gemini SDK documentation for more information.
+
+```bash
+export GEMINI_API_KEY="your-api-key"
+```
+
+Gemini's SDK interacts directly with the MCP client session. To call the server, you'll need to instantiate a FastMCP client, enter its connection context, and pass the client session to the Gemini SDK.
+
+```python {5, 9, 15}
+from fastmcp import Client
+from google import genai
+import asyncio
+
+mcp_client = Client("server.py")
+gemini_client = genai.Client()
+
+async def main():
+ async with mcp_client:
+ response = await gemini_client.aio.models.generate_content(
+ model="gemini-2.0-flash",
+ contents="Roll 3 dice!",
+ config=genai.types.GenerateContentConfig(
+ temperature=0,
+ tools=[mcp_client.session], # Pass the FastMCP client session
+ ),
+ )
+ print(response.text)
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+If you run this code, you'll see output like:
+
+```text
+Okay, I rolled 3 dice and got a 5, 4, and 1.
+```
+
+### Remote & Authenticated Servers
+
+In the above example, we connected to our local server using `stdio` transport. Because we're using a FastMCP client, you can also connect to any local or remote MCP server, using any [transport](/clients/transports) or [auth](/clients/auth/oauth) method supported by FastMCP, simply by changing the client configuration.
+
+For example, to connect to a remote, authenticated server, you can use the following client:
+
+```python
+from fastmcp import Client
+from fastmcp.client.auth import BearerAuth
+
+mcp_client = Client(
+ "https://my-server.com/mcp/",
+ auth=BearerAuth(""),
+)
+```
+
+The rest of the code remains the same.
+
+
diff --git a/docs/v3/integrations/github.mdx b/docs/v3/integrations/github.mdx
new file mode 100644
index 000000000..d493eb1ef
--- /dev/null
+++ b/docs/v3/integrations/github.mdx
@@ -0,0 +1,175 @@
+---
+title: GitHub OAuth 🤝 FastMCP
+sidebarTitle: GitHub
+description: Secure your FastMCP server with GitHub OAuth
+icon: github
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+This guide shows you how to secure your FastMCP server using **GitHub OAuth**. Since GitHub doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/servers/auth/oauth-proxy) pattern to bridge GitHub's traditional OAuth with MCP's authentication requirements.
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+1. A **[GitHub Account](https://github.com/)** with access to create OAuth Apps
+2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
+
+### Step 1: Create a GitHub OAuth App
+
+Create an OAuth App in your GitHub settings to get the credentials needed for authentication:
+
+
+
+ Go to **Settings → Developer settings → OAuth Apps** in your GitHub account, or visit [github.com/settings/developers](https://github.com/settings/developers).
+
+ Click **"New OAuth App"** to create a new application.
+
+
+
+ Fill in the application details:
+
+ - **Application name**: Choose a name users will recognize (e.g., "My FastMCP Server")
+ - **Homepage URL**: Your application's homepage or documentation URL
+ - **Authorization callback URL**: Your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)
+
+
+ The callback URL must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. For local development, GitHub allows `http://localhost` URLs. For production, you must use HTTPS.
+
+
+
+ If you want to use a custom callback path (e.g., `/auth/github/callback`), make sure to set the same path in both your GitHub OAuth App settings and the `redirect_path` parameter when configuring the GitHubProvider.
+
+
+
+
+ After creating the app, you'll see:
+
+ - **Client ID**: A public identifier like `Ov23liAbcDefGhiJkLmN`
+ - **Client Secret**: Click "Generate a new client secret" and save the value securely
+
+
+ Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.
+
+
+
+
+### Step 2: FastMCP Configuration
+
+Create your FastMCP server using the `GitHubProvider`, which handles GitHub's OAuth quirks automatically:
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.github import GitHubProvider
+
+# The GitHubProvider handles GitHub's token format and validation
+auth_provider = GitHubProvider(
+ client_id="Ov23liAbcDefGhiJkLmN", # Your GitHub OAuth App Client ID
+ client_secret="github_pat_...", # Your GitHub OAuth App Client Secret
+ base_url="http://localhost:8000", # Must match your OAuth App configuration
+ # redirect_path="/auth/callback" # Default value, customize if needed
+)
+
+mcp = FastMCP(name="GitHub Secured App", auth=auth_provider)
+
+# Add a protected tool to test authentication
+@mcp.tool
+async def get_user_info() -> dict:
+ """Returns information about the authenticated GitHub user."""
+ from fastmcp.server.dependencies import get_access_token
+
+ token = get_access_token()
+ # The GitHubProvider stores user data in token claims
+ return {
+ "github_user": token.claims.get("login"),
+ "name": token.claims.get("name"),
+ "email": token.claims.get("email")
+ }
+```
+
+## Testing
+
+### Running the Server
+
+Start your FastMCP server with HTTP transport to enable OAuth flows:
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+Your server is now running and protected by GitHub OAuth authentication.
+
+### Testing with a Client
+
+Create a test client that authenticates with your GitHub-protected server:
+
+```python test_client.py
+from fastmcp import Client
+import asyncio
+
+async def main():
+ # The client will automatically handle GitHub OAuth
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ # First-time connection will open GitHub login in your browser
+ print("✓ Authenticated with GitHub!")
+
+ # Test the protected tool
+ result = await client.call_tool("get_user_info")
+ print(f"GitHub user: {result.data['github_user']}")
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+When you run the client for the first time:
+1. Your browser will open to GitHub's authorization page
+2. After you authorize the app, you'll be redirected back
+3. The client receives the token and can make authenticated requests
+
+
+The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.
+
+
+## Production Configuration
+
+
+
+For production deployments with persistent token management across server restarts, configure `jwt_signing_key` and `client_storage`:
+
+```python server.py
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.github import GitHubProvider
+from key_value.aio.stores.redis import RedisStore
+from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
+from cryptography.fernet import Fernet
+
+# Production setup with encrypted persistent token storage
+auth_provider = GitHubProvider(
+ client_id="Ov23liAbcDefGhiJkLmN",
+ client_secret="github_pat_...",
+ base_url="https://your-production-domain.com",
+
+ # Production token management
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ client_storage=FernetEncryptionWrapper(
+ key_value=RedisStore(
+ host=os.environ["REDIS_HOST"],
+ port=int(os.environ["REDIS_PORT"])
+ ),
+ fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
+ )
+)
+
+mcp = FastMCP(name="Production GitHub App", auth=auth_provider)
+```
+
+
+Parameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.
+
+For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
+
diff --git a/docs/v3/integrations/google.mdx b/docs/v3/integrations/google.mdx
new file mode 100644
index 000000000..17d49d12f
--- /dev/null
+++ b/docs/v3/integrations/google.mdx
@@ -0,0 +1,189 @@
+---
+title: Google OAuth 🤝 FastMCP
+sidebarTitle: Google
+description: Secure your FastMCP server with Google OAuth
+icon: google
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+This guide shows you how to secure your FastMCP server using **Google OAuth**. Since Google doesn't support Dynamic Client Registration, this integration uses the [**OAuth Proxy**](/servers/auth/oauth-proxy) pattern to bridge Google's traditional OAuth with MCP's authentication requirements.
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+1. A **[Google Cloud Account](https://console.cloud.google.com/)** with access to create OAuth 2.0 Client IDs
+2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
+
+### Step 1: Create a Google OAuth 2.0 Client ID
+
+Create an OAuth 2.0 Client ID in your Google Cloud Console to get the credentials needed for authentication:
+
+
+
+ Go to the [Google Cloud Console](https://console.cloud.google.com/apis/credentials) and select your project (or create a new one).
+
+ First, configure the OAuth consent screen by navigating to **APIs & Services → OAuth consent screen**. Choose "External" for testing or "Internal" for G Suite organizations.
+
+
+
+ Navigate to **APIs & Services → Credentials** and click **"+ CREATE CREDENTIALS"** → **"OAuth client ID"**.
+
+ Configure your OAuth client:
+
+ - **Application type**: Web application
+ - **Name**: Choose a descriptive name (e.g., "FastMCP Server")
+ - **Authorized JavaScript origins**: Add your server's base URL (e.g., `http://localhost:8000`)
+ - **Authorized redirect URIs**: Add your server URL + `/auth/callback` (e.g., `http://localhost:8000/auth/callback`)
+
+
+ The redirect URI must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter. For local development, Google allows `http://localhost` URLs with various ports. For production, you must use HTTPS.
+
+
+
+ If you want to use a custom callback path (e.g., `/auth/google/callback`), make sure to set the same path in both your Google OAuth Client settings and the `redirect_path` parameter when configuring the GoogleProvider.
+
+
+
+
+ After creating the client, you'll receive:
+
+ - **Client ID**: A string ending in `.apps.googleusercontent.com`
+ - **Client Secret**: A string starting with `GOCSPX-`
+
+ Download the JSON credentials or copy these values securely.
+
+
+ Store these credentials securely. Never commit them to version control. Use environment variables or a secrets manager in production.
+
+
+
+
+### Step 2: FastMCP Configuration
+
+Create your FastMCP server using the `GoogleProvider`, which handles Google's OAuth flow automatically:
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.google import GoogleProvider
+
+# The GoogleProvider handles Google's token format and validation
+auth_provider = GoogleProvider(
+ client_id="123456789.apps.googleusercontent.com", # Your Google OAuth Client ID
+ client_secret="GOCSPX-abc123...", # Your Google OAuth Client Secret
+ base_url="http://localhost:8000", # Must match your OAuth configuration
+ required_scopes=[ # Request user information
+ "openid",
+ "https://www.googleapis.com/auth/userinfo.email",
+ ],
+ # redirect_path="/auth/callback" # Default value, customize if needed
+)
+
+mcp = FastMCP(name="Google Secured App", auth=auth_provider)
+
+# Add a protected tool to test authentication
+@mcp.tool
+async def get_user_info() -> dict:
+ """Returns information about the authenticated Google user."""
+ from fastmcp.server.dependencies import get_access_token
+
+ token = get_access_token()
+ # The GoogleProvider stores user data in token claims
+ return {
+ "google_id": token.claims.get("sub"),
+ "email": token.claims.get("email"),
+ "name": token.claims.get("name"),
+ "picture": token.claims.get("picture"),
+ "locale": token.claims.get("locale")
+ }
+```
+
+## Testing
+
+### Running the Server
+
+Start your FastMCP server with HTTP transport to enable OAuth flows:
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+Your server is now running and protected by Google OAuth authentication.
+
+### Testing with a Client
+
+Create a test client that authenticates with your Google-protected server:
+
+```python test_client.py
+from fastmcp import Client
+import asyncio
+
+async def main():
+ # The client will automatically handle Google OAuth
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ # First-time connection will open Google login in your browser
+ print("✓ Authenticated with Google!")
+
+ # Test the protected tool
+ result = await client.call_tool("get_user_info")
+ print(f"Google user: {result['email']}")
+ print(f"Name: {result['name']}")
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+When you run the client for the first time:
+1. Your browser will open to Google's authorization page
+2. Sign in with your Google account and grant the requested permissions
+3. After authorization, you'll be redirected back
+4. The client receives the token and can make authenticated requests
+
+
+The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.
+
+
+## Production Configuration
+
+
+
+For production deployments with persistent token management across server restarts, configure `jwt_signing_key` and `client_storage`:
+
+```python server.py
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.google import GoogleProvider
+from key_value.aio.stores.redis import RedisStore
+from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
+from cryptography.fernet import Fernet
+
+# Production setup with encrypted persistent token storage
+auth_provider = GoogleProvider(
+ client_id="123456789.apps.googleusercontent.com",
+ client_secret="GOCSPX-abc123...",
+ base_url="https://your-production-domain.com",
+ required_scopes=["openid", "https://www.googleapis.com/auth/userinfo.email"],
+
+ # Production token management
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ client_storage=FernetEncryptionWrapper(
+ key_value=RedisStore(
+ host=os.environ["REDIS_HOST"],
+ port=int(os.environ["REDIS_PORT"])
+ ),
+ fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
+ )
+)
+
+mcp = FastMCP(name="Production Google App", auth=auth_provider)
+```
+
+
+Parameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.
+
+For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
+
\ No newline at end of file
diff --git a/docs/v3/integrations/goose.mdx b/docs/v3/integrations/goose.mdx
new file mode 100644
index 000000000..fc2ff8e39
--- /dev/null
+++ b/docs/v3/integrations/goose.mdx
@@ -0,0 +1,178 @@
+---
+title: Goose 🤝 FastMCP
+sidebarTitle: Goose
+description: Install and use FastMCP servers in Goose
+icon: message-smile
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+import { LocalFocusTip } from "/snippets/local-focus.mdx"
+
+
+
+[Goose](https://block.github.io/goose/) is an open-source AI agent from Block that supports MCP servers as extensions. FastMCP can install your server directly into Goose using its deeplink protocol — one command opens Goose with an install dialog ready to go.
+
+## Requirements
+
+This integration uses Goose's deeplink protocol to register your server as a STDIO extension running via `uvx`. You must have Goose installed on your system for the deeplink to open automatically.
+
+For remote deployments, configure your FastMCP server with HTTP transport and add it to Goose directly using `goose configure` or the config file.
+
+## Create a Server
+
+The examples in this guide will use the following simple dice-rolling server, saved as `server.py`.
+
+```python server.py
+import random
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="Dice Roller")
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ """Roll `n_dice` 6-sided dice and return the results."""
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+## Install the Server
+
+### FastMCP CLI
+
+
+The easiest way to install a FastMCP server in Goose is using the `fastmcp install goose` command. This generates a `goose://` deeplink and opens it, prompting Goose to install the server.
+
+```bash
+fastmcp install goose server.py
+```
+
+The install command supports the same `file.py:object` notation as the `run` command. If no object is specified, it will automatically look for a FastMCP server object named `mcp`, `server`, or `app` in your file:
+
+```bash
+# These are equivalent if your server object is named 'mcp'
+fastmcp install goose server.py
+fastmcp install goose server.py:mcp
+
+# Use explicit object name if your server has a different name
+fastmcp install goose server.py:my_custom_server
+```
+
+Under the hood, the generated command uses `uvx` to run your server in an isolated environment. Goose requires `uvx` rather than `uv run`, so the install produces a command like:
+
+```bash
+uvx --with pandas fastmcp run /path/to/server.py
+```
+
+#### Dependencies
+
+Use the `--with` flag to specify additional packages your server needs:
+
+```bash
+fastmcp install goose server.py --with pandas --with requests
+```
+
+Alternatively, you can use a `fastmcp.json` configuration file (recommended):
+
+```json fastmcp.json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py",
+ "entrypoint": "mcp"
+ },
+ "environment": {
+ "dependencies": ["pandas", "requests"]
+ }
+}
+```
+
+#### Python Version
+
+Use `--python` to specify which Python version your server should use:
+
+```bash
+fastmcp install goose server.py --python 3.11
+```
+
+
+The Goose install uses `uvx`, which does not support `--project`, `--with-requirements`, or `--with-editable`. If you need these options, use `fastmcp install mcp-json` to generate a full configuration and add it to Goose manually.
+
+
+#### Environment Variables
+
+Goose's deeplink protocol does not support environment variables. If your server needs them (like API keys), you have two options:
+
+1. **Configure after install**: Run `goose configure` and add environment variables to the extension.
+2. **Manual config**: Use `fastmcp install mcp-json` to generate the full configuration, then add it to `~/.config/goose/config.yaml` with the `envs` field.
+
+### Manual Configuration
+
+For more control, you can manually edit Goose's configuration file at `~/.config/goose/config.yaml`:
+
+```yaml
+extensions:
+ dice-roller:
+ name: Dice Roller
+ cmd: uvx
+ args: [fastmcp, run, /path/to/server.py]
+ enabled: true
+ type: stdio
+ timeout: 300
+```
+
+#### Dependencies
+
+When manually configuring, add packages using `--with` flags in the args:
+
+```yaml
+extensions:
+ dice-roller:
+ name: Dice Roller
+ cmd: uvx
+ args: [--with, pandas, --with, requests, fastmcp, run, /path/to/server.py]
+ enabled: true
+ type: stdio
+ timeout: 300
+```
+
+#### Environment Variables
+
+Environment variables can be specified in the `envs` field:
+
+```yaml
+extensions:
+ weather-server:
+ name: Weather Server
+ cmd: uvx
+ args: [fastmcp, run, /path/to/weather_server.py]
+ enabled: true
+ envs:
+ API_KEY: your-api-key
+ DEBUG: "true"
+ type: stdio
+ timeout: 300
+```
+
+You can also use `goose configure` to add extensions interactively, which prompts for environment variables.
+
+
+**`uvx` (from `uv`) must be installed and available in your system PATH**. Goose uses `uvx` to run Python-based extensions in isolated environments.
+
+
+## Using the Server
+
+Once your server is installed, you can start using your FastMCP server with Goose.
+
+Try asking Goose something like:
+
+> "Roll some dice for me"
+
+Goose will automatically detect your `roll_dice` tool and use it to fulfill your request, returning something like:
+
+> 🎲 Here are your dice rolls: 4, 6, 4
+>
+> You rolled 3 dice with a total of 14!
+
+Goose can now access all the tools, resources, and prompts you've defined in your FastMCP server.
diff --git a/docs/v3/integrations/huggingface.mdx b/docs/v3/integrations/huggingface.mdx
new file mode 100644
index 000000000..55794024b
--- /dev/null
+++ b/docs/v3/integrations/huggingface.mdx
@@ -0,0 +1,304 @@
+---
+title: Hugging Face OAuth 🤝 FastMCP
+sidebarTitle: Hugging Face
+description: Secure your FastMCP server with Hugging Face OAuth
+icon: hugging-face
+iconType: brands
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+This guide shows you how to secure your FastMCP server using **Hugging Face OAuth**.
+The `HuggingFaceProvider` uses FastMCP's [OAuth Proxy](/servers/auth/oauth-proxy)
+pattern with Hugging Face's OAuth and OpenID Connect endpoints. It works with
+manually created confidential apps, public PKCE apps, and Client ID Metadata
+Documents (CIMD).
+
+When deploying your MCP server to Hugging Face Spaces, Spaces can create and
+manage the OAuth app for you.
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+
+1. A **[Hugging Face account](https://huggingface.co/join)** with access to create OAuth apps
+2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
+
+### Step 1: Create a Hugging Face OAuth app
+
+Create an OAuth app from your [Hugging Face application settings](https://huggingface.co/settings/applications/new).
+For details, see Hugging Face's [OAuth documentation](https://huggingface.co/docs/hub/oauth).
+
+
+
+ Go to your [Hugging Face application settings](https://huggingface.co/settings/applications/new)
+ and create a new OAuth application.
+
+ Choose a name users will recognize, then configure the redirect URL for
+ your FastMCP server:
+
+ - Development: `http://localhost:8000/auth/callback`
+ - Production: `https://your-domain.com/auth/callback`
+
+
+ The redirect URL must match exactly. The default path is `/auth/callback`,
+ but you can customize it using the `redirect_path` parameter. For
+ production, use HTTPS.
+
+
+
+
+ After creating the app, save:
+
+ - **Client ID**: The public identifier for your Hugging Face OAuth app
+ - **Client Secret**: The app secret, if you created a confidential app
+
+
+ Store the client secret securely. Never commit it to version control. Use
+ environment variables or a secrets manager in production.
+
+
+
+
+### Step 2: Configure FastMCP
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.huggingface import HuggingFaceProvider
+
+# The HuggingFaceProvider handles Hugging Face's opaque OAuth access tokens
+# and stores user data in token claims.
+auth_provider = HuggingFaceProvider(
+ client_id="your-huggingface-client-id", # Your Hugging Face OAuth app client ID
+ client_secret="your-huggingface-client-secret", # Your Hugging Face OAuth app client secret
+ base_url="http://localhost:8000", # Must match your OAuth configuration
+ required_scopes=["openid", "profile"], # Default value
+ # redirect_path="/auth/callback" # Default value, customize if needed
+)
+
+mcp = FastMCP(name="Hugging Face Secured App", auth=auth_provider)
+
+
+# Add a protected tool to test authentication
+@mcp.tool
+async def get_user_info() -> dict:
+ """Returns information about the authenticated Hugging Face user."""
+ from fastmcp.server.dependencies import get_access_token
+
+ token = get_access_token()
+ return {
+ "subject": token.claims.get("sub"),
+ "username": token.claims.get("preferred_username"),
+ "profile": token.claims.get("profile"),
+ }
+```
+
+## Public OAuth apps, DCR, and CIMD
+
+Hugging Face supports public OAuth apps (no client secret). For public apps,
+omit `client_secret` and provide a `jwt_signing_key` so FastMCP can sign its
+own proxy tokens:
+
+```python
+auth_provider = HuggingFaceProvider(
+ client_id="your-public-huggingface-client-id",
+ base_url="http://localhost:8000",
+ jwt_signing_key="replace-with-a-secure-secret",
+)
+```
+
+MCP clients can use Dynamic Client Registration with your FastMCP server. The
+`HuggingFaceProvider` inherits FastMCP's OAuth Proxy behavior, which handles
+client registration locally and forwards authorization to Hugging Face using
+your configured Hugging Face OAuth app. In other words, MCP clients register
+with FastMCP, while FastMCP uses your Hugging Face `client_id` and optional
+`client_secret` for the upstream OAuth flow.
+
+You can also use a Client ID Metadata Document URL as the `client_id` when your
+client metadata is hosted at a stable HTTPS URL:
+
+```python
+auth_provider = HuggingFaceProvider(
+ client_id="https://your-client.example/.well-known/oauth-cimd",
+ base_url="http://localhost:8000",
+ jwt_signing_key="replace-with-a-secure-secret",
+)
+```
+
+## Testing
+
+### Running the Server
+
+Start your server with HTTP transport:
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+Your server is now running and protected by Hugging Face OAuth authentication.
+
+### Testing with a Client
+
+Create a test client that authenticates with your Hugging Face-protected server:
+
+```python test_client.py
+import asyncio
+from fastmcp import Client
+
+
+async def main():
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ result = await client.call_tool("get_user_info")
+ print(result)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+When you run the client for the first time:
+
+1. Your browser will open to Hugging Face's authorization page
+2. Sign in with your Hugging Face account and grant the requested permissions
+3. After authorization, you'll be redirected back
+4. The client receives the token and can make authenticated requests
+
+
+The client caches tokens locally, so you won't need to re-authenticate for
+subsequent runs unless the token expires or you explicitly clear the cache.
+
+
+## Hugging Face Spaces
+
+When deploying to [Hugging Face Spaces](https://huggingface.co/docs/hub/spaces-oauth),
+Spaces can create and manage the OAuth app for you. Add OAuth metadata to your
+Space README:
+
+```yaml
+---
+title: FastMCP Hugging Face OAuth
+sdk: docker
+hf_oauth: true
+hf_oauth_expiration_minutes: 480
+hf_oauth_scopes:
+ - email
+ - inference-api
+---
+```
+
+Spaces provide `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET`, `OAUTH_SCOPES`,
+`OPENID_PROVIDER_URL`, and `SPACE_HOST` environment variables:
+
+```python
+import os
+
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.huggingface import HuggingFaceProvider
+from fastmcp.utilities.auth import parse_scopes
+
+base_url = f"https://{os.environ['SPACE_HOST']}"
+
+auth_provider = HuggingFaceProvider(
+ client_id=os.environ["OAUTH_CLIENT_ID"],
+ client_secret=os.environ["OAUTH_CLIENT_SECRET"],
+ base_url=base_url,
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ required_scopes=parse_scopes(os.environ.get("OAUTH_SCOPES")) or ["openid", "profile"],
+)
+
+mcp = FastMCP(name="Hugging Face Space App", auth=auth_provider)
+```
+
+Set `JWT_SIGNING_KEY` as a Space secret.
+
+## Hugging Face scopes
+
+The default scopes are `openid` and `profile`. Add more scopes when your tools
+need Hub capabilities:
+
+| Scope | Description |
+|-------|-------------|
+| `email` | Access the user's email address |
+| `read-billing` | Know whether the user has a payment method set up |
+| `read-repos` | Read the user's personal repositories |
+| `gated-repos` | Read public gated repositories the user can access |
+| `contribute-repos` | Create repositories and access app-created repositories |
+| `write-repos` | Read and write the user's personal repositories |
+| `manage-repos` | Full repository access, including creation and deletion |
+| `read-collections` | Read the user's personal collections |
+| `write-collections` | Read and write the user's personal collections, including collection creation and deletion |
+| `inference-api` | Use Hugging Face Inference Providers as the user |
+| `jobs` | Run Hugging Face Jobs |
+| `webhooks` | Manage webhooks |
+| `write-discussions` | Open discussions and pull requests, and interact with discussions |
+
+```python
+auth_provider = HuggingFaceProvider(
+ client_id="your-huggingface-client-id",
+ client_secret="your-huggingface-client-secret",
+ base_url="https://your-domain.com",
+ required_scopes=["openid", "profile", "inference-api", "jobs"],
+)
+```
+
+For organization resources, use Hugging Face's normal OAuth organization grant
+flow. If you need a specific organization, pass Hugging Face's `orgIds`
+authorization parameter. The value is the organization ID from the
+`organizations.sub` field in the Hugging Face userinfo response:
+
+```python
+auth_provider = HuggingFaceProvider(
+ client_id="your-huggingface-client-id",
+ client_secret="your-huggingface-client-secret",
+ base_url="https://your-domain.com",
+ extra_authorize_params={"orgIds": "your-org-id"},
+)
+```
+
+## Production Configuration
+
+For production deployments with persistent token management across server
+restarts, configure `jwt_signing_key` and `client_storage`:
+
+```python server.py
+import os
+from cryptography.fernet import Fernet
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.huggingface import HuggingFaceProvider
+from key_value.aio.stores.redis import RedisStore
+from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
+
+# Production setup with encrypted persistent token storage
+auth_provider = HuggingFaceProvider(
+ client_id="your-huggingface-client-id",
+ client_secret=os.environ["HUGGINGFACE_CLIENT_SECRET"],
+ base_url="https://your-production-domain.com",
+ required_scopes=["openid", "profile", "email"],
+
+ # Production token management
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ client_storage=FernetEncryptionWrapper(
+ key_value=RedisStore(
+ host=os.environ["REDIS_HOST"],
+ port=int(os.environ["REDIS_PORT"])
+ ),
+ fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
+ )
+)
+
+mcp = FastMCP(name="Production Hugging Face App", auth=auth_provider)
+```
+
+
+Parameters (`jwt_signing_key` and `client_storage`) work together to ensure
+tokens and client registrations survive server restarts. **Wrap your storage in
+`FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without
+it, tokens are stored in plaintext. Store secrets in environment variables and
+use a persistent storage backend like Redis for distributed deployments.
+
+For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
+
diff --git a/docs/v3/integrations/images/authkit/enable_dcr.png b/docs/v3/integrations/images/authkit/enable_dcr.png
new file mode 100644
index 000000000..e5942f60e
Binary files /dev/null and b/docs/v3/integrations/images/authkit/enable_dcr.png differ
diff --git a/docs/v3/integrations/images/oci/ociaddapplication.png b/docs/v3/integrations/images/oci/ociaddapplication.png
new file mode 100644
index 000000000..690f8d391
Binary files /dev/null and b/docs/v3/integrations/images/oci/ociaddapplication.png differ
diff --git a/docs/v3/integrations/images/oci/ocieditdomainsettings.png b/docs/v3/integrations/images/oci/ocieditdomainsettings.png
new file mode 100644
index 000000000..08812ba9b
Binary files /dev/null and b/docs/v3/integrations/images/oci/ocieditdomainsettings.png differ
diff --git a/docs/v3/integrations/images/oci/ocieditdomainsettingsbutton.png b/docs/v3/integrations/images/oci/ocieditdomainsettingsbutton.png
new file mode 100644
index 000000000..3954dab07
Binary files /dev/null and b/docs/v3/integrations/images/oci/ocieditdomainsettingsbutton.png differ
diff --git a/docs/v3/integrations/images/oci/ocioauthconfiguration.png b/docs/v3/integrations/images/oci/ocioauthconfiguration.png
new file mode 100644
index 000000000..f00782154
Binary files /dev/null and b/docs/v3/integrations/images/oci/ocioauthconfiguration.png differ
diff --git a/docs/v3/integrations/images/permit/abac_condition_example.png b/docs/v3/integrations/images/permit/abac_condition_example.png
new file mode 100644
index 000000000..a5592abca
Binary files /dev/null and b/docs/v3/integrations/images/permit/abac_condition_example.png differ
diff --git a/docs/v3/integrations/images/permit/abac_policy_example.png b/docs/v3/integrations/images/permit/abac_policy_example.png
new file mode 100644
index 000000000..bd4b5cf33
Binary files /dev/null and b/docs/v3/integrations/images/permit/abac_policy_example.png differ
diff --git a/docs/v3/integrations/images/permit/policy_mapping.png b/docs/v3/integrations/images/permit/policy_mapping.png
new file mode 100644
index 000000000..d100b1eab
Binary files /dev/null and b/docs/v3/integrations/images/permit/policy_mapping.png differ
diff --git a/docs/v3/integrations/images/permit/role_assignement.png b/docs/v3/integrations/images/permit/role_assignement.png
new file mode 100644
index 000000000..c65e34181
Binary files /dev/null and b/docs/v3/integrations/images/permit/role_assignement.png differ
diff --git a/docs/v3/integrations/keycloak.mdx b/docs/v3/integrations/keycloak.mdx
new file mode 100644
index 000000000..22d61f132
--- /dev/null
+++ b/docs/v3/integrations/keycloak.mdx
@@ -0,0 +1,141 @@
+---
+title: Keycloak OAuth 🤝 FastMCP
+sidebarTitle: Keycloak
+description: Secure your FastMCP server with Keycloak OAuth
+icon: shield-check
+tag: NEW
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+This guide shows you how to secure your FastMCP server using **Keycloak OAuth**. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern with Dynamic Client Registration (DCR), where Keycloak handles user login and your FastMCP server validates the tokens.
+
+
+**Keycloak 26.6.0 or later is required.** Earlier versions had a DCR incompatibility with MCP clients ([PR #45309](https://github.com/keycloak/keycloak/pull/45309)) that is fixed in 26.6.0.
+
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+1. A running **[Keycloak](https://keycloak.org/)** instance (e.g., `http://localhost:8080`)
+2. A Keycloak realm with **Dynamic Client Registration** enabled and a trusted host policy that allows your server URL (e.g., `http://localhost:8000/*`)
+3. Your FastMCP server's public URL (e.g., `http://localhost:8000`)
+
+### FastMCP Configuration
+
+Create your FastMCP server and use `KeycloakAuthProvider` to handle OAuth:
+
+```python server.py
+import os
+
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
+from fastmcp.server.dependencies import get_access_token
+
+auth = KeycloakAuthProvider(
+ realm_url=os.getenv("KEYCLOAK_REALM_URL") or "http://localhost:8080/realms/myrealm",
+ base_url="http://localhost:8000",
+ # audience="http://localhost:8000", # Recommended for production
+)
+
+mcp = FastMCP("Keycloak Example Server", auth=auth)
+
+
+@mcp.tool
+async def get_access_token_claims() -> dict:
+ """Get the authenticated user's access token claims."""
+ token = get_access_token()
+ return {
+ "sub": token.claims.get("sub"),
+ "scope": token.claims.get("scope"),
+ "azp": token.claims.get("azp"),
+ }
+```
+
+
+**Production security**: Always configure the `audience` parameter in production. Without it, your server accepts tokens issued for any audience. Configure Keycloak audience mappers and set `audience` to your server's base URL to ensure tokens are specifically intended for your server.
+
+
+## Local Development
+
+Local infrastructure tooling is deliberately kept out of the FastMCP core library to keep auth integrations slim and the associated maintenance burden as low as possible. That said, Keycloak is a popular identity provider for local development and testing, so a dedicated FastMCP-compatible setup blueprint lives in the companion project [**fastmcp-keycloak-local**](https://github.com/stephaneberle9/fastmcp-keycloak-local).
+
+It provides everything needed to develop and test FastMCP servers with Keycloak OAuth locally: a Docker-based Keycloak setup with a pre-configured `fastmcp` realm (Dynamic Client Registration enabled, test user included), cross-platform start scripts, and integration guides for the MCP Inspector, Claude Desktop, and Claude Code CLI.
+
+## Testing
+
+### Running the Server
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+### Testing with a Client
+
+```python client.py
+import asyncio
+from fastmcp import Client
+
+async def main():
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ print("✓ Authenticated with Keycloak!")
+ result = await client.call_tool("get_access_token_claims")
+ print(f"sub: {result.data.get('sub', 'N/A')}")
+
+asyncio.run(main())
+```
+
+On first run, your browser will open to Keycloak's authorization page. After login, the client receives a token and caches it for subsequent runs.
+
+## Features
+
+### JWT Token Validation
+
+- **Signature Verification**: Validates tokens against Keycloak's JWKS endpoint
+- **Expiration Checking**: Automatically rejects expired tokens
+- **Issuer Validation**: Ensures tokens come from your specific Keycloak realm
+- **Scope Enforcement**: Verifies required OAuth scopes are present
+- **Audience Validation**: Optional validation that tokens target your server (configure `audience`)
+
+### User Claims
+
+Access user information from Keycloak JWT tokens:
+
+```python
+from fastmcp.server.dependencies import get_access_token
+
+@mcp.tool
+async def admin_only_tool() -> str:
+ """A tool only available to admin users."""
+ token = get_access_token()
+ roles = token.claims.get("realm_access", {}).get("roles", [])
+ if "admin" not in roles:
+ raise ValueError("This tool requires admin access")
+ return "Admin access granted!"
+```
+
+## Advanced Configuration
+
+### Custom Token Verifier
+
+```python
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
+
+custom_verifier = JWTVerifier(
+ jwks_uri="http://localhost:8080/realms/myrealm/protocol/openid-connect/certs",
+ issuer="http://localhost:8080/realms/myrealm",
+ audience="my-resource-server",
+ required_scopes=["api:read", "api:write"],
+)
+
+auth = KeycloakAuthProvider(
+ realm_url="http://localhost:8080/realms/myrealm",
+ base_url="http://localhost:8000",
+ token_verifier=custom_verifier,
+)
+```
diff --git a/docs/v3/integrations/mcp-json-configuration.mdx b/docs/v3/integrations/mcp-json-configuration.mdx
new file mode 100644
index 000000000..fec8ffc01
--- /dev/null
+++ b/docs/v3/integrations/mcp-json-configuration.mdx
@@ -0,0 +1,514 @@
+---
+title: MCP JSON Configuration 🤝 FastMCP
+sidebarTitle: MCP.json
+description: Generate standard MCP configuration files for any compatible client
+icon: brackets-curly
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+FastMCP can generate standard MCP JSON configuration files that work with any MCP-compatible client including Claude Desktop, VS Code, Cursor, and other applications that support the Model Context Protocol.
+
+## MCP JSON Configuration Standard
+
+The MCP JSON configuration format is an **emergent standard** that has developed across the MCP ecosystem. This format defines how MCP clients should configure and launch MCP servers, providing a consistent way to specify server commands, arguments, and environment variables.
+
+### Configuration Structure
+
+The standard uses a `mcpServers` object where each key represents a server name and the value contains the server's configuration:
+
+```json
+{
+ "mcpServers": {
+ "server-name": {
+ "command": "executable",
+ "args": ["arg1", "arg2"],
+ "env": {
+ "VAR": "value"
+ }
+ }
+ }
+}
+```
+
+### Server Configuration Fields
+
+#### `command` (required)
+The executable command to run the MCP server. This should be an absolute path or a command available in the system PATH.
+
+```json
+{
+ "command": "python"
+}
+```
+
+#### `args` (optional)
+An array of command-line arguments passed to the server executable. Arguments are passed in order.
+
+```json
+{
+ "args": ["server.py", "--verbose", "--port", "8080"]
+}
+```
+
+#### `env` (optional)
+An object containing environment variables to set when launching the server. All values must be strings.
+
+```json
+{
+ "env": {
+ "API_KEY": "secret-key",
+ "DEBUG": "true",
+ "PORT": "8080"
+ }
+}
+```
+
+### Client Adoption
+
+This format is widely adopted across the MCP ecosystem:
+
+- **Claude Desktop**: Uses `~/.claude/claude_desktop_config.json`
+- **Cursor**: Uses `~/.cursor/mcp.json`
+- **VS Code**: Uses workspace `.vscode/mcp.json`
+- **Other clients**: Many MCP-compatible applications follow this standard
+
+## Overview
+
+
+**For the best experience, use FastMCP's first-class integrations:** [`fastmcp install claude-code`](/integrations/claude-code), [`fastmcp install claude-desktop`](/integrations/claude-desktop), or [`fastmcp install cursor`](/integrations/cursor). Use MCP JSON generation for advanced use cases and unsupported clients.
+
+
+The `fastmcp install mcp-json` command generates configuration in the standard `mcpServers` format used across the MCP ecosystem. This is useful when:
+
+- **Working with unsupported clients** - Any MCP client not directly integrated with FastMCP
+- **CI/CD environments** - Automated configuration generation for deployments
+- **Configuration sharing** - Easy distribution of server setups to team members
+- **Custom tooling** - Integration with your own MCP management tools
+- **Manual setup** - When you prefer to manually configure your MCP client
+
+## Basic Usage
+
+Generate configuration and output to stdout (useful for piping):
+
+```bash
+fastmcp install mcp-json server.py
+```
+
+This outputs the server configuration JSON with the server name as the root key:
+
+```json
+{
+ "My Server": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--with",
+ "fastmcp",
+ "fastmcp",
+ "run",
+ "/absolute/path/to/server.py"
+ ]
+ }
+}
+```
+
+To use this in a client configuration file, add it to the `mcpServers` object in your client's configuration:
+
+```json
+{
+ "mcpServers": {
+ "My Server": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--with",
+ "fastmcp",
+ "fastmcp",
+ "run",
+ "/absolute/path/to/server.py"
+ ]
+ }
+ }
+}
+```
+
+
+When using `--python`, `--project`, or `--with-requirements`, the generated configuration will include these options in the `uv run` command, ensuring your server runs with the correct Python version and dependencies.
+
+
+
+Different MCP clients may have specific configuration requirements or formatting needs. Always consult your client's documentation to ensure proper integration.
+
+
+## Configuration Options
+
+### Server Naming
+
+```bash
+# Use server's built-in name (from FastMCP constructor)
+fastmcp install mcp-json server.py
+
+# Override with custom name
+fastmcp install mcp-json server.py --name "Custom Server Name"
+```
+
+### Dependencies
+
+Add Python packages your server needs:
+
+```bash
+# Single package
+fastmcp install mcp-json server.py --with pandas
+
+# Multiple packages
+fastmcp install mcp-json server.py --with pandas --with requests --with httpx
+
+# Editable local package
+fastmcp install mcp-json server.py --with-editable ./my-package
+
+# From requirements file
+fastmcp install mcp-json server.py --with-requirements requirements.txt
+```
+
+You can also use a `fastmcp.json` configuration file (recommended):
+
+```json fastmcp.json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py",
+ "entrypoint": "mcp"
+ },
+ "environment": {
+ "dependencies": ["pandas", "matplotlib", "seaborn"]
+ }
+}
+```
+
+Then simply install with:
+```bash
+fastmcp install mcp-json fastmcp.json
+```
+
+
+### Environment Variables
+
+```bash
+# Individual environment variables
+fastmcp install mcp-json server.py \
+ --env API_KEY=your-secret-key \
+ --env DEBUG=true
+
+# Load from .env file
+fastmcp install mcp-json server.py --env-file .env
+```
+
+### Python Version and Project Directory
+
+Specify Python version or run within a specific project:
+
+```bash
+# Use specific Python version
+fastmcp install mcp-json server.py --python 3.11
+
+# Run within a project directory
+fastmcp install mcp-json server.py --project /path/to/project
+```
+
+### Server Object Selection
+
+Use the same `file.py:object` notation as other FastMCP commands:
+
+```bash
+# Auto-detects server object (looks for 'mcp', 'server', or 'app')
+fastmcp install mcp-json server.py
+
+# Explicit server object
+fastmcp install mcp-json server.py:my_custom_server
+```
+
+## Clipboard Integration
+
+Copy configuration directly to your clipboard for easy pasting:
+
+```bash
+fastmcp install mcp-json server.py --copy
+```
+
+
+The `--copy` flag requires the `pyperclip` Python package. If not installed, you'll see an error message with installation instructions.
+
+
+## Usage Examples
+
+### Basic Server
+
+```bash
+fastmcp install mcp-json dice_server.py
+```
+
+Output:
+```json
+{
+ "Dice Server": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--with",
+ "fastmcp",
+ "fastmcp",
+ "run",
+ "/home/user/dice_server.py"
+ ]
+ }
+}
+```
+
+### Production Server with Dependencies
+
+```bash
+fastmcp install mcp-json api_server.py \
+ --name "Production API Server" \
+ --with requests \
+ --with python-dotenv \
+ --env API_BASE_URL=https://api.example.com \
+ --env TIMEOUT=30
+```
+
+### Advanced Configuration
+
+```bash
+fastmcp install mcp-json ml_server.py \
+ --name "ML Analysis Server" \
+ --python 3.11 \
+ --with-requirements requirements.txt \
+ --project /home/user/ml-project \
+ --env GPU_DEVICE=0
+```
+
+Output:
+```json
+{
+ "Production API Server": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--with",
+ "fastmcp",
+ "--with",
+ "python-dotenv",
+ "--with",
+ "requests",
+ "fastmcp",
+ "run",
+ "/home/user/api_server.py"
+ ],
+ "env": {
+ "API_BASE_URL": "https://api.example.com",
+ "TIMEOUT": "30"
+ }
+ }
+}
+```
+
+The advanced configuration example generates:
+```json
+{
+ "ML Analysis Server": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--python",
+ "3.11",
+ "--project",
+ "/home/user/ml-project",
+ "--with",
+ "fastmcp",
+ "--with-requirements",
+ "requirements.txt",
+ "fastmcp",
+ "run",
+ "/home/user/ml_server.py"
+ ],
+ "env": {
+ "GPU_DEVICE": "0"
+ }
+ }
+}
+```
+
+### Pipeline Usage
+
+Save configuration to file:
+
+```bash
+fastmcp install mcp-json server.py > mcp-config.json
+```
+
+Use in shell scripts:
+
+```bash
+#!/bin/bash
+CONFIG=$(fastmcp install mcp-json server.py --name "CI Server")
+echo "$CONFIG" | jq '."CI Server".command'
+# Output: "uv"
+```
+
+### UV-Managed Project Dependencies
+
+For servers that live inside a uv-managed project (with `pyproject.toml`), use the `--project` flag to run within that project's environment:
+
+```bash
+fastmcp install mcp-json server.py --project .
+```
+
+Output:
+```json
+{
+ "My Server": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--project",
+ "/absolute/path/to/project",
+ "--with",
+ "fastmcp",
+ "fastmcp",
+ "run",
+ "/absolute/path/to/project/server.py"
+ ]
+ }
+}
+```
+
+You can also use `fastmcp.json` with a local project:
+
+```json fastmcp.json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py"
+ },
+ "environment": {
+ "project": "."
+ }
+}
+```
+
+If your server needs additional packages beyond those in `pyproject.toml`, add them via the `dependencies` array or `--with`.
+
+### Published Packages with `uvx`
+
+If your team publishes MCP servers as pip packages, you can configure clients to run them with `uvx` directly instead of `uv run`. For example, if your package is called `my-mcp-server` and provides a CLI entry point of the same name:
+
+```json
+{
+ "mcpServers": {
+ "My Server": {
+ "command": "uvx",
+ "args": ["my-mcp-server"]
+ }
+ }
+}
+```
+
+If the package name differs from the CLI command (e.g., package `weather-mcp` with command `weather-server`):
+
+```json
+{
+ "mcpServers": {
+ "Weather": {
+ "command": "uvx",
+ "args": ["--from", "weather-mcp", "weather-server"]
+ }
+ }
+}
+```
+
+You can also pin Python versions or add extra dependencies:
+
+```json
+{
+ "mcpServers": {
+ "My Server": {
+ "command": "uvx",
+ "args": [
+ "--python", "3.12",
+ "--with", "requests",
+ "my-mcp-server"
+ ]
+ }
+ }
+}
+```
+
+
+`fastmcp install mcp-json` generates `uv run` configurations for local development. For published packages, you'll typically write the `uvx` configuration manually or generate it through your own packaging workflow.
+
+
+## Integration with MCP Clients
+
+The generated configuration works with any MCP-compatible application:
+
+### Claude Desktop
+
+**Prefer [`fastmcp install claude-desktop`](/integrations/claude-desktop)** for automatic installation. Use MCP JSON for advanced configuration needs.
+
+Copy the `mcpServers` object into `~/.claude/claude_desktop_config.json`
+
+### Cursor
+
+**Prefer [`fastmcp install cursor`](/integrations/cursor)** for automatic installation. Use MCP JSON for advanced configuration needs.
+
+Add to `~/.cursor/mcp.json`
+
+### VS Code
+Add to your workspace's `.vscode/mcp.json` file
+
+### Custom Applications
+Use the JSON configuration with any application that supports the MCP protocol
+
+## Configuration Format
+
+The generated configuration outputs a server object with the server name as the root key:
+
+```json
+{
+ "": {
+ "command": "",
+ "args": ["", "", "..."],
+ "env": {
+ "": ""
+ }
+ }
+}
+```
+
+To use this in an MCP client, add it to the client's `mcpServers` configuration object.
+
+**Fields:**
+- `command`: The executable to run (always `uv` for FastMCP servers)
+- `args`: Command-line arguments including dependencies and server path
+- `env`: Environment variables (only included if specified)
+
+
+**All file paths in the generated configuration are absolute paths**. This ensures the configuration works regardless of the working directory when the MCP client starts the server.
+
+
+## Requirements
+
+- **uv**: Must be installed and available in your system PATH
+- **pyperclip** (optional): Required only for `--copy` functionality
+
+Install uv if not already available:
+
+```bash
+# macOS
+brew install uv
+
+# Linux/Windows
+curl -LsSf https://astral.sh/uv/install.sh | sh
+```
diff --git a/docs/v3/integrations/oci.mdx b/docs/v3/integrations/oci.mdx
new file mode 100644
index 000000000..02fa36dae
--- /dev/null
+++ b/docs/v3/integrations/oci.mdx
@@ -0,0 +1,248 @@
+---
+title: OCI IAM OAuth 🤝 FastMCP
+sidebarTitle: Oracle
+description: Secure your FastMCP server with OCI IAM OAuth
+icon: shield-check
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+This guide shows you how to secure your FastMCP server using **OCI IAM OAuth**. Since OCI IAM doesn't support Dynamic Client Registration, this integration uses the [**OIDC Proxy**](/servers/auth/oidc-proxy) pattern to bridge OCI's traditional OAuth with MCP's authentication requirements.
+
+## Configuration
+
+### Prerequisites
+
+1. An OCI cloud Account with access to create an Integrated Application in an Identity Domain.
+2. Your FastMCP server's URL (For dev environments, it is http://localhost:8000. For PROD environments, it could be https://mcp.yourdomain.com)
+
+### Step 1: Make sure client access is enabled for JWK's URL
+
+
+
+
+ Login to OCI console (https://cloud.oracle.com for OCI commercial cloud).
+ From "Identity & Security" menu, open Domains page.
+ On the Domains list page, select the domain that you are using for MCP Authentication.
+ Open Settings tab.
+ Click on "Edit Domain Settings" button.
+
+
+
+
+
+
+
+
+ Enable "Configure client access" checkbox as shown in the screenshot.
+
+
+
+
+
+
+
+### Step 2: Create OAuth client for MCP server authentication
+
+Follow the Steps as mentioned below to create an OAuth client.
+
+
+
+
+ Login to OCI console (https://cloud.oracle.com for OCI commercial cloud).
+ From "Identity & Security" menu, open Domains page.
+ On the Domains list page, select the domain in which you want to create MCP server OAuth client. If you need help finding the list page for the domain, see [Listing Identity Domains.](https://docs.oracle.com/en-us/iaas/Content/Identity/domains/to-view-identity-domains.htm#view-identity-domains).
+ On the details page, select Integrated applications. A list of applications in the domain is displayed.
+
+
+
+
+ Select Add application.
+ In the Add application window, select Confidential Application.
+ Select Launch workflow.
+ In the Add application details page, Enter name and description as shown below.
+
+
+
+
+
+
+
+
+ Once the Integrated Application is created, Click on "OAuth configuration" tab.
+ Click on "Edit OAuth configuration" button.
+ Configure the application as OAuth client by selecting "Configure this application as a client now" radio button.
+ Select "Authorization code" grant type. If you are planning to use the same OAuth client application for token exchange, select "Client credentials" grant type as well. In the sample, we will use the same client.
+ For Authorization grant type, select redirect URL. In most cases, this will be the MCP server URL followed by "/oauth/callback".
+
+
+
+
+
+
+
+
+ Click on "Submit" button to update OAuth configuration for the client application.
+ **Note: You don't need to do any special configuration to support PKCE for the OAuth client.**
+ Make sure to Activate the client application.
+ Note down client ID and client secret for the application. You'll use these values when configuring the OCIProvider in your code.
+
+
+
+This is all you need to implement MCP server authentication against OCI IAM. However, you may want to use an authenticated user token to invoke OCI control plane APIs and propagate identity to the OCI control plane instead of using a service user account. In that case, you need to implement token exchange.
+
+### Step 3: Token Exchange Setup (Only if MCP server needs to talk to OCI Control Plane)
+
+Token exchange helps you exchange a logged-in user's OCI IAM token for an OCI control plane session token, also known as UPST (User Principal Session Token). To learn more about token exchange, refer to my [Workload Identity Federation Blog](https://www.ateam-oracle.com/post/workload-identity-federation)
+
+For token exchange, we need to configure Identity propagation trust. The blog above discusses setting up the trust using REST APIs. However, you can also use OCI CLI. Before using the CLI command below, ensure that you have created a token exchange OAuth client. In most cases, you can use the same OAuth client that you created above. Replace `` and `` in the CLI command below with your actual values.
+
+```bash
+oci identity-domains identity-propagation-trust create \
+--schemas '["urn:ietf:params:scim:schemas:oracle:idcs:IdentityPropagationTrust"]' \
+--public-key-endpoint "https://.identity.oraclecloud.com/admin/v1/SigningCert/jwk" \
+--name "For Token Exchange" --type "JWT" \
+--issuer "https://identity.oraclecloud.com/" --active true \
+--endpoint "https://.identity.oraclecloud.com" \
+--subject-claim-name "sub" --allow-impersonation false \
+--subject-mapping-attribute "username" \
+--subject-type "User" --client-claim-name "iss" \
+--client-claim-values '["https://identity.oraclecloud.com/"]' \
+--oauth-clients '[""]'
+```
+
+To exchange access token for OCI token and create a signer object, you need to add below code in MCP server. You can then use the signer object to create any OCI control plane client.
+
+```python
+
+from fastmcp.server.dependencies import get_access_token
+from fastmcp.utilities.logging import get_logger
+from oci.auth.signers import TokenExchangeSigner
+import os
+
+logger = get_logger(__name__)
+
+# Load configuration from environment
+OCI_IAM_GUID = os.environ.get("OCI_IAM_GUID")
+OCI_CLIENT_ID = os.environ.get("OCI_CLIENT_ID")
+OCI_CLIENT_SECRET = os.environ.get("OCI_CLIENT_SECRET")
+
+_global_token_cache = {} #In memory cache for OCI session token signer
+
+def get_oci_signer() -> TokenExchangeSigner:
+
+ authntoken = get_access_token()
+ tokenID = authntoken.claims.get("jti")
+ token = authntoken.token
+
+ #Check if the signer exists for the token ID in memory cache
+ cached_signer = _global_token_cache.get(tokenID)
+ logger.debug(f"Global cached signer: {cached_signer}")
+ if cached_signer:
+ logger.debug(f"Using globally cached signer for token ID: {tokenID}")
+ return cached_signer
+
+ #If the signer is not yet created for the token then create new OCI signer object
+ logger.debug(f"Creating new signer for token ID: {tokenID}")
+ signer = TokenExchangeSigner(
+ jwt_or_func=token,
+ oci_domain_id=OCI_IAM_GUID.split(".")[0] if OCI_IAM_GUID else "",
+ client_id=OCI_CLIENT_ID,
+ client_secret=OCI_CLIENT_SECRET,
+ )
+ logger.debug(f"Signer {signer} created for token ID: {tokenID}")
+
+ #Cache the signer object in memory cache
+ _global_token_cache[tokenID] = signer
+ logger.debug(f"Signer cached for token ID: {tokenID}")
+
+ return signer
+```
+
+## Running MCP server
+
+Once the setup is complete, to run the MCP server, run the below command.
+```bash
+fastmcp run server.py:mcp --transport http --port 8000
+```
+
+To run MCP client, run the below command.
+```bash
+python3 client.py
+```
+
+MCP Client sample is as below.
+```python client.py
+from fastmcp import Client
+import asyncio
+
+async def main():
+ # The client will automatically handle OCI OAuth flows
+ async with Client("http://localhost:8000/mcp/", auth="oauth") as client:
+ # First-time connection will open OCI login in your browser
+ print("✓ Authenticated with OCI IAM")
+
+ tools = await client.list_tools()
+ print(f"🔧 Available tools ({len(tools)}):")
+ for tool in tools:
+ print(f" - {tool.name}: {tool.description}")
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+When you run the client for the first time:
+1. Your browser will open to OCI IAM's login page
+2. Sign in with your OCI account and grant the requested consent
+3. After authorization, you'll be redirected back to the redirect path
+4. The client receives the token and can make authenticated requests
+
+## Production Configuration
+
+
+
+For production deployments with persistent token management across server restarts, configure `jwt_signing_key`, and `client_storage`:
+
+```python server.py
+
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.oci import OCIProvider
+
+from key_value.aio.stores.redis import RedisStore
+from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
+from cryptography.fernet import Fernet
+
+# Load configuration from environment
+# Production setup with encrypted persistent token storage
+auth_provider = OCIProvider(
+ config_url=os.environ.get("OCI_CONFIG_URL"),
+ client_id=os.environ.get("OCI_CLIENT_ID"),
+ client_secret=os.environ.get("OCI_CLIENT_SECRET"),
+ base_url=os.environ.get("BASE_URL", "https://your-production-domain.com"),
+
+ # Production token management
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ client_storage=FernetEncryptionWrapper(
+ key_value=RedisStore(
+ host=os.environ["REDIS_HOST"],
+ port=int(os.environ["REDIS_PORT"])
+ ),
+ fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
+ )
+)
+
+mcp = FastMCP(name="Production OCI App", auth=auth_provider)
+```
+
+
+Parameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at Rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.
+
+For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
+
+
+
+The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.
+
\ No newline at end of file
diff --git a/docs/v3/integrations/openai.mdx b/docs/v3/integrations/openai.mdx
new file mode 100644
index 000000000..94ca82b40
--- /dev/null
+++ b/docs/v3/integrations/openai.mdx
@@ -0,0 +1,227 @@
+---
+title: OpenAI API 🤝 FastMCP
+sidebarTitle: OpenAI API
+description: Connect FastMCP servers to the OpenAI API
+icon: message-code
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+## Responses API
+
+OpenAI's [Responses API](https://platform.openai.com/docs/api-reference/responses) supports [MCP servers](https://platform.openai.com/docs/guides/tools-remote-mcp) as remote tool sources, allowing you to extend AI capabilities with custom functions.
+
+
+The Responses API is a distinct API from OpenAI's Completions API or Assistants API. At this time, only the Responses API supports MCP.
+
+
+
+Currently, the Responses API only accesses **tools** from MCP servers—it queries the `list_tools` endpoint and exposes those functions to the AI agent. Other MCP features like resources and prompts are not currently supported.
+
+
+
+### Create a Server
+
+First, create a FastMCP server with the tools you want to expose. For this example, we'll create a server with a single tool that rolls dice.
+
+```python server.py
+import random
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="Dice Roller")
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ """Roll `n_dice` 6-sided dice and return the results."""
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
+```
+
+### Deploy the Server
+
+Your server must be deployed to a public URL in order for OpenAI to access it.
+
+For development, you can use tools like `ngrok` to temporarily expose a locally-running server to the internet. We'll do that for this example (you may need to install `ngrok` and create a free account), but you can use any other method to deploy your server.
+
+Assuming you saved the above code as `server.py`, you can run the following two commands in two separate terminals to deploy your server and expose it to the internet:
+
+
+```bash FastMCP server
+python server.py
+```
+
+```bash ngrok
+ngrok http 8000
+```
+
+
+
+This exposes your unauthenticated server to the internet. Only run this command in a safe environment if you understand the risks.
+
+
+### Call the Server
+
+To use the Responses API, you'll need to install the OpenAI Python SDK (not included with FastMCP):
+
+```bash
+pip install openai
+```
+
+You'll also need to authenticate with OpenAI. You can do this by setting the `OPENAI_API_KEY` environment variable. Consult the OpenAI SDK documentation for more information.
+
+```bash
+export OPENAI_API_KEY="your-api-key"
+```
+
+Here is an example of how to call your server from Python. Note that you'll need to replace `https://your-server-url.com` with the actual URL of your server. In addition, we use `/mcp/` as the endpoint because we deployed a streamable-HTTP server with the default path; you may need to use a different endpoint if you customized your server's deployment.
+
+```python {4, 11-16}
+from openai import OpenAI
+
+# Your server URL (replace with your actual URL)
+url = 'https://your-server-url.com'
+
+client = OpenAI()
+
+resp = client.responses.create(
+ model="gpt-4.1",
+ tools=[
+ {
+ "type": "mcp",
+ "server_label": "dice_server",
+ "server_url": f"{url}/mcp/",
+ "require_approval": "never",
+ },
+ ],
+ input="Roll a few dice!",
+)
+
+print(resp.output_text)
+```
+If you run this code, you'll see something like the following output:
+
+```text
+You rolled 3 dice and got the following results: 6, 4, and 2!
+```
+
+### Authentication
+
+
+
+The Responses API can include headers to authenticate the request, which means you don't have to worry about your server being publicly accessible.
+
+#### Server Authentication
+
+The simplest way to add authentication to the server is to use a bearer token scheme.
+
+For this example, we'll quickly generate our own tokens with FastMCP's `RSAKeyPair` utility, but this may not be appropriate for production use. For more details, see the complete server-side [Token Verification](/servers/auth/token-verification) documentation.
+
+We'll start by creating an RSA key pair to sign and verify tokens.
+
+```python
+from fastmcp.server.auth.providers.jwt import RSAKeyPair
+
+key_pair = RSAKeyPair.generate()
+access_token = key_pair.create_token(audience="dice-server")
+```
+
+
+FastMCP's `RSAKeyPair` utility is for development and testing only.
+
+
+Next, we'll create a `JWTVerifier` to authenticate the server.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import JWTVerifier
+
+auth = JWTVerifier(
+ public_key=key_pair.public_key,
+ audience="dice-server",
+)
+
+mcp = FastMCP(name="Dice Roller", auth=auth)
+```
+
+Here is a complete example that you can copy/paste. For simplicity and the purposes of this example only, it will print the token to the console. **Do NOT do this in production!**
+
+```python server.py [expandable]
+from fastmcp import FastMCP
+from fastmcp.server.auth import JWTVerifier
+from fastmcp.server.auth.providers.jwt import RSAKeyPair
+import random
+
+key_pair = RSAKeyPair.generate()
+access_token = key_pair.create_token(audience="dice-server")
+
+auth = JWTVerifier(
+ public_key=key_pair.public_key,
+ audience="dice-server",
+)
+
+mcp = FastMCP(name="Dice Roller", auth=auth)
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ """Roll `n_dice` 6-sided dice and return the results."""
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+if __name__ == "__main__":
+ print(f"\n---\n\n🔑 Dice Roller access token:\n\n{access_token}\n\n---\n")
+ mcp.run(transport="http", port=8000)
+```
+
+#### Client Authentication
+
+If you try to call the authenticated server with the same OpenAI code we wrote earlier, you'll get an error like this:
+
+```text
+APIStatusError: Error code: 424 - {
+ "error": {
+ "message": "Error retrieving tool list from MCP server: 'dice_server'. Http status code: 401 (Unauthorized)",
+ "type": "external_connector_error",
+ "param": "tools",
+ "code": "http_error"
+ }
+}
+```
+
+As expected, the server is rejecting the request because it's not authenticated.
+
+To authenticate the client, you can pass the token in the `Authorization` header with the `Bearer` scheme:
+
+
+```python {4, 7, 19-21} [expandable]
+from openai import OpenAI
+
+# Your server URL (replace with your actual URL)
+url = 'https://your-server-url.com'
+
+# Your access token (replace with your actual token)
+access_token = 'your-access-token'
+
+client = OpenAI()
+
+resp = client.responses.create(
+ model="gpt-4.1",
+ tools=[
+ {
+ "type": "mcp",
+ "server_label": "dice_server",
+ "server_url": f"{url}/mcp/",
+ "require_approval": "never",
+ "headers": {
+ "Authorization": f"Bearer {access_token}"
+ }
+ },
+ ],
+ input="Roll a few dice!",
+)
+
+print(resp.output_text)
+```
+
+You should now see the dice roll results in the output.
\ No newline at end of file
diff --git a/docs/v3/integrations/openapi.mdx b/docs/v3/integrations/openapi.mdx
new file mode 100644
index 000000000..f5f2b3dfa
--- /dev/null
+++ b/docs/v3/integrations/openapi.mdx
@@ -0,0 +1,456 @@
+---
+title: OpenAPI 🤝 FastMCP
+sidebarTitle: OpenAPI
+description: Generate MCP servers from any OpenAPI specification
+icon: list-tree
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+FastMCP can automatically generate an MCP server from any OpenAPI specification, allowing AI models to interact with existing APIs through the MCP protocol. Instead of manually creating tools and resources, you provide an OpenAPI spec and FastMCP intelligently converts API endpoints into the appropriate MCP components.
+
+
+Under the hood, OpenAPI integration uses OpenAPIProvider (v3.0.0+) to source tools from the specification. See [Providers](/servers/providers/overview) to understand how FastMCP sources components.
+
+
+
+Generating MCP servers from OpenAPI is a great way to get started with FastMCP, but in practice LLMs achieve **significantly better performance** with well-designed and curated MCP servers than with auto-converted OpenAPI servers. This is especially true for complex APIs with many endpoints and parameters.
+
+We recommend using the FastAPI integration for bootstrapping and prototyping, not for mirroring your API to LLM clients. See the post [Stop Converting Your REST APIs to MCP](https://www.jlowin.dev/blog/stop-converting-rest-apis-to-mcp) for more details.
+
+
+## Create a Server
+
+To convert an OpenAPI specification to an MCP server, use the `FastMCP.from_openapi()` class method:
+
+```python server.py
+import httpx
+from fastmcp import FastMCP
+
+# Create an HTTP client for your API
+client = httpx.AsyncClient(base_url="https://api.example.com")
+
+# Load your OpenAPI spec
+openapi_spec = httpx.get("https://api.example.com/openapi.json").json()
+
+# Create the MCP server
+mcp = FastMCP.from_openapi(
+ openapi_spec=openapi_spec,
+ client=client,
+ name="My API Server"
+)
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+### Authentication
+
+If your API requires authentication, configure it on the HTTP client:
+
+```python
+import httpx
+from fastmcp import FastMCP
+
+# Bearer token authentication
+api_client = httpx.AsyncClient(
+ base_url="https://api.example.com",
+ headers={"Authorization": "Bearer YOUR_TOKEN"}
+)
+
+# Create MCP server with authenticated client
+mcp = FastMCP.from_openapi(
+ openapi_spec=spec,
+ client=api_client,
+ timeout=30.0 # 30 second timeout for all requests
+)
+```
+
+## Route Mapping
+
+By default, FastMCP converts **every endpoint** in your OpenAPI specification into an MCP **Tool**. This provides a simple, predictable starting point that ensures all your API's functionality is immediately available to the vast majority of LLM clients which only support MCP tools.
+
+While this is a pragmatic default for maximum compatibility, you can easily customize this behavior. Internally, FastMCP uses an ordered list of `RouteMap` objects to determine how to map OpenAPI routes to various MCP component types.
+
+Each `RouteMap` specifies a combination of methods, patterns, and tags, as well as a corresponding MCP component type. Each OpenAPI route is checked against each `RouteMap` in order, and the first one that matches every criteria is used to determine its converted MCP type. A special type, `EXCLUDE`, can be used to exclude routes from the MCP server entirely.
+
+- **Methods**: HTTP methods to match (e.g. `["GET", "POST"]` or `"*"` for all)
+- **Pattern**: Regex pattern to match the route path (e.g. `r"^/users/.*"` or `r".*"` for all)
+- **Tags**: A set of OpenAPI tags that must all be present. An empty set (`{}`) means no tag filtering, so the route matches regardless of its tags.
+- **MCP type**: What MCP component type to create (`TOOL`, `RESOURCE`, `RESOURCE_TEMPLATE`, or `EXCLUDE`)
+- **MCP tags**: A set of custom tags to add to components created from matching routes
+
+Here is FastMCP's default rule:
+
+```python
+from fastmcp.server.providers.openapi import RouteMap, MCPType
+
+DEFAULT_ROUTE_MAPPINGS = [
+ # All routes become tools
+ RouteMap(mcp_type=MCPType.TOOL),
+]
+```
+
+### Custom Route Maps
+
+When creating your FastMCP server, you can customize routing behavior by providing your own list of `RouteMap` objects. Your custom maps are processed before the default route maps, and routes will be assigned to the first matching custom map.
+
+For example, prior to FastMCP 2.8.0, GET requests were automatically mapped to `Resource` and `ResourceTemplate` components based on whether they had path parameters. (This was changed solely for client compatibility reasons.) You can restore this behavior by providing custom route maps:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.providers.openapi import RouteMap, MCPType
+
+# Restore pre-2.8.0 semantic mapping
+semantic_maps = [
+ # GET requests with path parameters become ResourceTemplates
+ RouteMap(methods=["GET"], pattern=r".*\{.*\}.*", mcp_type=MCPType.RESOURCE_TEMPLATE),
+ # All other GET requests become Resources
+ RouteMap(methods=["GET"], pattern=r".*", mcp_type=MCPType.RESOURCE),
+]
+
+mcp = FastMCP.from_openapi(
+ openapi_spec=spec,
+ client=client,
+ route_maps=semantic_maps,
+)
+```
+
+With these maps, `GET` requests are handled semantically, and all other methods (`POST`, `PUT`, etc.) will fall through to the default rule and become `Tool`s.
+
+Here is a more complete example that uses custom route maps to convert all `GET` endpoints under `/analytics/` to tools while excluding all admin endpoints and all routes tagged "internal". All other routes will be handled by the default rules:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.providers.openapi import RouteMap, MCPType
+
+mcp = FastMCP.from_openapi(
+ openapi_spec=spec,
+ client=client,
+ route_maps=[
+ # Analytics `GET` endpoints are tools
+ RouteMap(
+ methods=["GET"],
+ pattern=r"^/analytics/.*",
+ mcp_type=MCPType.TOOL,
+ ),
+
+ # Exclude all admin endpoints
+ RouteMap(
+ pattern=r"^/admin/.*",
+ mcp_type=MCPType.EXCLUDE,
+ ),
+
+ # Exclude all routes tagged "internal"
+ RouteMap(
+ tags={"internal"},
+ mcp_type=MCPType.EXCLUDE,
+ ),
+ ],
+)
+```
+
+
+The default route maps are always applied after your custom maps, so you do not have to create route maps for every possible route.
+
+
+### Excluding Routes
+
+To exclude routes from the MCP server, use a route map to assign them to `MCPType.EXCLUDE`.
+
+You can use this to remove sensitive or internal routes by targeting them specifically:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.providers.openapi import RouteMap, MCPType
+
+mcp = FastMCP.from_openapi(
+ openapi_spec=spec,
+ client=client,
+ route_maps=[
+ RouteMap(pattern=r"^/admin/.*", mcp_type=MCPType.EXCLUDE),
+ RouteMap(tags={"internal"}, mcp_type=MCPType.EXCLUDE),
+ ],
+)
+```
+
+Or you can use a catch-all rule to exclude everything that your maps don't handle explicitly:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.providers.openapi import RouteMap, MCPType
+
+mcp = FastMCP.from_openapi(
+ openapi_spec=spec,
+ client=client,
+ route_maps=[
+ # custom mapping logic goes here
+ # ... your specific route maps ...
+ # exclude all remaining routes
+ RouteMap(mcp_type=MCPType.EXCLUDE),
+ ],
+)
+```
+
+
+Using a catch-all exclusion rule will prevent the default route mappings from being applied, since it will match every remaining route. This is useful if you want to explicitly allow-list certain routes.
+
+
+### Advanced Route Mapping
+
+
+
+For advanced use cases that require more complex logic, you can provide a `route_map_fn` callable. After the route map logic is applied, this function is called on each matched route and its assigned MCP component type. It can optionally return a different component type to override the mapped assignment. If it returns `None`, the assigned type is used.
+
+In addition to more precise targeting of methods, patterns, and tags, this function can access any additional OpenAPI metadata about the route.
+
+
+The `route_map_fn` is called on all routes, even those that matched `MCPType.EXCLUDE` in your custom maps. This gives you an opportunity to customize the mapping or even override an exclusion.
+
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.providers.openapi import RouteMap, MCPType
+from fastmcp.utilities.openapi import HTTPRoute
+
+def custom_route_mapper(route: HTTPRoute, mcp_type: MCPType) -> MCPType | None:
+ """Advanced route type mapping."""
+ # Convert all admin routes to tools regardless of HTTP method
+ if "/admin/" in route.path:
+ return MCPType.TOOL
+
+ elif "internal" in route.tags:
+ return MCPType.EXCLUDE
+
+ # Convert user detail routes to templates even if they're POST
+ elif route.path.startswith("/users/") and route.method == "POST":
+ return MCPType.RESOURCE_TEMPLATE
+
+ # Use defaults for all other routes
+ return None
+
+mcp = FastMCP.from_openapi(
+ openapi_spec=spec,
+ client=client,
+ route_map_fn=custom_route_mapper,
+)
+```
+
+## Customization
+
+### Component Names
+
+
+
+FastMCP automatically generates names for MCP components based on the OpenAPI specification. By default, it uses the `operationId` from your OpenAPI spec, up to the first double underscore (`__`).
+
+All component names are automatically:
+- **Slugified**: Spaces and special characters are converted to underscores or removed
+- **Truncated**: Limited to 56 characters maximum to ensure compatibility
+- **Unique**: If multiple components have the same name, a number is automatically appended to make them unique
+
+For more control over component names, you can provide an `mcp_names` dictionary that maps `operationId` values to your desired names. The `operationId` must be exactly as it appears in the OpenAPI spec. The provided name will always be slugified and truncated.
+
+```python
+mcp = FastMCP.from_openapi(
+ openapi_spec=spec,
+ client=client,
+ mcp_names={
+ "list_users__with_pagination": "user_list",
+ "create_user__admin_required": "create_user",
+ "get_user_details__admin_required": "user_detail",
+ }
+)
+```
+
+Any `operationId` not found in `mcp_names` will use the default strategy (operationId up to the first `__`).
+
+### Tags
+
+
+
+FastMCP provides several ways to add tags to your MCP components, allowing you to categorize and organize them for better discoverability and filtering. Tags are combined from multiple sources to create the final set of tags on each component.
+
+#### RouteMap Tags
+
+You can add custom tags to components created from specific routes using the `mcp_tags` parameter in `RouteMap`. These tags will be applied to all components created from routes that match that particular route map.
+
+```python
+from fastmcp.server.providers.openapi import RouteMap, MCPType
+
+mcp = FastMCP.from_openapi(
+ openapi_spec=spec,
+ client=client,
+ route_maps=[
+ # Add custom tags to all POST endpoints
+ RouteMap(
+ methods=["POST"],
+ pattern=r".*",
+ mcp_type=MCPType.TOOL,
+ mcp_tags={"write-operation", "api-mutation"}
+ ),
+
+ # Add different tags to detail view endpoints
+ RouteMap(
+ methods=["GET"],
+ pattern=r".*\{.*\}.*",
+ mcp_type=MCPType.RESOURCE_TEMPLATE,
+ mcp_tags={"detail-view", "parameterized"}
+ ),
+
+ # Add tags to list endpoints
+ RouteMap(
+ methods=["GET"],
+ pattern=r".*",
+ mcp_type=MCPType.RESOURCE,
+ mcp_tags={"list-data", "collection"}
+ ),
+ ],
+)
+```
+
+#### Global Tags
+
+You can add tags to **all** components by providing a `tags` parameter when creating your MCP server. These global tags will be applied to every component created from your OpenAPI specification.
+
+```python
+mcp = FastMCP.from_openapi(
+ openapi_spec=spec,
+ client=client,
+ tags={"api-v2", "production", "external"}
+)
+```
+
+#### OpenAPI Tags in Client Meta
+
+FastMCP automatically includes OpenAPI tags from your specification in the component's metadata. These tags are available to MCP clients through the `meta.fastmcp.tags` field, allowing clients to filter and organize components based on the original OpenAPI tagging:
+
+
+```json {5} OpenAPI spec with tags
+{
+ "paths": {
+ "/users": {
+ "get": {
+ "tags": ["users", "public"],
+ "operationId": "list_users",
+ "summary": "List all users"
+ }
+ }
+ }
+}
+```
+```python {6-9} Access OpenAPI tags in MCP client
+async with client:
+ tools = await client.list_tools()
+ for tool in tools:
+ if tool.meta:
+ # OpenAPI tags are now available in fastmcp namespace!
+ fastmcp_meta = tool.meta.get('fastmcp', {})
+ openapi_tags = fastmcp_meta.get('tags', [])
+ if 'users' in openapi_tags:
+ print(f"Found user-related tool: {tool.name}")
+```
+
+
+This makes it easy for clients to understand and organize API endpoints based on their original OpenAPI categorization.
+
+### Advanced Customization
+
+
+
+By default, FastMCP creates MCP components using a variety of metadata from the OpenAPI spec, such as incorporating the OpenAPI description into the MCP component description.
+
+At times you may want to modify those MCP components in a variety of ways, such as adding LLM-specific instructions or tags. For fine-grained customization, you can provide a `mcp_component_fn` when creating the MCP server. After each MCP component has been created, this function is called on it and has the opportunity to modify it in-place.
+
+
+Your `mcp_component_fn` is expected to modify the component in-place, not to return a new component. The result of the function is ignored.
+
+
+```python
+from fastmcp.server.providers.openapi import (
+ OpenAPITool,
+ OpenAPIResource,
+ OpenAPIResourceTemplate,
+)
+from fastmcp.utilities.openapi import HTTPRoute
+
+def customize_components(
+ route: HTTPRoute,
+ component: OpenAPITool | OpenAPIResource | OpenAPIResourceTemplate,
+) -> None:
+ # Add custom tags to all components
+ component.tags.add("openapi")
+
+ # Customize based on component type
+ if isinstance(component, OpenAPITool):
+ component.description = f"🔧 {component.description} (via API)"
+
+ if isinstance(component, OpenAPIResource):
+ component.description = f"📊 {component.description}"
+ component.tags.add("data")
+
+mcp = FastMCP.from_openapi(
+ openapi_spec=spec,
+ client=client,
+ mcp_component_fn=customize_components,
+)
+```
+
+## Request Parameter Handling
+
+FastMCP intelligently handles different types of parameters in OpenAPI requests:
+
+### Query Parameters
+
+By default, FastMCP only includes query parameters that have non-empty values. Parameters with `None` values or empty strings are automatically filtered out.
+
+```python
+# When calling this tool...
+await client.call_tool("search_products", {
+ "category": "electronics", # ✅ Included
+ "min_price": 100, # ✅ Included
+ "max_price": None, # ❌ Excluded
+ "brand": "", # ❌ Excluded
+})
+
+# The HTTP request will be: GET /products?category=electronics&min_price=100
+```
+
+### Path Parameters
+
+Path parameters are typically required by REST APIs. FastMCP:
+- Filters out `None` values
+- Validates that all required path parameters are provided
+- Raises clear errors for missing required parameters
+
+```python
+# ✅ This works
+await client.call_tool("get_user", {"user_id": 123})
+
+# ❌ This raises: "Missing required path parameters: {'user_id'}"
+await client.call_tool("get_user", {"user_id": None})
+```
+
+### Array Parameters
+
+FastMCP handles array parameters according to OpenAPI specifications:
+
+- **Query arrays**: Serialized based on the `explode` parameter (default: `True`)
+- **Path arrays**: Serialized as comma-separated values (OpenAPI 'simple' style)
+
+```python
+# Query array with explode=true (default)
+# ?tags=red&tags=blue&tags=green
+
+# Query array with explode=false
+# ?tags=red,blue,green
+
+# Path array (always comma-separated)
+# /items/red,blue,green
+```
+
+### Headers
+
+Header parameters are automatically converted to strings and included in the HTTP request.
\ No newline at end of file
diff --git a/docs/v3/integrations/permit.mdx b/docs/v3/integrations/permit.mdx
new file mode 100644
index 000000000..066f5b1ea
--- /dev/null
+++ b/docs/v3/integrations/permit.mdx
@@ -0,0 +1,352 @@
+---
+title: Permit.io Authorization 🤝 FastMCP
+sidebarTitle: Permit.io
+description: Add fine-grained authorization to your FastMCP servers with Permit.io
+icon: shield-check
+---
+
+Add **policy-based authorization** to your FastMCP servers with one-line code addition with the **[Permit.io][permit-github] authorization middleware**.
+
+Control which tools, resources and prompts MCP clients can view and execute on your server. Define dynamic policies using Permit.io's powerful RBAC, ABAC, and REBAC capabilities, and obtain comprehensive audit logs of all access attempts and violations.
+
+## How it Works
+
+Leveraging FastMCP's [Middleware][fastmcp-middleware], the Permit.io middleware intercepts all MCP requests to your server and automatically maps MCP methods to authorization checks against your Permit.io policies; covering both server methods and tool execution.
+
+### Policy Mapping
+
+The middleware automatically maps MCP methods to Permit.io resources and actions:
+
+- **MCP server methods** (e.g., `tools/list`, `resources/read`):
+ - **Resource**: `{server_name}_{component}` (e.g., `myserver_tools`)
+ - **Action**: The method verb (e.g., `list`, `read`)
+- **Tool execution** (method `tools/call`):
+ - **Resource**: `{server_name}` (e.g., `myserver`)
+ - **Action**: The tool name (e.g., `greet`)
+
+
+
+*Example: In Permit.io, the 'Admin' role is granted permissions on resources and actions as mapped by the middleware. For example, 'greet', 'greet-jwt', and 'login' are actions on the 'mcp_server' resource, and 'list' is an action on the 'mcp_server_tools' resource.*
+
+> **Note:**
+> Don't forget to assign the relevant role (e.g., Admin, User) to the user authenticating to your MCP server (such as the user in the JWT) in the Permit.io Directory. Without the correct role assignment, users will not have access to the resources and actions you've configured in your policies.
+>
+> 
+>
+> *Example: In Permit.io Directory, both 'client' and 'admin' users are assigned the 'Admin' role, granting them the permissions defined in your policy mapping.*
+
+For detailed policy mapping examples and configuration, see [Detailed Policy Mapping](https://github.com/permitio/permit-fastmcp/blob/main/docs/policy-mapping.md).
+
+### Listing Operations
+
+The middleware behaves as a filter for listing operations (`tools/list`, `resources/list`, `prompts/list`), hiding to the client components that are not authorized by the defined policies.
+
+```mermaid
+sequenceDiagram
+ participant MCPClient as MCP Client
+ participant PermitMiddleware as Permit.io Middleware
+ participant MCPServer as FastMCP Server
+ participant PermitPDP as Permit.io PDP
+
+ MCPClient->>PermitMiddleware: MCP Listing Request (e.g., tools/list)
+ PermitMiddleware->>MCPServer: MCP Listing Request
+ MCPServer-->>PermitMiddleware: MCP Listing Response
+ PermitMiddleware->>PermitPDP: Authorization Checks
+ PermitPDP->>PermitMiddleware: Authorization Decisions
+ PermitMiddleware-->>MCPClient: Filtered MCP Listing Response
+```
+
+### Execution Operations
+
+The middleware behaves as an enforcement point for execution operations (`tools/call`, `resources/read`, `prompts/get`), blocking operations that are not authorized by the defined policies.
+
+```mermaid
+sequenceDiagram
+ participant MCPClient as MCP Client
+ participant PermitMiddleware as Permit.io Middleware
+ participant MCPServer as FastMCP Server
+ participant PermitPDP as Permit.io PDP
+
+ MCPClient->>PermitMiddleware: MCP Execution Request (e.g., tools/call)
+ PermitMiddleware->>PermitPDP: Authorization Check
+ PermitPDP->>PermitMiddleware: Authorization Decision
+ PermitMiddleware-->>MCPClient: MCP Unauthorized Error (if denied)
+ PermitMiddleware->>MCPServer: MCP Execution Request (if allowed)
+ MCPServer-->>PermitMiddleware: MCP Execution Response (if allowed)
+ PermitMiddleware-->>MCPClient: MCP Execution Response (if allowed)
+```
+
+## Add Authorization to Your Server
+
+
+Permit.io is a cloud-native authorization service. You need a Permit.io account and a running Policy Decision Point (PDP) for the middleware to function. You can run the PDP locally with Docker or use Permit.io's cloud PDP.
+
+
+### Prerequisites
+
+1. **Permit.io Account**: Sign up at [permit.io](https://permit.io)
+2. **PDP Setup**: Run the Permit.io PDP locally or use the cloud PDP (RBAC only)
+3. **API Key**: Get your Permit.io API key from the dashboard
+
+### Run the Permit.io PDP
+
+Run the PDP locally with Docker:
+
+```bash
+docker run -p 7766:7766 permitio/pdp:latest
+```
+
+Or use the cloud PDP URL: `https://cloudpdp.api.permit.io`
+
+### Create a Server with Authorization
+
+First, install the `permit-fastmcp` package:
+
+```bash
+# Using UV (recommended)
+uv add permit-fastmcp
+
+# Using pip
+pip install permit-fastmcp
+```
+
+Then create a FastMCP server and add the Permit.io middleware:
+
+```python server.py
+from fastmcp import FastMCP
+from permit_fastmcp.middleware.middleware import PermitMcpMiddleware
+
+mcp = FastMCP("Secure FastMCP Server 🔒")
+
+@mcp.tool
+def greet(name: str) -> str:
+ """Greet a user by name"""
+ return f"Hello, {name}!"
+
+@mcp.tool
+def add(a: int, b: int) -> int:
+ """Add two numbers"""
+ return a + b
+
+# Add Permit.io authorization middleware
+mcp.add_middleware(PermitMcpMiddleware(
+ permit_pdp_url="http://localhost:7766",
+ permit_api_key="your-permit-api-key"
+))
+
+if __name__ == "__main__":
+ mcp.run(transport="http")
+```
+
+### Configure Access Policies
+
+Create your authorization policies in the Permit.io dashboard:
+
+1. **Create Resources**: Define resources like `mcp_server` and `mcp_server_tools`
+2. **Define Actions**: Add actions like `greet`, `add`, `list`, `read`
+3. **Create Roles**: Define roles like `Admin`, `User`, `Guest`
+4. **Assign Permissions**: Grant roles access to specific resources and actions
+5. **Assign Users**: Assign roles to users in the Permit.io Directory
+
+For step-by-step setup instructions and troubleshooting, see [Getting Started & FAQ](https://github.com/permitio/permit-fastmcp/blob/main/docs/getting-started.md).
+
+#### Example Policy Configuration
+
+Policies are defined in the Permit.io dashboard, but you can also use the [Permit.io Terraform provider](https://github.com/permitio/terraform-provider-permitio) to define policies in code.
+
+
+```terraform
+# Resources
+resource "permitio_resource" "mcp_server" {
+ name = "mcp_server"
+ key = "mcp_server"
+
+ actions = {
+ "greet" = { name = "greet" }
+ "add" = { name = "add" }
+ }
+}
+
+resource "permitio_resource" "mcp_server_tools" {
+ name = "mcp_server_tools"
+ key = "mcp_server_tools"
+
+ actions = {
+ "list" = { name = "list" }
+ }
+}
+
+# Roles
+resource "permitio_role" "Admin" {
+ key = "Admin"
+ name = "Admin"
+ permissions = [
+ "mcp_server:greet",
+ "mcp_server:add",
+ "mcp_server_tools:list"
+ ]
+}
+```
+
+You can also use the [Permit.io CLI](https://github.com/permitio/permit-cli), [API](https://api.permit.io/scalar) or [SDKs](https://github.com/permitio/permit-python) to manage policies, as well as writing policies directly in REGO (Open Policy Agent's policy language).
+
+For complete policy examples including ABAC and RBAC configurations, see [Example Policies](https://github.com/permitio/permit-fastmcp/tree/main/docs/example_policies).
+
+### Identity Management
+
+The middleware supports multiple identity extraction modes:
+
+- **Fixed Identity**: Use a fixed identity for all requests
+- **Header-based**: Extract identity from HTTP headers
+- **JWT-based**: Extract and verify JWT tokens
+- **Source-based**: Use the MCP context source field
+
+For detailed identity mode configuration and environment variables, see [Identity Modes & Environment Variables](https://github.com/permitio/permit-fastmcp/blob/main/docs/identity-modes.md).
+
+#### JWT Authentication Example
+
+```python
+import os
+
+# Configure JWT identity extraction
+os.environ["PERMIT_MCP_IDENTITY_MODE"] = "jwt"
+os.environ["PERMIT_MCP_IDENTITY_JWT_SECRET"] = "your-jwt-secret"
+
+mcp.add_middleware(PermitMcpMiddleware(
+ permit_pdp_url="http://localhost:7766",
+ permit_api_key="your-permit-api-key"
+))
+```
+
+### ABAC Policies with Tool Arguments
+
+The middleware supports Attribute-Based Access Control (ABAC) policies that can evaluate tool arguments as attributes. Tool arguments are automatically flattened as individual attributes (e.g., `arg_name`, `arg_number`) for granular policy conditions.
+
+
+
+*Example: Create dynamic resources with conditions like `resource.arg_number greater-than 10` to allow the `conditional-greet` tool only when the number argument exceeds 10.*
+
+#### Example: Conditional Access
+
+Create a dynamic resource with conditions like `resource.arg_number greater-than 10` to allow the `conditional-greet` tool only when the number argument exceeds 10.
+
+```python
+@mcp.tool
+def conditional_greet(name: str, number: int) -> str:
+ """Greet a user only if number > 10"""
+ return f"Hello, {name}! Your number is {number}"
+```
+
+
+
+*Example: The Admin role is granted access to the "conditional-greet" action on the "Big-greets" dynamic resource, while other tools like "greet", "greet-jwt", and "login" are granted on the base "mcp_server" resource.*
+
+For comprehensive ABAC configuration and advanced policy examples, see [ABAC Policies with Tool Arguments](https://github.com/permitio/permit-fastmcp/blob/main/docs/policy-mapping.md#abac-policies-with-tool-arguments).
+
+### Run the Server
+
+Start your FastMCP server normally:
+
+```bash
+python server.py
+```
+
+The middleware will now intercept all MCP requests and check them against your Permit.io policies. Requests include user identification through the configured identity mode and automatic mapping of MCP methods to authorization resources and actions.
+
+## Advanced Configuration
+
+### Environment Variables
+
+Configure the middleware using environment variables:
+
+```bash
+# Permit.io configuration
+export PERMIT_MCP_PERMIT_PDP_URL="http://localhost:7766"
+export PERMIT_MCP_PERMIT_API_KEY="your-api-key"
+
+# Identity configuration
+export PERMIT_MCP_IDENTITY_MODE="jwt"
+export PERMIT_MCP_IDENTITY_JWT_SECRET="your-jwt-secret"
+
+# Method configuration
+export PERMIT_MCP_KNOWN_METHODS='["tools/list","tools/call"]'
+export PERMIT_MCP_BYPASSED_METHODS='["initialize","ping"]'
+
+# Logging configuration
+export PERMIT_MCP_ENABLE_AUDIT_LOGGING="true"
+```
+
+For a complete list of all configuration options and environment variables, see [Configuration Reference](https://github.com/permitio/permit-fastmcp/blob/main/docs/configuration-reference.md).
+
+### Custom Middleware Configuration
+
+```python
+from permit_fastmcp.middleware.middleware import PermitMcpMiddleware
+
+middleware = PermitMcpMiddleware(
+ permit_pdp_url="http://localhost:7766",
+ permit_api_key="your-api-key",
+ enable_audit_logging=True,
+ bypass_methods=["initialize", "ping", "health/*"]
+)
+
+mcp.add_middleware(middleware)
+```
+
+For advanced configuration options and custom middleware extensions, see [Advanced Configuration](https://github.com/permitio/permit-fastmcp/blob/main/docs/advanced-configuration.md).
+
+## Example: Complete JWT Authentication Server
+
+See the [example server](https://github.com/permitio/permit-fastmcp/blob/main/permit_fastmcp/example_server/example.py) for a full implementation with JWT-based authentication. For additional examples and usage patterns, see [Example Server](https://github.com/permitio/permit-fastmcp/blob/main/permit_fastmcp/example_server/):
+
+```python
+from fastmcp import FastMCP, Context
+from permit_fastmcp.middleware.middleware import PermitMcpMiddleware
+import jwt
+import datetime
+
+# Configure JWT identity extraction
+os.environ["PERMIT_MCP_IDENTITY_MODE"] = "jwt"
+os.environ["PERMIT_MCP_IDENTITY_JWT_SECRET"] = "mysecretkey"
+
+mcp = FastMCP("My MCP Server")
+
+@mcp.tool
+def login(username: str, password: str) -> str:
+ """Login to get a JWT token"""
+ if username == "admin" and password == "password":
+ token = jwt.encode(
+ {"sub": username, "exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1)},
+ "mysecretkey",
+ algorithm="HS256"
+ )
+ return f"Bearer {token}"
+ raise Exception("Invalid credentials")
+
+@mcp.tool
+def greet_jwt(ctx: Context) -> str:
+ """Greet a user by extracting their name from JWT"""
+ # JWT extraction handled by middleware
+ return "Hello, authenticated user!"
+
+mcp.add_middleware(PermitMcpMiddleware(
+ permit_pdp_url="http://localhost:7766",
+ permit_api_key="your-permit-api-key"
+))
+
+if __name__ == "__main__":
+ mcp.run(transport="http")
+```
+
+
+ For detailed policy configuration, custom authentication, and advanced
+ deployment patterns, visit the [Permit.io FastMCP Middleware
+ repository][permit-fastmcp-github]. For troubleshooting common issues, see [Troubleshooting](https://github.com/permitio/permit-fastmcp/blob/main/docs/troubleshooting.md).
+
+
+
+[permit.io]: https://www.permit.io
+[permit-github]: https://github.com/permitio
+[permit-fastmcp-github]: https://github.com/permitio/permit-fastmcp
+[Agent.Security]: https://agent.security
+[fastmcp-middleware]: /servers/middleware
diff --git a/docs/v3/integrations/propelauth.mdx b/docs/v3/integrations/propelauth.mdx
new file mode 100644
index 000000000..7f21d2010
--- /dev/null
+++ b/docs/v3/integrations/propelauth.mdx
@@ -0,0 +1,164 @@
+---
+title: PropelAuth 🤝 FastMCP
+sidebarTitle: PropelAuth
+description: Secure your FastMCP server with PropelAuth
+icon: shield-check
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx";
+
+
+
+This guide shows you how to secure your FastMCP server using [**PropelAuth**](https://www.propelauth.com), a complete authentication and user management solution. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern, where PropelAuth handles user login, consent management, and your FastMCP server validates the tokens.
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+
+1. A [PropelAuth](https://www.propelauth.com) account
+2. Your FastMCP server's base URL (can be localhost for development, e.g., `http://localhost:8000`)
+
+### Step 1: Configure PropelAuth
+
+
+
+ Navigate to the **MCP** section in your PropelAuth dashboard, click **Enable MCP**, and choose which environments to enable it for (Test, Staging, Prod).
+
+
+
+ Under **MCP > Allowed MCP Clients**, add redirect URIs for each MCP client you want to allow. PropelAuth provides templates for popular clients like Claude, Cursor, and ChatGPT.
+
+
+
+ Under **MCP > Scopes**, define the permissions available to MCP clients (e.g., `read:user_data`).
+
+
+
+ Under **MCP > Settings > How Do Users Create OAuth Clients?**, you can optionally enable:
+ - **Dynamic Client Registration** — clients self-register automatically via the DCR protocol
+ - **Manually via Hosted Pages** — PropelAuth creates a UI for your users to register OAuth clients
+
+ You can enable neither, one, or both. If you enable neither, you'll manage OAuth client creation yourself.
+
+
+
+ Go to **MCP > Request Validation** and click **Create Credentials**. Note the **Client ID** and **Client Secret** - you'll need these to validate tokens.
+
+
+
+ Find your Auth URL in the **Backend Integration** section of the dashboard (e.g., `https://auth.yourdomain.com`).
+
+
+
+For more details, see the [PropelAuth MCP documentation](https://docs.propelauth.com/mcp-authentication/overview).
+
+### Step 2: Environment Setup
+
+Create a `.env` file with your PropelAuth configuration:
+
+```bash
+PROPELAUTH_AUTH_URL=https://auth.yourdomain.com # From Backend Integration page
+PROPELAUTH_INTROSPECTION_CLIENT_ID=your-client-id # From MCP > Request Validation
+PROPELAUTH_INTROSPECTION_CLIENT_SECRET=your-client-secret # From MCP > Request Validation
+SERVER_URL=http://localhost:8000 # Your server's base URL
+```
+
+### Step 3: FastMCP Configuration
+
+Create your FastMCP server file and use the PropelAuthProvider to handle all the OAuth integration automatically:
+
+```python server.py
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.propelauth import PropelAuthProvider
+
+auth_provider = PropelAuthProvider(
+ auth_url=os.environ["PROPELAUTH_AUTH_URL"],
+ introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
+ introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
+ base_url=os.environ["SERVER_URL"],
+ required_scopes=["read:user_data"], # Optional scope enforcement
+)
+
+mcp = FastMCP(name="My PropelAuth Protected Server", auth=auth_provider)
+```
+
+## Testing
+
+With your `.env` loaded, start the server:
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+Then use a FastMCP client to verify authentication works:
+
+```python
+from fastmcp import Client
+import asyncio
+
+async def main():
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ assert await client.ping()
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+## Accessing User Information
+
+You can use `get_access_token()` inside your tools to identify the authenticated user:
+
+```python server.py
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.propelauth import PropelAuthProvider
+from fastmcp.server.dependencies import get_access_token
+
+auth = PropelAuthProvider(
+ auth_url=os.environ["PROPELAUTH_AUTH_URL"],
+ introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
+ introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
+ base_url=os.environ["SERVER_URL"],
+ required_scopes=["read:user_data"],
+)
+
+mcp = FastMCP(name="My PropelAuth Protected Server", auth=auth)
+
+@mcp.tool
+def whoami() -> dict:
+ """Return the authenticated user's ID."""
+ token = get_access_token()
+ if token is None:
+ return {"error": "Not authenticated"}
+ user_id = token.claims.get("sub")
+ return {"user_id": user_id}
+```
+
+## Advanced Configuration
+
+The `PropelAuthProvider` supports optional overrides for token introspection behavior, including caching and request timeouts:
+
+```python server.py
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.propelauth import PropelAuthProvider
+
+auth = PropelAuthProvider(
+ auth_url=os.environ["PROPELAUTH_AUTH_URL"],
+ introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
+ introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
+ base_url=os.environ.get("BASE_URL", "https://your-server.com"),
+ required_scopes=["read:user_data"],
+ resource="https://your-server.com/mcp", # Restrict to tokens intended for this server (RFC 8707)
+ token_introspection_overrides={
+ "cache_ttl_seconds": 300, # Cache introspection results for 5 minutes
+ "max_cache_size": 1000, # Maximum cached tokens
+ "timeout_seconds": 15, # HTTP request timeout
+ },
+)
+
+mcp = FastMCP(name="My PropelAuth Protected Server", auth=auth)
+```
diff --git a/docs/v3/integrations/pydantic-ai.mdx b/docs/v3/integrations/pydantic-ai.mdx
new file mode 100644
index 000000000..0c8ffa524
--- /dev/null
+++ b/docs/v3/integrations/pydantic-ai.mdx
@@ -0,0 +1,137 @@
+---
+title: Pydantic AI 🤝 FastMCP
+sidebarTitle: Pydantic AI
+description: Connect FastMCP servers to Pydantic AI agents using the FastMCPToolset
+icon: message-code
+---
+
+[Pydantic AI](https://ai.pydantic.dev/) ships a [`FastMCPToolset`](https://ai.pydantic.dev/mcp/fastmcp-client/) that lets a Pydantic AI agent call tools exposed by any MCP server through the [FastMCP Client](/clients/client). Because the toolset is built on the FastMCP Client, it works with FastMCP servers as well as any other MCP server, and supports the full range of [transports](/clients/transports): in-memory, STDIO, Streamable HTTP, and SSE.
+
+This page shows how to point `FastMCPToolset` at a FastMCP server, with examples for each transport. For the toolset's full API, see the [Pydantic AI documentation](https://ai.pydantic.dev/mcp/fastmcp-client/).
+
+
+The `FastMCPToolset` currently exposes **tools** to the agent. Other MCP features such as elicitation and sampling are not yet supported through this toolset; use Pydantic AI's standard [`MCPServer`](https://ai.pydantic.dev/mcp/client/) client if you need them.
+
+
+## Install
+
+`FastMCPToolset` lives in `pydantic-ai-slim` behind the `fastmcp` optional group:
+
+```bash
+pip install "pydantic-ai-slim[fastmcp]"
+```
+
+## Create a Server
+
+Create a FastMCP server with the tools you want to expose. We'll use a single dice-rolling tool throughout this guide.
+
+```python server.py
+import random
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="Dice Roller")
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ """Roll `n_dice` 6-sided dice and return the results."""
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
+```
+
+## In-Memory
+
+If your FastMCP server lives in the same process as your agent, pass the `FastMCP` instance directly. The toolset reuses an [in-memory transport](/clients/transports#in-memory-transport), which avoids a network round trip and is the fastest option for tests and embedded use.
+
+```python
+import asyncio
+import random
+from fastmcp import FastMCP
+from pydantic_ai import Agent
+from pydantic_ai.toolsets.fastmcp import FastMCPToolset
+
+mcp = FastMCP(name="Dice Roller")
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+toolset = FastMCPToolset(mcp)
+agent = Agent("openai:gpt-4.1", toolsets=[toolset])
+
+async def main():
+ result = await agent.run("Roll 3 dice!")
+ print(result.output)
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+## Streamable HTTP
+
+For a remote FastMCP server reachable over HTTP, pass the URL as a string. The toolset infers the [Streamable HTTP transport](/clients/transports#http-transport) from the URL.
+
+```python
+from pydantic_ai import Agent
+from pydantic_ai.toolsets.fastmcp import FastMCPToolset
+
+toolset = FastMCPToolset("https://your-server-url.com/mcp")
+agent = Agent("openai:gpt-4.1", toolsets=[toolset])
+```
+
+For [SSE](/clients/transports#sse-transport), use a `/sse` URL instead.
+
+## STDIO
+
+To launch a FastMCP server as a subprocess, pass a script path and the toolset will use the [STDIO transport](/clients/transports#stdio-transport).
+
+```python
+from pydantic_ai import Agent
+from pydantic_ai.toolsets.fastmcp import FastMCPToolset
+
+toolset = FastMCPToolset("server.py")
+agent = Agent("openai:gpt-4.1", toolsets=[toolset])
+```
+
+You can also pass a [`StdioTransport`](/clients/transports#stdio-transport) directly when you need control over the command, args, or environment.
+
+## MCP Configuration
+
+To wire up multiple servers at once, pass an [MCP configuration](/integrations/mcp-json-configuration) dictionary. The toolset opens one client per server and exposes all of their tools to the agent.
+
+```python
+from pydantic_ai import Agent
+from pydantic_ai.toolsets.fastmcp import FastMCPToolset
+
+mcp_config = {
+ "mcpServers": {
+ "dice": {"command": "python", "args": ["server.py"]},
+ "weather": {"url": "https://weather.example.com/mcp"},
+ }
+}
+
+toolset = FastMCPToolset(mcp_config)
+agent = Agent("openai:gpt-4.1", toolsets=[toolset])
+```
+
+## Authentication
+
+Because `FastMCPToolset` wraps a [FastMCP `Client`](/clients/client), it inherits the client's full [authentication](/clients/auth/bearer) story. To pass credentials such as a bearer token to a remote server, build a `Client` (or `StreamableHttpTransport`) yourself and hand it to the toolset.
+
+```python
+from fastmcp import Client
+from fastmcp.client.transports import StreamableHttpTransport
+from pydantic_ai import Agent
+from pydantic_ai.toolsets.fastmcp import FastMCPToolset
+
+transport = StreamableHttpTransport(
+ url="https://your-server-url.com/mcp",
+ headers={"Authorization": "Bearer your-access-token"},
+)
+
+toolset = FastMCPToolset(Client(transport))
+agent = Agent("openai:gpt-4.1", toolsets=[toolset])
+```
+
+For OAuth flows, use FastMCP's [`OAuth` helper](/clients/auth/oauth) when constructing the `Client`. For server-side token verification, see [Token Verification](/servers/auth/token-verification).
diff --git a/docs/v3/integrations/scalekit.mdx b/docs/v3/integrations/scalekit.mdx
new file mode 100644
index 000000000..191b81ca2
--- /dev/null
+++ b/docs/v3/integrations/scalekit.mdx
@@ -0,0 +1,155 @@
+---
+title: Scalekit 🤝 FastMCP
+sidebarTitle: Scalekit
+description: Secure your FastMCP server with Scalekit
+icon: shield-check
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Install auth stack to your FastMCP server with [Scalekit](https://scalekit.com) using the [Remote OAuth](/servers/auth/remote-oauth) pattern: Scalekit handles user authentication, and the MCP server validates issued tokens.
+
+### Prerequisites
+
+Before you begin
+
+1. Get a [Scalekit account](https://app.scalekit.com/) and grab your **Environment URL** from _Dashboard > Settings_ .
+2. Have your FastMCP server's base URL ready (can be localhost for development, e.g., `http://localhost:8000/`)
+
+### Step 1: Configure MCP server in Scalekit environment
+
+
+
+
+In your Scalekit dashboard:
+ 1. Open the **MCP Servers** section, then select **Create new server**
+ 2. Enter server details: a name, a resource identifier, and the desired MCP client authentication settings
+ 3. Save, then copy the **Resource ID** (for example, res_92015146095)
+
+In your FastMCP project's `.env`:
+
+```sh
+SCALEKIT_ENVIRONMENT_URL=
+SCALEKIT_RESOURCE_ID= # res_926EXAMPLE5878
+BASE_URL=http://localhost:8000/
+# Optional: additional scopes tokens must have
+# SCALEKIT_REQUIRED_SCOPES=read,write
+```
+
+
+
+
+### Step 2: Add auth to FastMCP server
+
+Create your FastMCP server file and use the ScalekitProvider to handle all the OAuth integration automatically:
+
+> **Warning:** The legacy `mcp_url` and `client_id` parameters are deprecated and will be removed in a future release. Use `base_url` instead of `mcp_url` and remove `client_id` from your configuration.
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.scalekit import ScalekitProvider
+
+# Discovers Scalekit endpoints and set up JWT token validation
+auth_provider = ScalekitProvider(
+ environment_url=SCALEKIT_ENVIRONMENT_URL, # Scalekit environment URL
+ resource_id=SCALEKIT_RESOURCE_ID, # Resource server ID
+ base_url=SERVER_URL, # Public MCP endpoint
+ required_scopes=["read"], # Optional scope enforcement
+)
+
+# Create FastMCP server with auth
+mcp = FastMCP(name="My Scalekit Protected Server", auth=auth_provider)
+
+@mcp.tool
+def auth_status() -> dict:
+ """Show Scalekit authentication status."""
+ # Extract user claims from the JWT
+ return {
+ "message": "This tool requires authentication via Scalekit",
+ "authenticated": True,
+ "provider": "Scalekit"
+ }
+
+```
+
+
+Set `required_scopes` when you need tokens to carry specific permissions. Leave it unset to allow any token issued for the resource.
+
+
+## Testing
+
+### Start the MCP server
+
+```sh
+uv run python server.py
+```
+
+Use any MCP client (for example, mcp-inspector, Claude, VS Code, or Windsurf) to connect to the running serve. Verify that authentication succeeds and requests are authorized as expected.
+
+## Production Configuration
+
+For production deployments, load configuration from environment variables:
+
+```python server.py
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.scalekit import ScalekitProvider
+
+# Load configuration from environment variables
+auth = ScalekitProvider(
+ environment_url=os.environ.get("SCALEKIT_ENVIRONMENT_URL"),
+ resource_id=os.environ.get("SCALEKIT_RESOURCE_ID"),
+ base_url=os.environ.get("BASE_URL", "https://your-server.com")
+)
+
+mcp = FastMCP(name="My Scalekit Protected Server", auth=auth)
+
+@mcp.tool
+def protected_action() -> str:
+ """A tool that requires authentication."""
+ return "Access granted via Scalekit!"
+```
+
+## Capabilities
+
+Scalekit supports OAuth 2.1 with Dynamic Client Registration for MCP clients and enterprise SSO, and provides built‑in JWT validation and security controls.
+
+**OAuth 2.1/DCR**: clients self‑register, use PKCE, and work with the Remote OAuth pattern without pre‑provisioned credentials.
+
+**Validation and SSO**: tokens are verified (keys, RS256, issuer, audience, expiry), and SAML, OIDC, OAuth 2.0, ADFS, Azure AD, and Google Workspace are supported; use HTTPS in production and review auth logs as needed.
+
+## Debugging
+
+Enable detailed logging to troubleshoot authentication issues:
+
+```python
+import logging
+logging.basicConfig(level=logging.DEBUG)
+```
+
+### Token inspection
+
+You can inspect JWT tokens in your tools to understand the user context:
+
+```python
+from fastmcp.server.context import request_ctx
+import jwt
+
+@mcp.tool
+def inspect_token() -> dict:
+ """Inspect the current JWT token claims."""
+ context = request_ctx.get()
+
+ # Extract token from Authorization header
+ if hasattr(context, 'request') and hasattr(context.request, 'headers'):
+ auth_header = context.request.headers.get('authorization', '')
+ if auth_header.startswith('Bearer '):
+ token = auth_header[7:]
+ # Decode without verification (already verified by provider)
+ claims = jwt.decode(token, options={"verify_signature": False})
+ return claims
+
+ return {"error": "No token found"}
+```
diff --git a/docs/v3/integrations/supabase.mdx b/docs/v3/integrations/supabase.mdx
new file mode 100644
index 000000000..9ffda444d
--- /dev/null
+++ b/docs/v3/integrations/supabase.mdx
@@ -0,0 +1,123 @@
+---
+title: Supabase 🤝 FastMCP
+sidebarTitle: Supabase
+description: Secure your FastMCP server with Supabase Auth
+icon: shield-check
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+This guide shows you how to secure your FastMCP server using **Supabase Auth**. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern, where Supabase handles user authentication and your FastMCP server validates the tokens.
+
+
+Supabase Auth does not currently support [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html) resource indicators, so FastMCP cannot validate that tokens were issued for the specific resource server.
+
+
+## Consent UI Requirement
+
+Supabase's OAuth Server delegates the user consent screen to your application. When an MCP client initiates authorization, Supabase authenticates the user and then redirects to your application at a configured callback URL (e.g., `https://your-app.com/oauth/callback?authorization_id=...`). Your application must host a page that calls Supabase's `approveAuthorization()` or `denyAuthorization()` APIs to complete the flow.
+
+`SupabaseProvider` handles the resource server side (token verification and metadata), but you are responsible for building and hosting the consent UI separately. See [Supabase's OAuth Server documentation](https://supabase.com/docs/guides/auth/oauth-server/getting-started) for details on implementing the authorization page.
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+1. A **[Supabase Account](https://supabase.com/)** with a project or a self-hosted **Supabase Auth** instance
+2. **OAuth Server enabled** in your Supabase Dashboard (Authentication → OAuth Server)
+3. **Dynamic Client Registration enabled** in the same settings
+4. A **consent UI** hosted at your configured authorization path (see above)
+5. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
+
+### Step 1: Enable Supabase OAuth Server
+
+In your Supabase Dashboard:
+1. Go to **Authentication → OAuth Server**
+2. Enable the **OAuth Server**
+3. Set your **Site URL** to where your consent UI is hosted
+4. Set the **Authorization Path** (e.g., `/oauth/callback`)
+5. Enable **Allow Dynamic OAuth Apps** for MCP client registration
+
+### Step 2: Get Supabase Project URL
+
+In your Supabase Dashboard:
+1. Go to **Project Settings**
+2. Copy your **Project URL** (e.g., `https://abc123.supabase.co`)
+
+### Step 3: FastMCP Configuration
+
+Create your FastMCP server using the `SupabaseProvider`:
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.supabase import SupabaseProvider
+
+auth = SupabaseProvider(
+ project_url="https://abc123.supabase.co",
+ base_url="http://localhost:8000",
+)
+
+mcp = FastMCP("Supabase Protected Server", auth=auth)
+
+@mcp.tool
+def protected_tool(message: str) -> str:
+ """This tool requires authentication."""
+ return f"Authenticated user says: {message}"
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
+```
+
+## Testing
+
+### Running the Server
+
+Start your FastMCP server with HTTP transport to enable OAuth flows:
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+### Testing with a Client
+
+Create a test client that authenticates with your Supabase-protected server:
+
+```python client.py
+from fastmcp import Client
+import asyncio
+
+async def main():
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ print("Authenticated with Supabase!")
+
+ result = await client.call_tool("protected_tool", {"message": "Hello!"})
+ print(result)
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+When you run the client for the first time:
+1. Your browser will open to Supabase's authorization endpoint
+2. After authenticating, Supabase redirects to your consent UI
+3. After you approve, the client receives the token and can make authenticated requests
+
+## Production Configuration
+
+For production deployments, load configuration from environment variables:
+
+```python server.py
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.supabase import SupabaseProvider
+
+auth = SupabaseProvider(
+ project_url=os.environ["SUPABASE_PROJECT_URL"],
+ base_url=os.environ.get("BASE_URL", "https://your-server.com"),
+)
+
+mcp = FastMCP(name="Supabase Secured App", auth=auth)
+```
diff --git a/docs/v3/integrations/workos.mdx b/docs/v3/integrations/workos.mdx
new file mode 100644
index 000000000..4f13a5512
--- /dev/null
+++ b/docs/v3/integrations/workos.mdx
@@ -0,0 +1,200 @@
+---
+title: WorkOS 🤝 FastMCP
+sidebarTitle: WorkOS
+description: Authenticate FastMCP servers with WorkOS Connect
+icon: shield-check
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Secure your FastMCP server with WorkOS Connect authentication. This integration uses the OAuth Proxy pattern to handle authentication through WorkOS Connect while maintaining compatibility with MCP clients.
+
+
+This guide covers WorkOS Connect applications. For Dynamic Client Registration (DCR) with AuthKit, see the [AuthKit integration](/integrations/authkit) instead.
+
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+1. A **[WorkOS Account](https://workos.com/)** with access to create OAuth Apps
+2. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`)
+
+### Step 1: Create a WorkOS OAuth App
+
+Create an OAuth App in your WorkOS dashboard to get the credentials needed for authentication:
+
+
+
+In your WorkOS dashboard:
+1. Navigate to **Applications**
+2. Click **Create Application**
+3. Select **OAuth Application**
+4. Name your application
+
+
+
+In your OAuth application settings:
+1. Copy your **Client ID** (starts with `client_`)
+2. Click **Generate Client Secret** and save it securely
+3. Copy your **AuthKit Domain** (e.g., `https://your-app.authkit.app`)
+
+
+
+In the **Redirect URIs** section:
+- Add: `http://localhost:8000/auth/callback` (for development)
+- For production, add your server's public URL + `/auth/callback`
+
+
+The callback URL must match exactly. The default path is `/auth/callback`, but you can customize it using the `redirect_path` parameter.
+
+
+
+
+### Step 2: FastMCP Configuration
+
+Create your FastMCP server using the `WorkOSProvider`:
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.workos import WorkOSProvider
+
+# Configure WorkOS OAuth
+auth = WorkOSProvider(
+ client_id="client_YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ authkit_domain="https://your-app.authkit.app",
+ base_url="http://localhost:8000",
+ required_scopes=["openid", "profile", "email"]
+)
+
+mcp = FastMCP("WorkOS Protected Server", auth=auth)
+
+@mcp.tool
+def protected_tool(message: str) -> str:
+ """This tool requires authentication."""
+ return f"Authenticated user says: {message}"
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
+```
+
+## Testing
+
+### Running the Server
+
+Start your FastMCP server with HTTP transport to enable OAuth flows:
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+Your server is now running and protected by WorkOS OAuth authentication.
+
+### Testing with a Client
+
+Create a test client that authenticates with your WorkOS-protected server:
+
+```python client.py
+from fastmcp import Client
+import asyncio
+
+async def main():
+ # The client will automatically handle WorkOS OAuth
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ # First-time connection will open WorkOS login in your browser
+ print("✓ Authenticated with WorkOS!")
+
+ # Test the protected tool
+ result = await client.call_tool("protected_tool", {"message": "Hello!"})
+ print(result)
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+When you run the client for the first time:
+1. Your browser will open to WorkOS's authorization page
+2. After you authorize the app, you'll be redirected back
+3. The client receives the token and can make authenticated requests
+
+
+The client caches tokens locally, so you won't need to re-authenticate for subsequent runs unless the token expires or you explicitly clear the cache.
+
+
+## Production Configuration
+
+
+
+For production deployments with persistent token management across server restarts, configure `jwt_signing_key`, and `client_storage`:
+
+```python server.py
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.workos import WorkOSProvider
+from key_value.aio.stores.redis import RedisStore
+from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
+from cryptography.fernet import Fernet
+
+# Production setup with encrypted persistent token storage
+auth = WorkOSProvider(
+ client_id="client_YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ authkit_domain="https://your-app.authkit.app",
+ base_url="https://your-production-domain.com",
+ required_scopes=["openid", "profile", "email"],
+
+ # Production token management
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ client_storage=FernetEncryptionWrapper(
+ key_value=RedisStore(
+ host=os.environ["REDIS_HOST"],
+ port=int(os.environ["REDIS_PORT"])
+ ),
+ fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
+ )
+)
+
+mcp = FastMCP(name="Production WorkOS App", auth=auth)
+```
+
+
+Parameters (`jwt_signing_key` and `client_storage`) work together to ensure tokens and client registrations survive server restarts. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. Store secrets in environment variables and use a persistent storage backend like Redis for distributed deployments.
+
+For complete details on these parameters, see the [OAuth Proxy documentation](/servers/auth/oauth-proxy#configuration-parameters).
+
+
+## Configuration Options
+
+
+
+WorkOS OAuth application client ID
+
+
+
+WorkOS OAuth application client secret
+
+
+
+Your WorkOS AuthKit domain URL (e.g., `https://your-app.authkit.app`)
+
+
+
+Your FastMCP server's public URL
+
+
+
+OAuth scopes to request
+
+
+
+OAuth callback path
+
+
+
+API request timeout
+
+
\ No newline at end of file
diff --git a/docs/v3/more/faq.mdx b/docs/v3/more/faq.mdx
new file mode 100644
index 000000000..d2bbbe05e
--- /dev/null
+++ b/docs/v3/more/faq.mdx
@@ -0,0 +1,25 @@
+---
+title: FAQ
+description: Answers to common questions about installing and using FastMCP
+icon: circle-question
+---
+
+## `import fastmcp` stopped working after I upgraded with pip
+
+This can happen when you upgrade to FastMCP 3.3 or later from FastMCP 3.2 or earlier with `pip`. The quick fix is `pip install --force-reinstall fastmcp`. See [Troubleshooting](/getting-started/installation#troubleshooting) for the clean-reinstall fallback and an explanation of why it happens.
+
+## What's the difference between `fastmcp` and `fastmcp-slim`?
+
+`fastmcp` is the full distribution. Installing it gives you the complete framework — server, client, CLI, and the common integrations — and is the right choice for most users:
+
+```bash
+pip install fastmcp
+```
+
+`fastmcp-slim` ships the same importable `fastmcp` package with a minimal set of required dependencies. You opt into the pieces you need through extras, which keeps environments lean when you only use part of the framework:
+
+```bash
+pip install "fastmcp-slim[client]"
+```
+
+Both distributions expose the same `import fastmcp`, so application code is identical regardless of which one you install.
diff --git a/docs/v3/more/settings.mdx b/docs/v3/more/settings.mdx
new file mode 100644
index 000000000..af6b862fe
--- /dev/null
+++ b/docs/v3/more/settings.mdx
@@ -0,0 +1,99 @@
+---
+title: Settings
+description: Configure FastMCP behavior through environment variables or a .env file.
+icon: gear
+---
+
+FastMCP uses [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/) for configuration. Every setting is available as an environment variable with a `FASTMCP_` prefix. Settings are loaded from environment variables and from a `.env` file (see the [Tasks (Docket)](#tasks-docket) section for a caveat about nested settings in `.env` files).
+
+```bash
+# Set via environment
+export FASTMCP_LOG_LEVEL=DEBUG
+export FASTMCP_PORT=3000
+
+# Or use a .env file (loaded automatically)
+echo "FASTMCP_LOG_LEVEL=DEBUG" >> .env
+```
+
+You can change which `.env` file is loaded by setting the `FASTMCP_ENV_FILE` environment variable (defaults to `.env`). Because this controls which file is loaded, it must be set as an environment variable — it cannot be set inside a `.env` file itself.
+
+## Logging
+
+| Environment Variable | Type | Default | Description |
+|---|---|---|---|
+| `FASTMCP_LOG_LEVEL` | `Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]` | `INFO` | Log level for FastMCP's own logging output. Case-insensitive. |
+| `FASTMCP_LOG_ENABLED` | `bool` | `true` | Enable or disable FastMCP logging entirely. |
+| `FASTMCP_CLIENT_LOG_LEVEL` | `Literal["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"]` | None | Default minimum log level for messages sent to MCP clients via `context.log()`. When set, messages below this level are suppressed. Individual clients can override this per-session using the MCP `logging/setLevel` request. |
+| `FASTMCP_ENABLE_RICH_LOGGING` | `bool` | `true` | Use rich formatting for log output. Set to `false` for plain Python logging. |
+| `FASTMCP_ENABLE_RICH_TRACEBACKS` | `bool` | `true` | Use rich tracebacks for errors. |
+| `FASTMCP_DEPRECATION_WARNINGS` | `bool` | `true` | Show deprecation warnings. |
+
+## Transport & HTTP
+
+These control how the server listens when running with an HTTP transport.
+
+| Environment Variable | Type | Default | Description |
+|---|---|---|---|
+| `FASTMCP_TRANSPORT` | `Literal["stdio", "http", "sse", "streamable-http"]` | `stdio` | Default transport. |
+| `FASTMCP_HOST` | `str` | `127.0.0.1` | Host to bind to. |
+| `FASTMCP_PORT` | `int` | `8000` | Port to bind to. |
+| `FASTMCP_SSE_PATH` | `str` | `/sse` | Path for SSE endpoint. |
+| `FASTMCP_MESSAGE_PATH` | `str` | `/messages/` | Path for SSE message endpoint. |
+| `FASTMCP_STREAMABLE_HTTP_PATH` | `str` | `/mcp` | Path for Streamable HTTP endpoint. |
+| `FASTMCP_STATELESS_HTTP` | `bool` | `false` | Enable stateless HTTP mode (new transport per request). Useful for multi-worker deployments. |
+| `FASTMCP_JSON_RESPONSE` | `bool` | `false` | Use JSON responses instead of SSE for Streamable HTTP. |
+| `FASTMCP_HTTP_HOST_ORIGIN_PROTECTION` | `bool \| "auto"` | `false` | Validate `Host` and browser `Origin` headers for Streamable HTTP requests. `auto` protects localhost-bound servers and explicit host/origin allowlists. |
+| `FASTMCP_HTTP_ALLOWED_HOSTS` | `list[str] \| null` | `null` | Additional trusted hostnames when Host and Origin protection is enabled. Use a JSON array, such as `["mcp.example.com"]`. |
+| `FASTMCP_HTTP_ALLOWED_ORIGINS` | `list[str] \| null` | `null` | Browser origins trusted when Host and Origin protection is enabled. Configure CORS separately for cross-origin browser reads. Use a JSON array, such as `["https://app.example.com"]`. |
+| `FASTMCP_DEBUG` | `bool` | `false` | Enable debug mode. |
+
+## Error Handling
+
+| Environment Variable | Type | Default | Description |
+|---|---|---|---|
+| `FASTMCP_MASK_ERROR_DETAILS` | `bool` | `false` | Mask error details before sending to clients. When enabled, only messages from explicitly raised `ToolError`, `ResourceError`, or `PromptError` are included in responses. |
+| `FASTMCP_STRICT_INPUT_VALIDATION` | `bool` | `false` | Strictly validate tool inputs against the JSON schema. When disabled, compatible inputs are coerced (e.g., the string `"10"` becomes the integer `10`). |
+| `FASTMCP_MOUNTED_COMPONENTS_RAISE_ON_LOAD_ERROR` | `bool` | `false` | Raise errors when loading mounted components instead of logging warnings. |
+
+## Client
+
+| Environment Variable | Type | Default | Description |
+|---|---|---|---|
+| `FASTMCP_CLIENT_INIT_TIMEOUT` | `float \| None` | None | Timeout in seconds for the client initialization handshake. Set to `0` or leave unset to disable. |
+| `FASTMCP_CLIENT_DISCONNECT_TIMEOUT` | `float` | `5` | Maximum time in seconds to wait for a clean disconnect before giving up. |
+| `FASTMCP_CLIENT_RAISE_FIRST_EXCEPTIONGROUP_ERROR` | `bool` | `true` | When an `ExceptionGroup` is raised, re-raise the first error directly instead of the group. Simplifies debugging but may mask secondary errors. |
+
+## CLI & Display
+
+| Environment Variable | Type | Default | Description |
+|---|---|---|---|
+| `FASTMCP_SHOW_SERVER_BANNER` | `bool` | `true` | Show the server banner on startup. Also controllable via `--no-banner` or `server.run(show_banner=False)`. |
+| `FASTMCP_CHECK_FOR_UPDATES` | `Literal["stable", "prerelease", "off"]` | `stable` | Update checking on CLI startup. `stable` checks stable releases only, `prerelease` includes pre-releases, `off` disables checking. |
+
+## Tasks (Docket)
+
+These configure the [Docket](https://github.com/prefecthq/docket) task queue used by [server tasks](/servers/tasks). All use the `FASTMCP_DOCKET_` prefix.
+
+
+When setting Docket values in a `.env` file, use a **double** underscore: `FASTMCP_DOCKET__URL` (not `FASTMCP_DOCKET_URL`). This is because `.env` values are resolved through the parent `Settings` class, which uses `__` as its nested delimiter. As regular environment variables (e.g., `export`), the single-underscore form `FASTMCP_DOCKET_URL` works fine.
+
+
+| Environment Variable | Type | Default | Description |
+|---|---|---|---|
+| `FASTMCP_DOCKET_NAME` | `str` | `fastmcp` | Queue name. Servers and workers sharing the same name and backend URL share a task queue. |
+| `FASTMCP_DOCKET_URL` | `str` | `memory://` | Backend URL. Use `memory://` for single-process or `redis://host:port/db` for distributed workers. |
+| `FASTMCP_DOCKET_WORKER_NAME` | `str \| None` | None | Worker name. Auto-generated if unset. |
+| `FASTMCP_DOCKET_CONCURRENCY` | `int` | `10` | Maximum concurrent tasks per worker. |
+| `FASTMCP_DOCKET_REDELIVERY_TIMEOUT` | `timedelta` | `300s` | If a worker doesn't complete a task within this time, it's redelivered to another worker. |
+| `FASTMCP_DOCKET_RECONNECTION_DELAY` | `timedelta` | `5s` | Delay between reconnection attempts when the worker loses its backend connection. |
+| `FASTMCP_DOCKET_MINIMUM_CHECK_INTERVAL` | `timedelta` | `50ms` | How frequently the worker polls for new tasks. Lower values reduce latency at the cost of more CPU usage. |
+
+## Advanced
+
+| Environment Variable | Type | Default | Description |
+|---|---|---|---|
+| `FASTMCP_HOME` | `Path` | Platform default | Data directory for FastMCP. Defaults to the platform-specific user data directory. |
+| `FASTMCP_ENV_FILE` | `str` | `.env` | Path to the `.env` file to load settings from. Must be set as an environment variable (see above). |
+| `FASTMCP_SERVER_DEPENDENCIES` | `list[str]` | `[]` | Additional dependencies to install in the server environment. |
+| `FASTMCP_DECORATOR_MODE` | `Literal["function", "object"]` | `function` | Controls what `@tool`, `@resource`, and `@prompt` decorators return. `function` returns the original function (default); `object` returns component objects (deprecated, will be removed). |
+| `FASTMCP_TEST_MODE` | `bool` | `false` | Enable test mode. |
diff --git a/docs/patterns/cli.mdx b/docs/v3/patterns/cli.mdx
similarity index 100%
rename from docs/patterns/cli.mdx
rename to docs/v3/patterns/cli.mdx
diff --git a/docs/v3/patterns/contrib.mdx b/docs/v3/patterns/contrib.mdx
new file mode 100644
index 000000000..04ef45aff
--- /dev/null
+++ b/docs/v3/patterns/contrib.mdx
@@ -0,0 +1,45 @@
+---
+title: "Contrib Modules"
+description: "Community-contributed modules extending FastMCP"
+icon: "cubes"
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+FastMCP includes a `contrib` package that holds community-contributed modules. These modules extend FastMCP's functionality but aren't officially maintained by the core team.
+
+Contrib modules provide additional features, integrations, or patterns that complement the core FastMCP library. They offer a way for the community to share useful extensions while keeping the core library focused and maintainable.
+
+The available modules can be viewed in the [contrib directory](https://github.com/PrefectHQ/fastmcp/tree/main/fastmcp_slim/fastmcp/contrib).
+
+## Usage
+
+To use a contrib module, import it from the `fastmcp.contrib` package:
+
+```python test="skip"
+from fastmcp.contrib import my_module
+```
+
+## Important Considerations
+
+- **Stability**: Modules in `contrib` may have different testing requirements or stability guarantees compared to the core library.
+- **Compatibility**: Changes to core FastMCP might break modules in `contrib` without explicit warnings in the main changelog.
+- **Dependencies**: Contrib modules may have additional dependencies not required by the core library. These dependencies are typically documented in the module's README or separate requirements files.
+
+## Contributing
+
+We welcome contributions to the `contrib` package! If you have a module that extends FastMCP in a useful way, consider contributing it:
+
+1. Create a new directory in `fastmcp_slim/fastmcp/contrib/` for your module
+3. Add proper tests for your module in `tests/contrib/`
+2. Include comprehensive documentation in a README.md file, including usage and examples, as well as any additional dependencies or installation instructions
+5. Submit a pull request
+
+The ideal contrib module:
+- Solves a specific use case or integration need
+- Follows FastMCP coding standards
+- Includes thorough documentation and examples
+- Has comprehensive tests
+- Specifies any additional dependencies
diff --git a/docs/patterns/testing.mdx b/docs/v3/patterns/testing.mdx
similarity index 100%
rename from docs/patterns/testing.mdx
rename to docs/v3/patterns/testing.mdx
diff --git a/docs/v3/servers/auth/authentication.mdx b/docs/v3/servers/auth/authentication.mdx
new file mode 100644
index 000000000..d37c57f36
--- /dev/null
+++ b/docs/v3/servers/auth/authentication.mdx
@@ -0,0 +1,252 @@
+---
+title: Authentication
+sidebarTitle: Overview
+description: Secure your FastMCP server with flexible authentication patterns, from simple API keys to full OAuth 2.1 integration with external identity providers.
+icon: user-shield
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Authentication in MCP presents unique challenges that differ from traditional web applications. MCP clients need to discover authentication requirements automatically, negotiate OAuth flows without user intervention, and work seamlessly across different identity providers. FastMCP addresses these challenges by providing authentication patterns that integrate with the MCP protocol while remaining simple to implement and deploy.
+
+
+Authentication applies only to FastMCP's HTTP-based transports (`http` and `sse`). The STDIO transport inherits security from its local execution environment.
+
+
+
+**Authentication is rapidly evolving in MCP.** The specification and best practices are changing quickly. FastMCP aims to provide stable, secure patterns that adapt to these changes while keeping your code simple and maintainable.
+
+
+## MCP Authentication Challenges
+
+Traditional web authentication assumes a human user with a browser who can interact with login forms and consent screens. MCP clients are often automated systems that need to authenticate without human intervention. This creates several unique requirements:
+
+**Automatic Discovery**: MCP clients must discover authentication requirements by examining server metadata rather than encountering login redirects.
+
+**Programmatic OAuth**: OAuth flows must work without human interaction, relying on pre-configured credentials or Dynamic Client Registration.
+
+**Token Management**: Clients need to obtain, refresh, and manage tokens automatically across multiple MCP servers.
+
+**Protocol Integration**: Authentication must integrate cleanly with MCP's transport mechanisms and error handling.
+
+These challenges mean that not all authentication approaches work well with MCP. The patterns that do work fall into three categories based on the level of authentication responsibility your server assumes.
+
+## Authentication Responsibility
+
+Authentication responsibility exists on a spectrum. Your MCP server can validate tokens created elsewhere, coordinate with external identity providers, or handle the complete authentication lifecycle internally. Each approach involves different trade-offs between simplicity, security, and control.
+
+### Token Validation
+
+Your server validates tokens but delegates their creation to external systems. This approach treats your MCP server as a pure resource server that trusts tokens signed by known issuers.
+
+Token validation works well when you already have authentication infrastructure that can issue structured tokens like JWTs. Your existing API gateway, microservices platform, or enterprise SSO system becomes the source of truth for user identity, while your MCP server focuses on its core functionality.
+
+The key insight is that token validation separates authentication (proving who you are) from authorization (determining what you can do). Your MCP server receives proof of identity in the form of a signed token and makes access decisions based on the claims within that token.
+
+This pattern excels in microservices architectures where multiple services need to validate the same tokens, or when integrating MCP servers into existing systems that already handle user authentication.
+
+### External Identity Providers
+
+Your server coordinates with established identity providers to create seamless authentication experiences for MCP clients. This approach leverages OAuth 2.0 and OpenID Connect protocols to delegate user authentication while maintaining control over authorization decisions.
+
+External identity providers handle the complex aspects of authentication: user credential verification, multi-factor authentication, account recovery, and security monitoring. Your MCP server receives tokens from these trusted providers and validates them using the provider's public keys.
+
+The MCP protocol's support for Dynamic Client Registration makes this pattern particularly powerful. MCP clients can automatically discover your authentication requirements and register themselves with your identity provider without manual configuration.
+
+This approach works best for production applications that need enterprise-grade authentication features without the complexity of building them from scratch. It scales well across multiple applications and provides consistent user experiences.
+
+### Full OAuth Implementation
+
+Your server implements a complete OAuth 2.0 authorization server, handling everything from user credential verification to token lifecycle management. This approach provides maximum control at the cost of significant complexity.
+
+Full OAuth implementation means building user interfaces for login and consent, implementing secure credential storage, managing token lifecycles, and maintaining ongoing security updates. The complexity extends beyond initial implementation to include threat monitoring, compliance requirements, and keeping pace with evolving security best practices.
+
+This pattern makes sense only when you need complete control over the authentication process, operate in air-gapped environments, or have specialized requirements that external providers cannot meet.
+
+## FastMCP Authentication Providers
+
+FastMCP translates these authentication responsibility levels into a variety of concrete classes that handle the complexities of MCP protocol integration. You can build on these classes to handle the complexities of MCP protocol integration.
+
+### TokenVerifier
+
+`TokenVerifier` provides pure token validation without OAuth metadata endpoints. This class focuses on the essential task of determining whether a token is valid and extracting authorization information from its claims.
+
+The implementation handles JWT signature verification, expiration checking, and claim extraction. It validates tokens against known issuers and audiences, ensuring that tokens intended for your server are not accepted by other systems.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+auth = JWTVerifier(
+ jwks_uri="https://your-auth-system.com/.well-known/jwks.json",
+ issuer="https://your-auth-system.com",
+ audience="your-mcp-server"
+)
+
+mcp = FastMCP(name="Protected Server", auth=auth)
+```
+
+This example configures token validation against a JWT issuer. The `JWTVerifier` will fetch public keys from the JWKS endpoint and validate incoming tokens against those keys. Only tokens with the correct issuer and audience claims will be accepted.
+
+`TokenVerifier` works well when you control both the token issuer and your MCP server, or when integrating with existing JWT-based infrastructure.
+
+→ **Complete guide**: [Token Verification](/servers/auth/token-verification)
+
+### RemoteAuthProvider
+
+`RemoteAuthProvider` enables authentication with identity providers that **support Dynamic Client Registration (DCR)**, such as Descope and WorkOS AuthKit. With DCR, MCP clients can automatically register themselves with the identity provider and obtain credentials without any manual configuration.
+
+This class combines token validation with OAuth discovery metadata. It extends `TokenVerifier` functionality by adding OAuth 2.0 protected resource endpoints that advertise your authentication requirements. MCP clients examine these endpoints to understand which identity providers you trust and how to obtain valid tokens.
+
+The key requirement is that your identity provider must support DCR - the ability for clients to dynamically register and obtain credentials. This is what enables the seamless, automated authentication flow that MCP requires.
+
+For example, the built-in `AuthKitProvider` uses WorkOS AuthKit, which fully supports DCR:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.workos import AuthKitProvider
+
+auth = AuthKitProvider(
+ authkit_domain="https://your-project.authkit.app",
+ base_url="https://your-fastmcp-server.com"
+)
+
+mcp = FastMCP(name="Enterprise Server", auth=auth)
+```
+
+This example uses WorkOS AuthKit as the external identity provider. The `AuthKitProvider` automatically configures token validation against WorkOS and provides the OAuth metadata that MCP clients need for automatic authentication.
+
+`RemoteAuthProvider` is ideal for production applications when your identity provider supports Dynamic Client Registration (DCR). This enables fully automated authentication without manual client configuration.
+
+→ **Complete guide**: [Remote OAuth](/servers/auth/remote-oauth)
+
+### OAuthProxy
+
+
+
+`OAuthProxy` enables authentication with OAuth providers that **don't support Dynamic Client Registration (DCR)**, such as GitHub, Google, Azure, AWS, and most traditional enterprise identity systems.
+
+When identity providers require manual app registration and fixed credentials, `OAuthProxy` bridges the gap. It presents a DCR-compliant interface to MCP clients (accepting any registration request) while using your pre-registered credentials with the upstream provider. The proxy handles the complexity of callback forwarding, enabling dynamic client callbacks to work with providers that require fixed redirect URIs.
+
+This class solves the fundamental incompatibility between MCP's expectation of dynamic registration and traditional OAuth providers' requirement for manual app registration.
+
+For example, the built-in `GitHubProvider` extends `OAuthProxy` to work with GitHub's OAuth system:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.github import GitHubProvider
+
+auth = GitHubProvider(
+ client_id="Ov23li...", # Your GitHub OAuth App ID
+ client_secret="abc123...", # Your GitHub OAuth App Secret
+ base_url="https://your-server.com"
+)
+
+mcp = FastMCP(name="GitHub-Protected Server", auth=auth)
+```
+
+This example uses the GitHub provider, which extends `OAuthProxy` with GitHub-specific token validation. The proxy handles the complete OAuth flow while making GitHub's non-DCR authentication work seamlessly with MCP clients.
+
+`OAuthProxy` is essential when integrating with OAuth providers that don't support DCR. This includes most established providers like GitHub, Google, and Azure, which require manual app registration through their developer consoles.
+
+→ **Complete guide**: [OAuth Proxy](/servers/auth/oauth-proxy)
+
+### OAuthProvider
+
+`OAuthProvider` implements a complete OAuth 2.0 authorization server within your MCP server. This class handles the full authentication lifecycle from user credential verification to token management.
+
+The implementation provides all required OAuth endpoints including authorization, token, and discovery endpoints. It manages client registration, user consent, and token lifecycle while integrating with your user storage and authentication logic.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import OAuthProvider
+
+auth = MyOAuthProvider(
+ user_store=your_user_database,
+ client_store=your_client_registry,
+ # Additional configuration...
+)
+
+mcp = FastMCP(name="Auth Server", auth=auth)
+```
+
+This example shows the basic structure of a custom OAuth provider. The actual implementation requires significant additional configuration for user management, client registration, and security policies.
+
+`OAuthProvider` should be used only when you have specific requirements that external providers cannot meet and the expertise to implement OAuth securely.
+
+→ **Complete guide**: [Full OAuth Server](/servers/auth/full-oauth-server)
+
+### MultiAuth
+
+
+
+`MultiAuth` composes multiple authentication sources into a single `auth` provider. When a server needs to accept tokens from different issuers — for example, an OAuth proxy for interactive clients alongside JWT verification for machine-to-machine tokens — `MultiAuth` tries each source in order and accepts the first successful verification.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import MultiAuth, OAuthProxy
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+auth = MultiAuth(
+ server=OAuthProxy(
+ issuer_url="https://login.example.com/...",
+ client_id="my-app",
+ client_secret="secret",
+ base_url="https://my-server.com",
+ ),
+ verifiers=[
+ JWTVerifier(
+ jwks_uri="https://internal-issuer.example.com/.well-known/jwks.json",
+ issuer="https://internal-issuer.example.com",
+ audience="my-mcp-server",
+ ),
+ ],
+)
+
+mcp = FastMCP("My Server", auth=auth)
+```
+
+The server (if provided) owns all OAuth routes and metadata. Verifiers contribute only token verification logic. This keeps the MCP discovery surface clean while supporting multiple token sources.
+
+→ **Complete guide**: [Multiple Auth Sources](/servers/auth/multi-auth)
+
+## Configuration
+
+Authentication providers are configured programmatically by instantiating them directly in your code with their required parameters. This makes dependencies explicit and allows your IDE to provide helpful autocompletion and type checking.
+
+For production deployments, load sensitive values like client secrets from environment variables:
+
+```python
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.github import GitHubProvider
+
+# Load secrets from environment variables
+auth = GitHubProvider(
+ client_id=os.environ.get("GITHUB_CLIENT_ID"),
+ client_secret=os.environ.get("GITHUB_CLIENT_SECRET"),
+ base_url=os.environ.get("BASE_URL", "http://localhost:8000")
+)
+
+mcp = FastMCP(name="My Server", auth=auth)
+```
+
+This approach keeps secrets out of your codebase while maintaining explicit configuration. You can use any environment variable names you prefer - there are no special prefixes required.
+
+## Choosing Your Implementation
+
+The authentication approach you choose depends on your existing infrastructure, security requirements, and operational constraints.
+
+**For OAuth providers without DCR support (GitHub, Google, Azure, AWS, most enterprise systems), use OAuth Proxy.** These providers require manual app registration through their developer consoles. OAuth Proxy bridges the gap by presenting a DCR-compliant interface to MCP clients while using your fixed credentials with the provider. The proxy's callback forwarding pattern enables dynamic client ports to work with providers that require fixed redirect URIs.
+
+**For identity providers with DCR support (Descope, WorkOS AuthKit, modern auth platforms), use RemoteAuthProvider.** These providers allow clients to dynamically register and obtain credentials without manual configuration. This enables the fully automated authentication flow that MCP is designed for, providing the best user experience and simplest implementation.
+
+**Token validation works well when you already have authentication infrastructure that issues structured tokens.** If your organization already uses JWT-based systems, API gateways, or enterprise SSO that can generate tokens, this approach integrates seamlessly while keeping your MCP server focused on its core functionality. The simplicity comes from leveraging existing investment in authentication infrastructure.
+
+**When you need tokens from multiple sources, use MultiAuth.** This is common in hybrid architectures where interactive clients authenticate through an OAuth proxy while backend services send JWT tokens directly. `MultiAuth` composes an optional auth server with additional token verifiers, trying each source in order until one succeeds.
+
+**Full OAuth implementation should be avoided unless you have compelling reasons that external providers cannot address.** Air-gapped environments, specialized compliance requirements, or unique organizational constraints might justify this approach, but it requires significant security expertise and ongoing maintenance commitment. The complexity extends far beyond initial implementation to include threat monitoring, security updates, and keeping pace with evolving attack vectors.
+
+FastMCP's architecture supports migration between these approaches as your requirements evolve. You can integrate with existing token systems initially and migrate to external identity providers as your application scales, or implement custom solutions when your requirements outgrow standard patterns.
\ No newline at end of file
diff --git a/docs/v3/servers/auth/full-oauth-server.mdx b/docs/v3/servers/auth/full-oauth-server.mdx
new file mode 100644
index 000000000..529a01784
--- /dev/null
+++ b/docs/v3/servers/auth/full-oauth-server.mdx
@@ -0,0 +1,229 @@
+---
+title: Full OAuth Server
+sidebarTitle: Full OAuth Server
+description: Build a self-contained authentication system where your FastMCP server manages users, issues tokens, and validates them.
+icon: users-between-lines
+
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+
+**This is an extremely advanced pattern that most users should avoid.** Building a secure OAuth 2.1 server requires deep expertise in authentication protocols, cryptography, and security best practices. The complexity extends far beyond initial implementation to include ongoing security monitoring, threat response, and compliance maintenance.
+
+**Use [Remote OAuth](/servers/auth/remote-oauth) instead** unless you have compelling requirements that external identity providers cannot meet, such as air-gapped environments or specialized compliance needs.
+
+
+The Full OAuth Server pattern exists to support the MCP protocol specification's requirements. Your FastMCP server becomes both an Authorization Server and Resource Server, handling the complete authentication lifecycle from user login to token validation.
+
+This documentation exists for completeness - the vast majority of applications should use external identity providers instead.
+
+## OAuthProvider
+
+FastMCP provides the `OAuthProvider` abstract class that implements the OAuth 2.1 specification. To use this pattern, you must subclass `OAuthProvider` and implement all required abstract methods.
+
+
+`OAuthProvider` handles OAuth endpoints, protocol flows, and security requirements, but delegates all storage, user management, and business logic to your implementation of the abstract methods.
+
+
+## Required Implementation
+
+You must implement these abstract methods to create a functioning OAuth server:
+
+### Client Management
+
+
+
+ Retrieve client information by ID from your database.
+
+
+
+ Client identifier to look up
+
+
+
+
+
+ Client information object or `None` if client not found
+
+
+
+
+
+ Store new client registration information in your database.
+
+
+
+ Complete client registration information to store
+
+
+
+
+
+ No return value
+
+
+
+
+
+### Authorization Flow
+
+
+
+ Handle authorization request and return redirect URL. Must implement user authentication and consent collection.
+
+
+
+ OAuth client making the authorization request
+
+
+ Authorization request parameters from the client
+
+
+
+
+
+ Redirect URL to send the client to
+
+
+
+
+
+ Load authorization code from storage by code string. Return `None` if code is invalid or expired.
+
+
+
+ OAuth client attempting to use the authorization code
+
+
+ Authorization code string to look up
+
+
+
+
+
+ Authorization code object or `None` if not found
+
+
+
+
+
+### Token Management
+
+
+
+ Exchange authorization code for access and refresh tokens. Must validate code and create new tokens.
+
+
+
+ OAuth client exchanging the authorization code
+
+
+ Valid authorization code object to exchange
+
+
+
+
+
+ New OAuth token containing access and refresh tokens
+
+
+
+
+
+ Load refresh token from storage by token string. Return `None` if token is invalid or expired.
+
+
+
+ OAuth client attempting to use the refresh token
+
+
+ Refresh token string to look up
+
+
+
+
+
+ Refresh token object or `None` if not found
+
+
+
+
+
+ Exchange refresh token for new access/refresh token pair. Must validate scopes and token.
+
+
+
+ OAuth client using the refresh token
+
+
+ Valid refresh token object to exchange
+
+
+ Requested scopes for the new access token
+
+
+
+
+
+ New OAuth token with updated access and refresh tokens
+
+
+
+
+
+ Load an access token by its token string.
+
+
+
+ The access token to verify
+
+
+
+
+
+ The access token object, or `None` if the token is invalid
+
+
+
+
+
+ Revoke access or refresh token, marking it as invalid in storage.
+
+
+
+ Token object to revoke and mark invalid
+
+
+
+
+
+ No return value
+
+
+
+
+
+ Verify bearer token for incoming requests. Return `AccessToken` if valid, `None` if invalid.
+
+
+
+ Bearer token string from incoming request
+
+
+
+
+
+ Access token object if valid, `None` if invalid or expired
+
+
+
+
+
+Each method must handle storage, validation, security, and error cases according to the OAuth 2.1 specification. The implementation complexity is substantial and requires expertise in OAuth security considerations.
+
+
+**Security Notice:** OAuth server implementation involves numerous security considerations including PKCE, state parameters, redirect URI validation, token binding, replay attack prevention, and secure storage requirements. Mistakes can lead to serious security vulnerabilities.
+
\ No newline at end of file
diff --git a/docs/v3/servers/auth/multi-auth.mdx b/docs/v3/servers/auth/multi-auth.mdx
new file mode 100644
index 000000000..ba54d25ab
--- /dev/null
+++ b/docs/v3/servers/auth/multi-auth.mdx
@@ -0,0 +1,95 @@
+---
+title: Multiple Auth Sources
+sidebarTitle: Multiple Auth Sources
+description: Accept tokens from multiple authentication sources with a single server.
+icon: layer-group
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Production servers often need to accept tokens from multiple authentication sources. An interactive application might authenticate through an OAuth proxy, while a backend service sends machine-to-machine JWT tokens directly. `MultiAuth` composes these sources into a single `auth` provider so every valid token is accepted regardless of where it was issued.
+
+## Understanding MultiAuth
+
+`MultiAuth` wraps an optional auth server (like `OAuthProxy`) together with one or more token verifiers (like `JWTVerifier`). When a request arrives with a bearer token, `MultiAuth` tries each source in order and accepts the first successful verification.
+
+The auth server, if provided, is tried first. It owns all OAuth routes and metadata — the verifiers contribute only token verification logic. This keeps the MCP discovery surface clean: one set of routes, one set of metadata, multiple verification paths.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import MultiAuth, OAuthProxy
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+auth = MultiAuth(
+ server=OAuthProxy(
+ issuer_url="https://login.example.com/...",
+ client_id="my-app",
+ client_secret="secret",
+ base_url="https://my-server.com",
+ ),
+ verifiers=[
+ JWTVerifier(
+ jwks_uri="https://internal-issuer.example.com/.well-known/jwks.json",
+ issuer="https://internal-issuer.example.com",
+ audience="my-mcp-server",
+ ),
+ ],
+)
+
+mcp = FastMCP("My Server", auth=auth)
+```
+
+Interactive MCP clients authenticate through the OAuth proxy as usual. Backend services skip OAuth entirely and send a JWT signed by the internal issuer. Both paths are validated, and the first match wins.
+
+## Verification Order
+
+`MultiAuth` checks sources in a deterministic order:
+
+1. **Server** (if provided) — the full auth provider's `verify_token` runs first
+2. **Verifiers** — each `TokenVerifier` is tried in list order
+
+The first source that returns a valid `AccessToken` wins. If every source returns `None`, the request receives a 401 response.
+
+This ordering means the server acts as the "primary" authentication path, with verifiers as fallbacks for tokens the server doesn't recognize.
+
+## Verifiers Only
+
+You don't always need a full OAuth server. If your server only needs to accept tokens from multiple issuers, pass verifiers without a server:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import MultiAuth
+from fastmcp.server.auth.providers.jwt import JWTVerifier, StaticTokenVerifier
+
+auth = MultiAuth(
+ verifiers=[
+ JWTVerifier(
+ jwks_uri="https://issuer-a.example.com/.well-known/jwks.json",
+ issuer="https://issuer-a.example.com",
+ audience="my-server",
+ ),
+ JWTVerifier(
+ jwks_uri="https://issuer-b.example.com/.well-known/jwks.json",
+ issuer="https://issuer-b.example.com",
+ audience="my-server",
+ ),
+ ],
+)
+
+mcp = FastMCP("Multi-Issuer Server", auth=auth)
+```
+
+Without a server, no OAuth routes or metadata are served. This is appropriate for internal systems where clients already know how to obtain tokens.
+
+## API Reference
+
+### MultiAuth
+
+| Parameter | Type | Description |
+| --- | --- | --- |
+| `server` | `AuthProvider \| None` | Optional auth provider that owns routes and OAuth metadata. Also tried first for token verification. |
+| `verifiers` | `list[TokenVerifier] \| TokenVerifier` | One or more token verifiers tried after the server. |
+| `base_url` | `str \| None` | Override the base URL. Defaults to the server's `base_url`. |
+| `required_scopes` | `list[str] \| None` | Override required scopes. Defaults to the server's scopes. |
diff --git a/docs/v3/servers/auth/oauth-proxy.mdx b/docs/v3/servers/auth/oauth-proxy.mdx
new file mode 100644
index 000000000..79ba7bf09
--- /dev/null
+++ b/docs/v3/servers/auth/oauth-proxy.mdx
@@ -0,0 +1,743 @@
+---
+title: OAuth Proxy
+sidebarTitle: OAuth Proxy
+description: Bridge traditional OAuth providers to work seamlessly with MCP's authentication flow.
+icon: share
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx";
+
+
+
+The OAuth proxy enables FastMCP servers to authenticate with OAuth providers that **don't support Dynamic Client Registration (DCR)**. This includes virtually all traditional OAuth providers: GitHub, Google, Azure, AWS, Discord, Facebook, and most enterprise identity systems. For providers that do support DCR (like Descope and WorkOS AuthKit), use [`RemoteAuthProvider`](/servers/auth/remote-oauth) instead.
+
+MCP clients expect to register automatically and obtain credentials on the fly, but traditional providers require manual app registration through their developer consoles. The OAuth proxy bridges this gap by presenting a DCR-compliant interface to MCP clients while using your pre-registered credentials with the upstream provider. When a client attempts to register, the proxy returns your fixed credentials. When a client initiates authorization, the proxy handles the complexity of callback forwarding—storing the client's dynamic callback URL, using its own fixed callback with the provider, then forwarding back to the client after token exchange.
+
+This approach enables any MCP client (whether using random localhost ports or fixed URLs like Claude.ai) to authenticate with any traditional OAuth provider, all while maintaining full OAuth 2.1 and PKCE security.
+
+
+ For providers that support OIDC discovery (Auth0, Google with OIDC
+ configuration, Azure AD), consider using [`OIDC
+ Proxy`](/servers/auth/oidc-proxy) for automatic configuration. OIDC Proxy
+ extends the OAuth proxy to automatically discover endpoints from the provider's
+ `/.well-known/openid-configuration` URL, simplifying setup.
+
+
+## Implementation
+
+### Provider Setup Requirements
+
+Before using the OAuth proxy, you need to register your application with your OAuth provider:
+
+1. **Register your application** in the provider's developer console (GitHub Settings, Google Cloud Console, Azure Portal, etc.)
+2. **Configure the redirect URI** as your FastMCP server URL plus your chosen callback path:
+ - Default: `https://your-server.com/auth/callback`
+ - Custom: `https://your-server.com/your/custom/path` (if you set `redirect_path`)
+ - Development: `http://localhost:8000/auth/callback`
+3. **Obtain your credentials**: Client ID and Client Secret
+4. **Note the OAuth endpoints**: Authorization URL and Token URL (usually found in the provider's OAuth documentation)
+
+
+ The redirect URI you configure with your provider must exactly match your
+ FastMCP server's URL plus the callback path. If you customize `redirect_path`
+ in the OAuth proxy, update your provider's redirect URI accordingly.
+
+
+### Basic Setup
+
+Here's how to implement the OAuth proxy with any provider:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import OAuthProxy
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+# Configure token verification for your provider
+# See the Token Verification guide for provider-specific setups
+token_verifier = JWTVerifier(
+ jwks_uri="https://your-provider.com/.well-known/jwks.json",
+ issuer="https://your-provider.com",
+ audience="your-app-id"
+)
+
+# Create the OAuth proxy
+auth = OAuthProxy(
+ # Provider's OAuth endpoints (from their documentation)
+ upstream_authorization_endpoint="https://provider.com/oauth/authorize",
+ upstream_token_endpoint="https://provider.com/oauth/token",
+
+ # Your registered app credentials
+ upstream_client_id="your-client-id",
+ upstream_client_secret="your-client-secret",
+
+ # Token validation (see Token Verification guide)
+ token_verifier=token_verifier,
+
+ # Your FastMCP server's public URL
+ base_url="https://your-server.com",
+
+ # Optional: customize the callback path (default is "/auth/callback")
+ # redirect_path="/custom/callback",
+)
+
+mcp = FastMCP(name="My Server", auth=auth)
+```
+
+### Configuration Parameters
+
+
+
+ URL of your OAuth provider's authorization endpoint (e.g., `https://github.com/login/oauth/authorize`)
+
+
+
+ URL of your OAuth provider's token endpoint (e.g.,
+ `https://github.com/login/oauth/access_token`)
+
+
+
+ Client ID from your registered OAuth application
+
+
+
+ Client secret from your registered OAuth application. Optional for PKCE public
+ clients or when using alternative credentials (e.g., managed identity client
+ assertions via a subclass). When omitted, `jwt_signing_key` must be provided
+ explicitly since it cannot be derived from the secret.
+
+
+
+ A [`TokenVerifier`](/servers/auth/token-verification) instance to validate the
+ provider's tokens
+
+
+
+ Public URL where OAuth endpoints will be accessible, **including any mount path** (e.g., `https://your-server.com/api`).
+
+ This URL is used to construct OAuth callback URLs and operational endpoints. When mounting under a path prefix, include that prefix in `base_url`. Use `issuer_url` separately to specify where auth server metadata is located (typically at root level).
+
+
+
+ Optional public base URL for the protected resource metadata and token audience.
+
+ Use this when your OAuth callbacks and operational endpoints need to live under one public URL, but the protected MCP resource should be advertised under another. FastMCP will still append the MCP mount path (for example, `/mcp`) to this base URL.
+
+
+
+ Path for OAuth callbacks. Must match the redirect URI configured in your OAuth
+ application
+
+
+
+ Optional URL of provider's token revocation endpoint
+
+
+
+ Issuer URL for OAuth authorization server metadata (defaults to `base_url`).
+
+ When `issuer_url` has a path component (either explicitly or by defaulting from `base_url`), FastMCP creates path-aware discovery routes per RFC 8414. For example, if `base_url` is `http://localhost:8000/api`, the authorization server metadata will be at `/.well-known/oauth-authorization-server/api`.
+
+ **Default behavior (recommended for most cases):**
+ ```python
+ auth = GitHubProvider(
+ base_url="http://localhost:8000/api", # OAuth endpoints under /api
+ # issuer_url defaults to base_url - path-aware discovery works automatically
+ )
+ ```
+
+ **When to set explicitly:**
+ Set `issuer_url` to root level only if you want multiple MCP servers to share a single discovery endpoint:
+ ```python
+ auth = GitHubProvider(
+ base_url="http://localhost:8000/api",
+ issuer_url="http://localhost:8000" # Shared root-level discovery
+ )
+ ```
+
+ See the [HTTP Deployment guide](/deployment/http#mounting-authenticated-servers) for complete mounting examples.
+
+
+
+ Optional URL to your service documentation
+
+
+
+ Whether to forward PKCE (Proof Key for Code Exchange) to the upstream OAuth
+ provider. When enabled and the client uses PKCE, the proxy generates its own
+ PKCE parameters to send upstream while separately validating the client's
+ PKCE. This ensures end-to-end PKCE security at both layers (client-to-proxy
+ and proxy-to-upstream). - `True` (default): Forward PKCE for providers that
+ support it (Google, Azure, AWS, GitHub, etc.) - `False`: Disable only if upstream
+ provider doesn't support PKCE
+
+
+
+ Whether to forward RFC 8707 `resource` parameters from MCP clients to the
+ upstream OAuth provider. When enabled, the proxy includes the resource indicator
+ in authorization requests, allowing providers that support RFC 8707 to scope
+ tokens to specific resources. Disable for providers that reject unknown
+ parameters.
+
+
+
+ Token endpoint authentication method for the upstream OAuth server. Controls
+ how the proxy authenticates when exchanging authorization codes and refresh
+ tokens with the upstream provider. - `"client_secret_basic"`: Send credentials
+ in Authorization header (most common) - `"client_secret_post"`: Send
+ credentials in request body (required by some providers) - `"none"`: No
+ authentication (for public clients) - `None` (default): Uses authlib's default
+ (typically `"client_secret_basic"`) Set this if your provider requires a
+ specific authentication method and the default doesn't work.
+
+
+
+ List of allowed redirect URI patterns for MCP clients. Patterns support
+ wildcards (e.g., `"http://localhost:*"`, `"https://*.example.com/*"`).
+ - `None` (default): DCR clients use registered redirect URIs, with loopback
+ ports allowed to vary for MCP compatibility. Unsafe browser schemes such as
+ `javascript:`, `data:`, `file:`, and `vbscript:` are rejected.
+ - Empty list `[]`: No redirect URIs allowed
+ - Custom list: Only matching patterns allowed
+
+ These patterns apply to MCP client loopback redirects. Configure the upstream
+ OAuth app redirect URI separately with `redirect_path`.
+
+
+
+ List of all possible valid scopes for the OAuth provider. These are advertised
+ to clients through the `/.well-known` endpoints. Defaults to `required_scopes`
+ from your TokenVerifier if not specified.
+
+
+
+ Additional parameters to forward to the upstream authorization endpoint. Useful for provider-specific parameters that aren't part of the standard OAuth2 flow.
+
+ For example, Auth0 requires an `audience` parameter to issue JWT tokens:
+ ```python
+ extra_authorize_params={"audience": "https://api.example.com"}
+ ```
+
+ These parameters are added to every authorization request sent to the upstream provider.
+
+
+
+ Additional parameters to forward to the upstream token endpoint during code exchange and token refresh. Useful for provider-specific requirements during token operations.
+
+For example, some providers require additional context during token exchange:
+
+```python
+extra_token_params={"audience": "https://api.example.com"}
+```
+
+These parameters are included in all token requests to the upstream provider.
+
+
+
+
+
+
+ Storage backend for persisting OAuth client registrations and upstream tokens.
+
+ **Default behavior:**
+ By default, clients are automatically persisted to an encrypted disk store, allowing them to survive server restarts as long as the filesystem remains accessible. This means MCP clients only need to register once and can reconnect seamlessly. The disk store is encrypted using a key derived from the JWT Signing Key (which is derived from the upstream client secret by default). For client registrations to survive upstream client secret rotation, you should provide a JWT Signing Key or your own client_storage.
+
+For production deployments with multiple servers or cloud deployments, see [Storage Backends](/servers/storage-backends) for available options.
+
+
+ **When providing custom storage**, wrap it in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest:
+
+ ```python
+ from key_value.aio.stores.redis import RedisStore
+ from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
+ from cryptography.fernet import Fernet
+ import os
+
+ auth = OAuthProxy(
+ ...,
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ client_storage=FernetEncryptionWrapper(
+ key_value=RedisStore(host="redis.example.com", port=6379),
+ fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
+ )
+ )
+ ```
+
+ Without encryption, upstream OAuth tokens are stored in plaintext.
+
+
+Testing with in-memory storage (unencrypted):
+
+```python
+from key_value.aio.stores.memory import MemoryStore
+
+# Use in-memory storage for testing (clients lost on restart)
+auth = OAuthProxy(..., client_storage=MemoryStore())
+```
+
+
+
+
+
+
+ Secret used to sign FastMCP JWT tokens issued to clients. Accepts any string or bytes - will be derived into a proper 32-byte cryptographic key using HKDF.
+
+ **Default behavior (`None`):**
+ Derives a 32-byte key using PBKDF2 from the upstream client secret.
+
+ **For production:**
+ Provide an explicit secret (e.g., from environment variable) to use a fixed key instead of the key derived from the upstream client secret. This allows you to manage keys securely in cloud environments, allows keys to work across multiple instances, and allows you to rotate keys without losing client registrations.
+
+ ```python
+ import os
+
+ auth = OAuthProxy(
+ ...,
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"], # Any sufficiently complex string!
+ client_storage=RedisStore(...) # Persistent storage
+ )
+ ```
+
+ See [HTTP Deployment - OAuth Token Security](/deployment/http#oauth-token-security) for complete production setup.
+
+
+
+
+ Consent screen behavior for authorization requests. The consent page displays which client is requesting access, defending against [confused deputy and AS-in-the-middle attacks](#confused-deputy-attacks) by requiring explicit user approval.
+
+ **`True` (default) — always prompt:**
+ Users see the consent screen on every authorization. Strongest protection against AS-in-the-middle attacks where a malicious MCP server redirects the victim's browser into a legitimate proxy and relies on a previously-remembered approval to silently complete the flow.
+
+ **`"remember"` — silent consent on return:**
+ Users see the consent screen on first authorization; subsequent flows from the same browser for the same `(client_id, redirect_uri)` are silently approved via a signed cookie. Cross-site navigations (detected via `Sec-Fetch-Site`) fall back to the prompt. `Sec-Fetch-Site` is a browser-level heuristic rather than a protocol guarantee: an attacker who finds a way to initiate a non-cross-site navigation (XSS on a sibling origin, a same-site redirect chain, etc.) can reach the silent-consent path. `True` does not depend on this signal. See [Confused Deputy Attacks](#confused-deputy-attacks) for the underlying attack class.
+
+ **`"external"` — externally managed:**
+ Follows the same authorization path as `False`: FastMCP skips its consent page and associated browser-binding protections, then redirects directly to the upstream provider. The difference is logging. `False` emits a security warning, while `"external"` suppresses that warning as an explicit acknowledgment that the operator is enforcing equivalent consent and transaction-binding protections elsewhere. FastMCP does not provide or verify those external protections.
+
+ Ordinary upstream OAuth consent is generally not equivalent. It typically authorizes FastMCP's shared upstream application without identifying the downstream MCP client or binding approval to that client's transaction. Use `"external"` only when your surrounding authorization system supplies those protections.
+
+ **`False` — disable entirely:**
+ Authorization proceeds directly to the upstream provider without any consent UI. Logs a security warning. Only for local development or testing.
+
+ ```python
+ # Development/testing only - skip consent screen
+ auth = OAuthProxy(
+ ...,
+ require_authorization_consent=False # ⚠️ Security warning: only for local/testing
+ )
+
+ # Convenience mode - silent consent on return visits (less safe than True)
+ auth = OAuthProxy(
+ ...,
+ require_authorization_consent="remember",
+ )
+ ```
+
+
+ Both `False` and `"external"` disable FastMCP's consent and browser-binding protections. `False` warns about this configuration; `"external"` suppresses the warning because it is an operator acknowledgment that equivalent protections exist elsewhere. Prefer the default `True` unless you own that external authorization flow.
+
+
+
+
+ Content Security Policy for the consent page.
+
+ - `None` (default): Uses the built-in CSP policy with appropriate directives for form submission
+ - Empty string `""`: Disables CSP entirely (no meta tag rendered)
+ - Custom string: Uses the provided value as the CSP policy
+
+ This is useful for organizations that have their own CSP policies and need to override or disable FastMCP's built-in CSP directives.
+
+ ```python
+ # Disable CSP entirely (let org CSP policies apply)
+ auth = OAuthProxy(..., consent_csp_policy="")
+
+ # Use custom CSP policy
+ auth = OAuthProxy(..., consent_csp_policy="default-src 'self'; style-src 'unsafe-inline'")
+ ```
+
+
+
+### Using Built-in Providers
+
+FastMCP includes pre-configured providers for common services:
+
+```python
+from fastmcp.server.auth.providers.github import GitHubProvider
+
+auth = GitHubProvider(
+ client_id="your-github-app-id",
+ client_secret="your-github-app-secret",
+ base_url="https://your-server.com"
+)
+
+mcp = FastMCP(name="My Server", auth=auth)
+```
+
+Available providers include `GitHubProvider`, `GoogleProvider`, and others. These handle token verification automatically.
+
+### Token Verification
+
+The OAuth proxy requires a compatible `TokenVerifier` to validate tokens from your provider. Different providers use different token formats:
+
+- **JWT tokens** (Google, Azure): Use `JWTVerifier` with the provider's JWKS endpoint
+- **Opaque tokens with RFC 7662 introspection** (Auth0, Okta, WorkOS): Use `IntrospectionTokenVerifier`
+- **Opaque tokens (provider-specific)** (GitHub, Discord): Use provider-specific verifiers like `GitHubTokenVerifier`
+
+See the [Token Verification guide](/servers/auth/token-verification) for detailed setup instructions for your provider.
+
+### Scope Configuration
+
+OAuth scopes control what permissions your application requests from users. They're configured through your `TokenVerifier` (required for the OAuth proxy to validate tokens from your provider). Set `required_scopes` to automatically request the permissions your application needs:
+
+```python
+JWTVerifier(..., required_scopes = ["read:user", "write:data"])
+```
+
+Dynamic clients created by the proxy will automatically include these scopes in their authorization requests. See the [Token Verification](#token-verification) section below for detailed setup.
+
+### Custom Parameters
+
+Some OAuth providers require additional parameters beyond the standard OAuth2 flow. Use `extra_authorize_params` and `extra_token_params` to pass provider-specific requirements. For example, Auth0 requires an `audience` parameter to issue JWT tokens instead of opaque tokens:
+
+```python
+auth = OAuthProxy(
+ upstream_authorization_endpoint="https://your-domain.auth0.com/authorize",
+ upstream_token_endpoint="https://your-domain.auth0.com/oauth/token",
+ upstream_client_id="your-auth0-client-id",
+ upstream_client_secret="your-auth0-client-secret",
+
+ # Auth0-specific audience parameter
+ extra_authorize_params={"audience": "https://your-api-identifier.com"},
+ extra_token_params={"audience": "https://your-api-identifier.com"},
+
+ token_verifier=JWTVerifier(
+ jwks_uri="https://your-domain.auth0.com/.well-known/jwks.json",
+ issuer="https://your-domain.auth0.com/",
+ audience="https://your-api-identifier.com"
+ ),
+ base_url="https://your-server.com"
+)
+```
+
+The proxy also forwards RFC 8707 `resource` parameters from MCP clients to upstream providers that support them. This is enabled by default via the `forward_resource` parameter. Disable it for providers that reject unknown parameters.
+
+## OAuth Flow
+
+```mermaid
+sequenceDiagram
+ participant Client as MCP Client (localhost:random)
+ participant User as User
+ participant Proxy as FastMCP OAuth Proxy (server:8000)
+ participant Provider as OAuth Provider (GitHub, etc.)
+
+ Note over Client, Proxy: Dynamic Registration (Local)
+ Client->>Proxy: 1. POST /register redirect_uri: localhost:54321/callback
+ Proxy-->>Client: 2. Returns fixed upstream credentials
+
+ Note over Client, User: Authorization with User Consent
+ Client->>Proxy: 3. GET /authorize redirect_uri=localhost:54321/callback code_challenge=CLIENT_CHALLENGE
+ Note over Proxy: Store transaction with client PKCE Generate proxy PKCE pair
+ Proxy->>User: 4. Show consent page (client details, redirect URI, scopes)
+ User->>Proxy: 5. Approve/deny consent
+ Note over Proxy: Set consent binding cookie (binds browser to this flow)
+ Proxy->>Provider: 6. Redirect to provider redirect_uri=server:8000/auth/callback code_challenge=PROXY_CHALLENGE
+
+ Note over Provider, Proxy: Provider Callback
+ Provider->>Proxy: 7. GET /auth/callback with authorization code
+ Note over Proxy: Verify consent binding cookie (reject if missing or mismatched)
+ Proxy->>Provider: 8. Exchange code for tokens code_verifier=PROXY_VERIFIER
+ Provider-->>Proxy: 9. Access & refresh tokens
+
+ Note over Proxy, Client: Client Callback Forwarding
+ Proxy->>Client: 10. Redirect to localhost:54321/callback with new authorization code
+
+ Note over Client, Proxy: Token Exchange
+ Client->>Proxy: 11. POST /token with code code_verifier=CLIENT_VERIFIER
+ Proxy-->>Client: 12. Returns FastMCP JWT tokens
+```
+
+The flow diagram above illustrates the complete OAuth proxy pattern. Let's understand each phase:
+
+### Registration Phase
+
+When an MCP client calls `/register` with its dynamic callback URL, the proxy responds with your pre-configured upstream credentials. The client stores these credentials believing it has registered a new app. Meanwhile, the proxy records the client's callback URL for later use.
+
+### Authorization Phase
+
+The client initiates OAuth by redirecting to the proxy's `/authorize` endpoint. The proxy:
+
+1. Stores the client's transaction with its PKCE challenge
+2. Generates its own PKCE parameters for upstream security
+3. Shows the user a consent page with the client's details, redirect URI, and requested scopes
+4. If the user approves (or the client was previously approved), sets a consent binding cookie and redirects to the upstream provider using the fixed callback URL
+
+This dual-PKCE approach maintains end-to-end security at both the client-to-proxy and proxy-to-provider layers. The consent step protects against confused deputy attacks by ensuring you explicitly approve each client before it can complete authorization, and the consent binding cookie ensures that only the browser that approved consent can complete the callback.
+
+### Callback Phase
+
+After user authorization, the provider redirects back to the proxy's fixed callback URL. The proxy:
+
+1. Verifies the consent binding cookie matches the transaction (rejecting requests from a different browser)
+2. Exchanges the authorization code for tokens with the provider
+3. Stores these tokens temporarily
+4. Generates a new authorization code for the client
+5. Redirects to the client's original dynamic callback URL
+
+### Token Exchange Phase
+
+Finally, the client exchanges its authorization code with the proxy. The proxy validates the client's PKCE verifier, then issues its own FastMCP JWT tokens (rather than forwarding the upstream provider's tokens). See [Token Architecture](#token-architecture) for details on this design.
+
+This entire flow is transparent to the MCP client—it experiences a standard OAuth flow with dynamic registration, unaware that a proxy is managing the complexity behind the scenes.
+
+### Token Architecture
+
+The OAuth proxy implements a **token factory pattern**: instead of directly forwarding tokens from the upstream OAuth provider, it issues its own JWT tokens to MCP clients. This maintains proper OAuth 2.0 token audience boundaries and enables better security controls.
+
+**How it works:**
+
+When an MCP client completes authorization, the proxy:
+
+1. **Receives upstream tokens** from the OAuth provider (GitHub, Google, etc.)
+2. **Encrypts and stores** these tokens using Fernet encryption (AES-128-CBC + HMAC-SHA256)
+3. **Issues FastMCP JWT tokens** to the client, signed with HS256
+
+The FastMCP JWT contains minimal claims: issuer, audience, client ID, scopes, expiration, and a unique token identifier (JTI). The JTI acts as a reference linking to the encrypted upstream token.
+
+**Token validation:**
+
+When a client makes an MCP request with its FastMCP token:
+
+1. **FastMCP validates the JWT** signature, expiration, issuer, and audience
+2. **Looks up the upstream token** using the JTI from the validated JWT
+3. **Decrypts and validates** the upstream token with the provider
+
+This two-tier validation ensures that FastMCP tokens can only be used with this server (via audience validation) while maintaining full upstream token security.
+
+This architecture also prevents [token passthrough](#token-passthrough) — see the [Security](#security) section for details.
+
+**Token expiry alignment:**
+
+By default, FastMCP token lifetimes match the upstream token lifetimes. When the upstream token expires, the FastMCP token also expires, maintaining consistent security boundaries.
+
+**Extending the FastMCP token lifetime:**
+
+Some upstream providers issue short-lived access tokens (5–60 minutes is common). Because the FastMCP token is a reference into the proxy's storage rather than the upstream credential itself, its client-facing lifetime can be longer than the upstream token's without weakening security: every request re-validates the upstream token and transparently refreshes it when it has expired, so a revoked or genuinely expired upstream session still fails validation and forces re-authentication.
+
+This matters for MCP clients that don't refresh gracefully. For example, [`mcp-remote`](https://github.com/geelen/mcp-remote) (used by Claude Desktop) has known issues handling access-token expiry, so a short upstream lifetime can push users through a full OAuth flow after every idle period. Set `fastmcp_access_token_expiry_seconds` to decouple the FastMCP token lifetime from the upstream `expires_in`:
+
+```python
+from fastmcp.server.auth import OAuthProxy
+
+auth = OAuthProxy(
+ upstream_authorization_endpoint="https://provider.com/oauth/authorize",
+ upstream_token_endpoint="https://provider.com/oauth/token",
+ upstream_client_id="your-client-id",
+ upstream_client_secret="your-client-secret",
+ token_verifier=token_verifier,
+ base_url="https://your-server.com",
+ fastmcp_access_token_expiry_seconds=60 * 60 * 24, # 24 hours
+)
+```
+
+The upstream token's real expiry is preserved internally to drive transparent refresh; only the FastMCP-issued token lives longer. This parameter is available on every provider built on the OAuth proxy (`GitHubProvider`, `GoogleProvider`, `AzureProvider`, and the rest).
+
+Extending the lifetime only works when the upstream provider issues a refresh token, since that's what lets the proxy renew the access token behind the scenes. When the upstream provides no refresh token, the FastMCP token lifetime is capped at the upstream `expires_in` — issuing a longer-lived token would claim a validity the proxy can't honor.
+
+**Refresh tokens:**
+
+The proxy issues its own refresh tokens that map to upstream refresh tokens. When a client uses a FastMCP refresh token, the proxy refreshes the upstream token and issues a new FastMCP access token.
+
+### PKCE Forwarding
+
+The OAuth proxy automatically handles PKCE (Proof Key for Code Exchange) when working with providers that support or require it. The proxy generates its own PKCE parameters to send upstream while separately validating the client's PKCE, ensuring end-to-end security at both layers.
+
+This is enabled by default via the `forward_pkce` parameter and works seamlessly with providers like Google, Azure AD, and GitHub. Only disable it for legacy providers that don't support PKCE:
+
+```python
+# Disable PKCE forwarding only if upstream doesn't support it
+auth = OAuthProxy(
+ ...,
+ forward_pkce=False # Default is True
+)
+```
+
+### Redirect URI Validation
+
+By default, the OAuth proxy validates DCR clients against their registered redirect URIs while allowing loopback ports to vary for MCP compatibility. Unsafe browser schemes such as `javascript:` are always rejected. You can restrict which clients can connect at the server level by specifying allowed patterns:
+
+```python
+# Allow only localhost clients (common for development)
+auth = OAuthProxy(
+ # ... other parameters ...
+ allowed_client_redirect_uris=[
+ "http://localhost:*",
+ "http://127.0.0.1:*"
+ ]
+)
+
+# Allow specific known clients
+auth = OAuthProxy(
+ # ... other parameters ...
+ allowed_client_redirect_uris=[
+ "http://localhost:*",
+ "https://claude.ai/api/mcp/auth_callback",
+ "https://*.mycompany.com/auth/*" # Wildcard patterns supported
+ ]
+)
+```
+
+Check your server logs for "Client registered with redirect_uri" messages to identify what URLs your clients use.
+
+## CIMD Support
+
+
+
+The OAuth proxy supports **Client ID Metadata Documents (CIMD)**, an alternative to Dynamic Client Registration where clients host a static JSON document at an HTTPS URL. Instead of registering dynamically, clients simply provide their CIMD URL as their `client_id`, and the server fetches and validates the metadata.
+
+CIMD clients appear in the consent screen with a verified domain badge, giving users confidence about which application is requesting access. This provides stronger identity verification than DCR, where any client can claim any name.
+
+### How CIMD Works
+
+When a client presents an HTTPS URL as its `client_id` (for example, `https://myapp.example.com/oauth/client.json`), the OAuth proxy recognizes it as a CIMD client and:
+
+1. Fetches the JSON document from that URL
+2. Validates that the document's `client_id` field matches the URL
+3. Extracts client metadata (name, redirect URIs, scopes, etc.)
+4. Stores the client persistently alongside DCR clients
+5. Shows the verified domain in the consent screen
+
+This flow happens transparently. MCP clients that support CIMD simply provide their metadata URL instead of registering, and the OAuth proxy handles the rest.
+
+### CIMD Configuration
+
+CIMD support is enabled by default for `OAuthProxy`.
+
+
+
+ Whether to accept CIMD URLs as client identifiers. When enabled, clients can use HTTPS URLs pointing to metadata documents as their `client_id` instead of registering via DCR.
+
+
+
+### Private Key JWT Authentication
+
+CIMD clients can authenticate using `private_key_jwt` instead of the default `none` authentication method. This provides cryptographic proof of client identity by signing JWT assertions with a private key, while the server verifies using the client's public key from their CIMD document.
+
+To use `private_key_jwt`, the CIMD document must include either a `jwks_uri` (URL to fetch the public key set) or inline `jwks` (the key set directly in the document):
+
+```json
+{
+ "client_id": "https://myapp.example.com/oauth/client.json",
+ "client_name": "My Secure App",
+ "redirect_uris": ["http://localhost:*/callback"],
+ "token_endpoint_auth_method": "private_key_jwt",
+ "jwks_uri": "https://myapp.example.com/.well-known/jwks.json"
+}
+```
+
+The OAuth proxy validates JWT assertions according to RFC 7523, checking the signature, issuer, audience, subject claims, and preventing replay attacks via JTI tracking.
+
+### Security Considerations
+
+CIMD provides several security advantages over DCR:
+
+- **Verified identity**: The domain in the `client_id` URL is verified by HTTPS, so users know which organization is requesting access
+- **No registration required**: Clients don't need to store or manage dynamically-issued credentials
+- **Redirect URI enforcement**: CIMD documents must declare `redirect_uris`, which are enforced by the proxy (wildcard patterns supported)
+- **SSRF protection**: The OAuth proxy blocks fetches to localhost, private IPs, and reserved addresses
+- **Replay prevention**: For `private_key_jwt` clients, JTI claims are tracked to prevent assertion replay
+- **Cache-aware fetching**: CIMD documents are cached according to HTTP cache headers and revalidated when required
+
+CIMD is enabled by default. To disable it entirely (for example, to require all clients to register via DCR), set `enable_cimd=False` explicitly:
+
+```python
+auth = OAuthProxy(
+ ...,
+ enable_cimd=False,
+)
+```
+
+## Security
+
+### Key and Storage Management
+
+
+The OAuth proxy requires cryptographic keys for JWT signing and storage encryption, plus persistent storage to maintain valid tokens across server restarts.
+
+**Default behavior (appropriate for development only):**
+- **Mac/Windows**: FastMCP automatically generates keys and stores them in your system keyring. Storage defaults to disk. Tokens survive server restarts. This is **only** suitable for development and local testing.
+- **Linux**: Keys are ephemeral (random salt at startup). Storage defaults to memory. Tokens become invalid on server restart.
+
+**For production:**
+Configure the following parameters together: provide a unique `jwt_signing_key` (for signing FastMCP JWTs), and a shared `client_storage` backend (for storing tokens). Both are required for production deployments. Use a network-accessible storage backend like Redis or DynamoDB rather than local disk storage. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** (see the `client_storage` parameter documentation above for examples). The keys accept any secret string and derive proper cryptographic keys using HKDF. See [OAuth Token Security](/deployment/http#oauth-token-security) and [Storage Backends](/servers/storage-backends) for complete production setup.
+
+### Confused Deputy Attacks
+
+
+
+A confused deputy attack allows a malicious client to steal your authorization by tricking you into granting it access under your identity.
+
+The OAuth proxy works by bridging DCR clients to traditional auth providers, which means that multiple MCP clients connect through a single upstream OAuth application. An attacker can exploit this shared application by registering a malicious client with their own redirect URI, then sending you an authorization link. When you click it, your browser goes through the OAuth flow—but since you may have already authorized this OAuth app before, the provider might auto-approve the request. The authorization code then gets sent to the attacker's redirect URI instead of a legitimate client, giving them access under your credentials.
+
+#### Mitigation
+
+FastMCP's OAuth proxy defends against confused deputy attacks with two layers of protection:
+
+**Consent screen.** Before any authorization happens, you see a consent page showing the client's details, redirect URI, and requested scopes. This gives you the opportunity to review and deny suspicious requests. By default (`require_authorization_consent=True`), the page is shown on every flow, which is the strongest protection. Setting `require_authorization_consent="remember"` approves previously-approved `(client_id, redirect_uri)` pairs silently on return visits, trading some protection for UX (see below). The consent mechanism is implemented with CSRF tokens and cryptographically signed cookies to prevent tampering.
+
+
+
+The consent page automatically displays your server's name, icon, and website URL, if available. These visual identifiers help users confirm they're authorizing the correct server.
+
+**Browser-session binding.** When you approve consent (or when a previously-approved client auto-approves), the proxy sets a cryptographically signed cookie that binds your browser session to the authorization flow. When the identity provider redirects back to the proxy's callback, the proxy verifies that this cookie is present and matches the expected transaction. A different browser — such as a victim who was sent the authorization URL by an attacker — won't have this cookie, and the callback will be rejected with a 403 error. This prevents the attack even when the identity provider skips the consent page for previously-authorized applications.
+
+#### AS-in-the-middle variant
+
+A related attack works even with browser-session binding in place: a malicious MCP server advertises its own authorization server, which redirects the victim's browser into the legitimate proxy's `/authorize` endpoint. Because the victim's browser carries both the prior-approval cookie and the newly-issued session-binding cookie throughout, both layers pass. The defense is the consent prompt itself: if consent is shown (`require_authorization_consent=True`), the victim sees the benign MCP server's name on the consent page — which doesn't match the malicious server they thought they were connecting to — and can deny.
+
+`require_authorization_consent="remember"` adds a `Sec-Fetch-Site` check to keep this path safe for legitimate return flows (the attack navigation lands as `cross-site` and falls back to the prompt), but this is a browser-level heuristic. For the strongest defense, leave `require_authorization_consent=True`.
+
+**Learn more:**
+- [MCP Security Best Practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#confused-deputy-problem) - Official specification guidance
+- [Confused Deputy Attacks Explained](https://den.dev/blog/mcp-confused-deputy-api-management/) - Detailed walkthrough by Den Delimarsky
+
+### Token Passthrough
+
+[Token passthrough](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#token-passthrough) occurs when an intermediary exposes upstream tokens to downstream clients, allowing those clients to impersonate the intermediary or access services they shouldn't reach.
+
+#### Client-facing mitigation
+
+The OAuth proxy's [token factory architecture](#token-architecture) prevents this by design. MCP clients only ever receive FastMCP-issued JWTs — the upstream provider token is never sent to the client. A FastMCP JWT is scoped to your server and cannot be used to access the upstream provider directly, even if intercepted.
+
+#### Calling downstream services
+
+When your MCP server needs to call other APIs on behalf of the authenticated user, avoid forwarding the upstream token directly — this reintroduces the token passthrough problem in the other direction. Instead, use a token exchange flow like [OAuth 2.0 Token Exchange (RFC 8693)](https://datatracker.ietf.org/doc/html/rfc8693) or your provider's equivalent (such as Azure's [On-Behalf-Of flow](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-on-behalf-of-flow)) to obtain a new token scoped to the downstream service.
+
+The upstream token is available in your tool functions via `get_access_token()` or the `CurrentAccessToken` dependency, which you can use as the assertion for a token exchange. The exchanged token will be scoped to the specific downstream service and identify your MCP server as the authorized intermediary, maintaining proper audience boundaries throughout the chain.
+
+## Production Configuration
+
+For production deployments, load sensitive credentials from environment variables:
+
+```python
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.github import GitHubProvider
+
+# Load secrets from environment variables
+auth = GitHubProvider(
+ client_id=os.environ.get("GITHUB_CLIENT_ID"),
+ client_secret=os.environ.get("GITHUB_CLIENT_SECRET"),
+ base_url=os.environ.get("BASE_URL", "https://your-production-server.com")
+)
+
+mcp = FastMCP(name="My Server", auth=auth)
+
+@mcp.tool
+def protected_tool(data: str) -> str:
+ """This tool is now protected by OAuth."""
+ return f"Processed: {data}"
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
+```
+
+This keeps secrets out of your codebase while maintaining explicit configuration.
diff --git a/docs/v3/servers/auth/oidc-proxy.mdx b/docs/v3/servers/auth/oidc-proxy.mdx
new file mode 100644
index 000000000..81ca677a2
--- /dev/null
+++ b/docs/v3/servers/auth/oidc-proxy.mdx
@@ -0,0 +1,287 @@
+---
+title: OIDC Proxy
+sidebarTitle: OIDC Proxy
+description: Bridge OIDC providers to work seamlessly with MCP's authentication flow.
+icon: share
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx";
+
+
+
+The OIDC proxy enables FastMCP servers to authenticate with OIDC providers that **don't support Dynamic Client Registration (DCR)** out of the box. This includes OAuth providers like: Auth0, Google, Azure, AWS, etc. For providers that do support DCR (like WorkOS AuthKit), use [`RemoteAuthProvider`](/servers/auth/remote-oauth) instead.
+
+The OIDC proxy is built upon [`OAuthProxy`](/servers/auth/oauth-proxy) so it has all the same functionality under the covers.
+
+## Implementation
+
+### Provider Setup Requirements
+
+Before using the OIDC proxy, you need to register your application with your OAuth provider:
+
+1. **Register your application** in the provider's developer console (Auth0 Applications, Google Cloud Console, Azure Portal, etc.)
+2. **Configure the redirect URI** as your FastMCP server URL plus your chosen callback path:
+ - Default: `https://your-server.com/auth/callback`
+ - Custom: `https://your-server.com/your/custom/path` (if you set `redirect_path`)
+ - Development: `http://localhost:8000/auth/callback`
+3. **Obtain your credentials**: Client ID and Client Secret
+
+
+ The redirect URI you configure with your provider must exactly match your
+ FastMCP server's URL plus the callback path. If you customize `redirect_path`
+ in the OIDC proxy, update your provider's redirect URI accordingly.
+
+
+### Basic Setup
+
+Here's how to implement the OIDC proxy with any provider:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.oidc_proxy import OIDCProxy
+
+# Create the OIDC proxy
+auth = OIDCProxy(
+ # Provider's configuration URL
+ config_url="https://provider.com/.well-known/openid-configuration",
+
+ # Your registered app credentials
+ client_id="your-client-id",
+ client_secret="your-client-secret",
+
+ # Your FastMCP server's public URL
+ base_url="https://your-server.com",
+
+ # Optional: customize the callback path (default is "/auth/callback")
+ # redirect_path="/custom/callback",
+)
+
+mcp = FastMCP(name="My Server", auth=auth)
+```
+
+### Configuration Parameters
+
+
+
+ URL of your OAuth provider's OIDC configuration
+
+
+
+ Client ID from your registered OAuth application
+
+
+
+ Client secret from your registered OAuth application. Optional for PKCE public
+ clients. When omitted, `jwt_signing_key` must be provided.
+
+
+
+ Public URL of your FastMCP server (e.g., `https://your-server.com`)
+
+
+
+ Optional public base URL for the protected resource metadata and token audience.
+
+ Use this when your OAuth callbacks and operational endpoints need to live under one public URL, but the protected MCP resource should be advertised under another. FastMCP will still append the MCP mount path (for example, `/mcp`) to this base URL.
+
+
+
+ Strict flag for configuration validation. When True, requires all OIDC
+ mandatory fields.
+
+
+
+ Audience parameter for OIDC providers that require it (e.g., Auth0). This is
+ typically your API identifier.
+
+
+
+ HTTP request timeout in seconds for fetching OIDC configuration
+
+
+
+
+
+ Custom token verifier for validating tokens. When provided, FastMCP uses your custom verifier instead of creating a default `JWTVerifier`.
+
+ Cannot be used with `algorithm` or `required_scopes` parameters - configure these on your verifier instead. The verifier's `required_scopes` are automatically loaded and advertised.
+
+
+
+ JWT algorithm to use for token verification (e.g., "RS256"). If not specified,
+ uses the provider's default. Only used when `token_verifier` is not provided.
+
+
+
+ List of OAuth scopes for token validation. These are automatically
+ included in authorization requests. Only used when `token_verifier` is not provided.
+
+
+
+ Path for OAuth callbacks. Must match the redirect URI configured in your OAuth
+ application
+
+
+
+ List of allowed redirect URI patterns for MCP clients. Patterns support wildcards (e.g., `"http://localhost:*"`, `"https://*.example.com/*"`).
+ - `None` (default): DCR clients use registered redirect URIs, with loopback ports allowed to vary for MCP compatibility. Unsafe browser schemes such as `javascript:`, `data:`, `file:`, and `vbscript:` are rejected.
+ - Empty list `[]`: No redirect URIs allowed
+ - Custom list: Only matching patterns allowed
+
+These patterns apply to MCP client loopback redirects. Configure the upstream OAuth app redirect URI separately with `redirect_path`.
+
+
+
+
+ Token endpoint authentication method for the upstream OAuth server. Controls how the proxy authenticates when exchanging authorization codes and refresh tokens with the upstream provider.
+ - `"client_secret_basic"`: Send credentials in Authorization header (most common)
+ - `"client_secret_post"`: Send credentials in request body (required by some providers)
+ - `"none"`: No authentication (for public clients)
+ - `None` (default): Uses authlib's default (typically `"client_secret_basic"`)
+
+Set this if your provider requires a specific authentication method and the default doesn't work.
+
+
+
+
+
+
+ Secret used to sign FastMCP JWT tokens issued to clients. Accepts any string or bytes - will be derived into a proper 32-byte cryptographic key using HKDF.
+
+ **Default behavior (`None`):**
+ - **Mac/Windows**: Auto-managed via system keyring. Keys are generated once and persisted, surviving server restarts with zero configuration. Keys are automatically derived from server attributes, so this approach, while convenient, is **only** suitable for development and local testing. For production, you must provide an explicit secret.
+ - **Linux**: Ephemeral (random salt at startup). Tokens become invalid on server restart, triggering client re-authentication.
+
+ **For production:**
+ Provide an explicit secret (e.g., from environment variable) to use a fixed key instead of the auto-generated one.
+
+
+
+
+
+ Storage backend for persisting OAuth client registrations and upstream tokens.
+
+ **Default behavior:**
+ - **Mac/Windows**: Encrypted DiskStore in your platform's data directory (derived from `platformdirs`)
+ - **Linux**: MemoryStore (ephemeral - clients lost on restart)
+
+ By default on Mac/Windows, clients are automatically persisted to encrypted disk storage, allowing them to survive server restarts as long as the filesystem remains accessible. This means MCP clients only need to register once and can reconnect seamlessly. On Linux where keyring isn't available, ephemeral storage is used to match the ephemeral key strategy.
+
+For production deployments with multiple servers or cloud deployments, use a network-accessible storage backend rather than local disk storage. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest.** See [Storage Backends](/servers/storage-backends) for available options.
+
+Testing with in-memory storage (unencrypted):
+
+```python
+from key_value.aio.stores.memory import MemoryStore
+
+# Use in-memory storage for testing (clients lost on restart)
+auth = OIDCProxy(..., client_storage=MemoryStore())
+```
+
+Production with encrypted Redis storage:
+
+```python
+from key_value.aio.stores.redis import RedisStore
+from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
+from cryptography.fernet import Fernet
+import os
+
+auth = OIDCProxy(
+ ...,
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ client_storage=FernetEncryptionWrapper(
+ key_value=RedisStore(host="redis.example.com", port=6379),
+ fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
+ )
+)
+```
+
+
+
+
+ Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (same authorization path as `False`, but the warning is suppressed because the operator asserts that equivalent protections are enforced externally), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs.
+
+
+
+ Content Security Policy for the consent page.
+
+ - `None` (default): Uses the built-in CSP policy with appropriate directives for form submission
+ - Empty string `""`: Disables CSP entirely (no meta tag rendered)
+ - Custom string: Uses the provided value as the CSP policy
+
+ This is useful for organizations that have their own CSP policies and need to override or disable FastMCP's built-in CSP directives.
+
+
+
+### Using Built-in Providers
+
+FastMCP includes pre-configured OIDC providers for common services:
+
+```python
+from fastmcp.server.auth.providers.auth0 import Auth0Provider
+
+auth = Auth0Provider(
+ config_url="https://.../.well-known/openid-configuration",
+ client_id="your-auth0-client-id",
+ client_secret="your-auth0-client-secret",
+ audience="https://...",
+ base_url="https://localhost:8000"
+)
+
+mcp = FastMCP(name="My Server", auth=auth)
+```
+
+Available providers include `Auth0Provider` at present.
+
+### Scope Configuration
+
+OAuth scopes are configured with `required_scopes` to automatically request the permissions your application needs.
+
+Dynamic clients created by the proxy will automatically include these scopes in their authorization requests.
+
+## CIMD Support
+
+
+
+The OIDC proxy inherits full CIMD (Client ID Metadata Document) support from `OAuthProxy`. Clients can use HTTPS URLs as their `client_id` instead of registering dynamically, and the proxy will fetch and validate their metadata document.
+
+See the [OAuth Proxy CIMD documentation](/servers/auth/oauth-proxy#cimd-support) for complete details on how CIMD works, including private key JWT authentication and security considerations.
+
+The CIMD-related parameters available on `OIDCProxy` are:
+
+
+
+ Whether to accept CIMD URLs as client identifiers.
+
+
+
+## Production Configuration
+
+For production deployments, load sensitive credentials from environment variables:
+
+```python
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.auth0 import Auth0Provider
+
+# Load secrets from environment variables
+auth = Auth0Provider(
+ config_url=os.environ.get("AUTH0_CONFIG_URL"),
+ client_id=os.environ.get("AUTH0_CLIENT_ID"),
+ client_secret=os.environ.get("AUTH0_CLIENT_SECRET"),
+ audience=os.environ.get("AUTH0_AUDIENCE"),
+ base_url=os.environ.get("BASE_URL", "https://localhost:8000")
+)
+
+mcp = FastMCP(name="My Server", auth=auth)
+
+@mcp.tool
+def protected_tool(data: str) -> str:
+ """This tool is now protected by OAuth."""
+ return f"Processed: {data}"
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
+```
+
+This keeps secrets out of your codebase while maintaining explicit configuration.
diff --git a/docs/v3/servers/auth/remote-oauth.mdx b/docs/v3/servers/auth/remote-oauth.mdx
new file mode 100644
index 000000000..c2256b052
--- /dev/null
+++ b/docs/v3/servers/auth/remote-oauth.mdx
@@ -0,0 +1,240 @@
+---
+title: Remote OAuth
+sidebarTitle: Remote OAuth
+description: Integrate your FastMCP server with external identity providers like Descope, WorkOS, Auth0, and corporate SSO systems.
+icon: camera-cctv
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Remote OAuth integration allows your FastMCP server to leverage external identity providers that **support Dynamic Client Registration (DCR)**. With DCR, MCP clients can automatically register themselves with the identity provider and obtain credentials without any manual configuration. This provides enterprise-grade authentication with fully automated flows, making it ideal for production applications with modern identity providers.
+
+
+**When to use RemoteAuthProvider vs OAuth Proxy:**
+- **RemoteAuthProvider**: For providers WITH Dynamic Client Registration (Descope, WorkOS AuthKit, modern OIDC providers)
+- **OAuth Proxy**: For providers WITHOUT Dynamic Client Registration (GitHub, Google, Azure, AWS, Discord, etc.)
+
+RemoteAuthProvider requires DCR support for fully automated client registration and authentication.
+
+
+## DCR-Enabled Providers
+
+RemoteAuthProvider works with identity providers that support **Dynamic Client Registration (DCR)** - a critical capability that enables automated authentication flows:
+
+| Feature | DCR Providers (RemoteAuth) | Non-DCR Providers (OAuth Proxy) |
+|---------|---------------------------|--------------------------------|
+| **Client Registration** | Automatic via API | Manual in provider console |
+| **Credentials** | Dynamic per client | Fixed app credentials |
+| **Configuration** | Zero client config | Pre-shared credentials |
+| **Examples** | Descope, WorkOS AuthKit, modern OIDC | GitHub, Google, Azure |
+| **FastMCP Class** | `RemoteAuthProvider` | [`OAuthProxy`](/servers/auth/oauth-proxy) |
+
+If your provider doesn't support DCR (most traditional OAuth providers), you'll need to use [`OAuth Proxy`](/servers/auth/oauth-proxy) instead, which bridges the gap between MCP's DCR expectations and fixed OAuth credentials.
+
+## The Remote OAuth Challenge
+
+Traditional OAuth flows assume human users with web browsers who can interact with login forms, consent screens, and redirects. MCP clients operate differently - they're often automated systems that need to authenticate programmatically without human intervention.
+
+This creates several unique requirements that standard OAuth implementations don't address well:
+
+**Automatic Discovery**: MCP clients must discover authentication requirements by examining server metadata rather than encountering HTTP redirects. They need to know which identity provider to use and how to reach it before making any authenticated requests.
+
+**Programmatic Registration**: Clients need to register themselves with identity providers automatically. Manual client registration doesn't work when clients might be dynamically created tools or services.
+
+**Seamless Token Management**: Clients must obtain, store, and refresh tokens without user interaction. The authentication flow needs to work in headless environments where no human is available to complete OAuth consent flows.
+
+**Protocol Integration**: The authentication process must integrate cleanly with MCP's JSON-RPC transport layer and error handling mechanisms.
+
+These requirements mean that your MCP server needs to do more than just validate tokens - it needs to provide discovery metadata that enables MCP clients to understand and navigate your authentication requirements automatically.
+
+## MCP Authentication Discovery
+
+MCP authentication discovery relies on well-known endpoints that clients can examine to understand your authentication requirements. Your server becomes a bridge between MCP clients and your chosen identity provider.
+
+The core discovery endpoint is `/.well-known/oauth-protected-resource`, which tells clients that your server requires OAuth authentication and identifies the authorization servers you trust. This endpoint contains static metadata that points clients to your identity provider without requiring any dynamic lookups.
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant FastMCPServer as FastMCP Server
+ participant ExternalIdP as Identity Provider
+
+ Client->>FastMCPServer: 1. GET /.well-known/oauth-protected-resource
+ FastMCPServer-->>Client: 2. "Use https://my-idp.com for auth"
+
+ note over Client, ExternalIdP: Client goes directly to the IdP
+ Client->>ExternalIdP: 3. Authenticate & get token via DCR
+ ExternalIdP-->>Client: 4. Access token
+
+ Client->>FastMCPServer: 5. MCP request with Bearer token
+ FastMCPServer->>FastMCPServer: 6. Verify token signature
+ FastMCPServer-->>Client: 7. MCP response
+```
+
+This flow separates concerns cleanly: your MCP server handles resource protection and token validation, while your identity provider handles user authentication and token issuance. The client coordinates between these systems using standardized OAuth discovery mechanisms.
+
+## FastMCP Remote Authentication
+
+
+
+FastMCP provides `RemoteAuthProvider` to handle the complexities of remote OAuth integration. This class combines token validation capabilities with the OAuth discovery metadata that MCP clients require.
+
+### RemoteAuthProvider
+
+`RemoteAuthProvider` works by composing a [`TokenVerifier`](/servers/auth/token-verification) with authorization server information. A `TokenVerifier` is another FastMCP authentication class that focuses solely on token validation - signature verification, expiration checking, and claim extraction. The `RemoteAuthProvider` takes that token validation capability and adds the OAuth discovery endpoints that enable MCP clients to automatically find and authenticate with your identity provider.
+
+This composition pattern means you can use any token validation strategy while maintaining consistent OAuth discovery behavior:
+- **JWT tokens**: Use `JWTVerifier` for self-contained tokens
+- **Opaque tokens**: Use `IntrospectionTokenVerifier` for RFC 7662 introspection
+- **Custom validation**: Implement your own `TokenVerifier` subclass
+
+The separation allows you to change token validation approaches without affecting the client discovery experience.
+
+The class automatically generates the required OAuth metadata endpoints using the MCP SDK's standardized route creation functions. This ensures compatibility with MCP clients while reducing the implementation complexity for server developers.
+
+### Basic Implementation
+
+Most applications can use `RemoteAuthProvider` directly without subclassing. The implementation requires a `TokenVerifier` instance, a list of trusted authorization servers, and your server's URL for metadata generation.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import RemoteAuthProvider
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+from pydantic import AnyHttpUrl
+
+# Configure token validation for your identity provider
+token_verifier = JWTVerifier(
+ jwks_uri="https://auth.yourcompany.com/.well-known/jwks.json",
+ issuer="https://auth.yourcompany.com",
+ audience="mcp-production-api"
+)
+
+# Create the remote auth provider
+auth = RemoteAuthProvider(
+ token_verifier=token_verifier,
+ authorization_servers=[AnyHttpUrl("https://auth.yourcompany.com")],
+ base_url="https://api.yourcompany.com", # Your server base URL
+ # Optional: restrict allowed client redirect URIs
+ allowed_client_redirect_uris=["http://localhost:*", "http://127.0.0.1:*"]
+)
+
+mcp = FastMCP(name="Company API", auth=auth)
+```
+
+This configuration creates a server that accepts tokens issued by `auth.yourcompany.com` and provides the OAuth discovery metadata that MCP clients need. The `JWTVerifier` handles token validation using your identity provider's public keys, while the `RemoteAuthProvider` generates the required OAuth endpoints.
+
+The `authorization_servers` list tells MCP clients which identity providers you trust. The `base_url` identifies your server in OAuth metadata, enabling proper token audience validation. **Important**: The `base_url` should point to your server base URL - for example, if your MCP server is accessible at `https://api.yourcompany.com/mcp`, use `https://api.yourcompany.com` as the base URL.
+
+### Overriding Advertised Scopes
+
+Some identity providers use different scope formats for authorization requests versus token claims. For example, Azure AD requires clients to request full URI scopes like `api://client-id/read`, but the token's `scp` claim contains just `read`. The `scopes_supported` parameter lets you advertise the full-form scopes in metadata while validating against the short form:
+
+```python
+auth = RemoteAuthProvider(
+ token_verifier=token_verifier,
+ authorization_servers=[AnyHttpUrl("https://auth.example.com")],
+ base_url="https://api.example.com",
+ scopes_supported=["api://my-api/read", "api://my-api/write"],
+)
+```
+
+When not set, `scopes_supported` defaults to the token verifier's `required_scopes`. For Azure AD specifically, see the [AzureJWTVerifier](/integrations/azure#token-verification-only-managed-identity) which handles this automatically.
+
+### Custom Endpoints
+
+You can extend `RemoteAuthProvider` to add additional endpoints beyond the standard OAuth protected resource metadata. These don't have to be OAuth-specific - you can add any endpoints your authentication integration requires.
+
+```python
+import httpx
+from starlette.responses import JSONResponse
+from starlette.routing import Route
+
+class CompanyAuthProvider(RemoteAuthProvider):
+ def __init__(self):
+ token_verifier = JWTVerifier(
+ jwks_uri="https://auth.yourcompany.com/.well-known/jwks.json",
+ issuer="https://auth.yourcompany.com",
+ audience="mcp-production-api"
+ )
+
+ super().__init__(
+ token_verifier=token_verifier,
+ authorization_servers=[AnyHttpUrl("https://auth.yourcompany.com")],
+ base_url="https://api.yourcompany.com" # Your server base URL
+ )
+
+ def get_routes(self) -> list[Route]:
+ """Add custom endpoints to the standard protected resource routes."""
+
+ # Get the standard OAuth protected resource routes
+ routes = super().get_routes()
+
+ # Add authorization server metadata forwarding for client convenience
+ async def authorization_server_metadata(request):
+ async with httpx.AsyncClient() as client:
+ response = await client.get(
+ "https://auth.yourcompany.com/.well-known/oauth-authorization-server"
+ )
+ response.raise_for_status()
+ return JSONResponse(response.json())
+
+ routes.append(
+ Route("/.well-known/oauth-authorization-server", authorization_server_metadata)
+ )
+
+ return routes
+
+mcp = FastMCP(name="Company API", auth=CompanyAuthProvider())
+```
+
+This pattern uses `super().get_routes()` to get the standard protected resource routes, then adds additional endpoints as needed. A common use case is providing authorization server metadata forwarding, which allows MCP clients to discover your identity provider's capabilities through your MCP server rather than contacting the identity provider directly.
+
+## WorkOS AuthKit Integration
+
+WorkOS AuthKit provides an excellent example of remote OAuth integration. The `AuthKitProvider` demonstrates how to implement both token validation and OAuth metadata forwarding in a production-ready package.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.workos import AuthKitProvider
+
+auth = AuthKitProvider(
+ authkit_domain="https://your-project.authkit.app",
+ base_url="https://your-mcp-server.com"
+)
+
+mcp = FastMCP(name="Protected Application", auth=auth)
+```
+
+The `AuthKitProvider` automatically configures JWT validation against WorkOS's public keys and provides both protected resource metadata and authorization server metadata forwarding. This implementation handles the complete remote OAuth integration with minimal configuration.
+
+WorkOS's support for Dynamic Client Registration makes it particularly well-suited for MCP applications. Clients can automatically register themselves with your WorkOS project and obtain the credentials needed for authentication without manual intervention.
+
+→ **Complete WorkOS tutorial**: [AuthKit Integration Guide](/integrations/authkit)
+
+## Client Redirect URI Security
+
+
+`RemoteAuthProvider` also supports the `allowed_client_redirect_uris` parameter for controlling which redirect URIs are accepted from MCP clients during DCR:
+
+- `None` (default): Broad DCR-compatible redirect support, while rejecting unsafe browser schemes such as `javascript:`, `data:`, `file:`, and `vbscript:`
+- Custom list: Specify allowed patterns with wildcard support
+- Empty list `[]`: No redirect URIs allowed
+
+This provides defense-in-depth even though DCR providers typically validate redirect URIs themselves.
+
+
+## Implementation Considerations
+
+Remote OAuth integration requires careful attention to several technical details that affect reliability and security.
+
+**Token Validation Performance**: Your server validates every incoming token by checking signatures against your identity provider's public keys. Consider implementing key caching and rotation handling to minimize latency while maintaining security.
+
+**Error Handling**: Network issues with your identity provider can affect token validation. Implement appropriate timeouts, retry logic, and graceful degradation to maintain service availability during identity provider outages.
+
+**Audience Validation**: Ensure that tokens intended for your server are not accepted by other applications. Proper audience validation prevents token misuse across different services in your ecosystem.
+
+**Scope Management**: Map token scopes to your application's permission model consistently. Consider how scope changes affect existing tokens and plan for smooth permission updates.
+
+The complexity of these considerations reinforces why external identity providers are recommended over custom OAuth implementations. Established providers handle these technical details with extensive testing and operational experience.
diff --git a/docs/v3/servers/auth/token-verification.mdx b/docs/v3/servers/auth/token-verification.mdx
new file mode 100644
index 000000000..a9146135f
--- /dev/null
+++ b/docs/v3/servers/auth/token-verification.mdx
@@ -0,0 +1,426 @@
+---
+title: Token Verification
+sidebarTitle: Token Verification
+description: Protect your server by validating bearer tokens issued by external systems.
+icon: key
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Token verification enables your FastMCP server to validate bearer tokens issued by external systems without participating in user authentication flows. Your server acts as a pure resource server, focusing on token validation and authorization decisions while delegating identity management to other systems in your infrastructure.
+
+
+Token verification operates somewhat outside the formal MCP authentication flow, which expects OAuth-style discovery. It's best suited for internal systems, microservices architectures, or when you have full control over token generation and distribution.
+
+
+## Understanding Token Verification
+
+Token verification addresses scenarios where authentication responsibility is distributed across multiple systems. Your MCP server receives structured tokens containing identity and authorization information, validates their authenticity, and makes access control decisions based on their contents.
+
+This pattern emerges naturally in microservices architectures where a central authentication service issues tokens that multiple downstream services validate independently. It also works well when integrating MCP servers into existing systems that already have established token-based authentication mechanisms.
+
+### The Token Verification Model
+
+Token verification treats your MCP server as a resource server in OAuth terminology. The key insight is that token validation and token issuance are separate concerns that can be handled by different systems.
+
+**Token Issuance**: Another system (API gateway, authentication service, or identity provider) handles user authentication and creates signed tokens containing identity and permission information.
+
+**Token Validation**: Your MCP server receives these tokens, verifies their authenticity using cryptographic signatures, and extracts authorization information from their claims.
+
+**Access Control**: Based on token contents, your server determines what resources, tools, and prompts the client can access.
+
+This separation allows your MCP server to focus on its core functionality while leveraging existing authentication infrastructure. The token acts as a portable proof of identity that travels with each request.
+
+### Token Security Considerations
+
+Token-based authentication relies on cryptographic signatures to ensure token integrity. Your MCP server validates tokens using public keys corresponding to the private keys used for token creation. This asymmetric approach means your server never needs access to signing secrets.
+
+Token validation must address several security requirements: signature verification ensures tokens haven't been tampered with, expiration checking prevents use of stale tokens, and audience validation ensures tokens intended for your server aren't accepted by other systems.
+
+The challenge in MCP environments is that clients need to obtain valid tokens before making requests, but the MCP protocol doesn't provide built-in discovery mechanisms for token endpoints. Clients must obtain tokens through separate channels or prior configuration.
+
+
+## TokenVerifier Class
+
+FastMCP provides the `TokenVerifier` class to handle token validation complexity while remaining flexible about token sources and validation strategies.
+
+`TokenVerifier` focuses exclusively on token validation without providing OAuth discovery metadata. This makes it ideal for internal systems where clients already know how to obtain tokens, or for microservices that trust tokens from known issuers.
+
+The class validates token signatures, checks expiration timestamps, and extracts authorization information from token claims. It supports various token formats and validation strategies while maintaining a consistent interface for authorization decisions.
+
+You can subclass `TokenVerifier` to implement custom validation logic for specialized token formats or validation requirements. The base class handles common patterns while allowing extension for unique use cases.
+
+## JWT Token Verification
+
+JSON Web Tokens (JWTs) represent the most common token format for modern applications. FastMCP's `JWTVerifier` validates JWTs using industry-standard cryptographic techniques and claim validation.
+
+### JWKS Endpoint Integration
+
+JWKS endpoint integration provides the most flexible approach for production systems. The verifier automatically fetches public keys from a JSON Web Key Set endpoint, enabling automatic key rotation without server configuration changes.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+# Configure JWT verification against your identity provider
+verifier = JWTVerifier(
+ jwks_uri="https://auth.yourcompany.com/.well-known/jwks.json",
+ issuer="https://auth.yourcompany.com",
+ audience="mcp-production-api"
+)
+
+mcp = FastMCP(name="Protected API", auth=verifier)
+```
+
+This configuration creates a server that validates JWTs issued by `auth.yourcompany.com`. The verifier periodically fetches public keys from the JWKS endpoint and validates incoming tokens against those keys. Only tokens with the correct issuer and audience claims will be accepted.
+
+The `issuer` parameter ensures tokens come from your trusted authentication system, while `audience` validation prevents tokens intended for other services from being accepted by your MCP server.
+
+### Symmetric Key Verification (HMAC)
+
+Symmetric key verification uses a shared secret for both signing and validation, making it ideal for internal microservices and trusted environments where the same secret can be securely distributed to both token issuers and validators.
+
+This approach is commonly used in microservices architectures where services share a secret key, or when your authentication service and MCP server are both managed by the same organization. The HMAC algorithms (HS256, HS384, HS512) provide strong security when the shared secret is properly managed.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+# Use a shared secret for symmetric key verification
+verifier = JWTVerifier(
+ public_key="your-shared-secret-key-minimum-32-chars", # Despite the name, this accepts symmetric secrets
+ issuer="internal-auth-service",
+ audience="mcp-internal-api",
+ algorithm="HS256" # or HS384, HS512 for stronger security
+)
+
+mcp = FastMCP(name="Internal API", auth=verifier)
+```
+
+The verifier will validate tokens signed with the same secret using the specified HMAC algorithm. This approach offers several advantages for internal systems:
+
+- **Simplicity**: No key pair management or certificate distribution
+- **Performance**: HMAC operations are typically faster than RSA
+- **Compatibility**: Works well with existing microservice authentication patterns
+
+
+The parameter is named `public_key` for backwards compatibility, but when using HMAC algorithms (HS256/384/512), it accepts the symmetric secret string.
+
+
+
+**Security Considerations for Symmetric Keys:**
+- Use a strong, randomly generated secret (minimum 32 characters recommended)
+- Never expose the secret in logs, error messages, or version control
+- Implement secure key distribution and rotation mechanisms
+- Consider using asymmetric keys (RSA/ECDSA) for external-facing APIs
+
+
+### Static Public Key Verification
+
+Static public key verification works when you have a fixed RSA or ECDSA signing key and don't need automatic key rotation. This approach is primarily useful for development environments or controlled deployments where JWKS endpoints aren't available.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+# Use a static public key for token verification
+public_key_pem = """-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
+-----END PUBLIC KEY-----"""
+
+verifier = JWTVerifier(
+ public_key=public_key_pem,
+ issuer="https://auth.yourcompany.com",
+ audience="mcp-production-api"
+)
+
+mcp = FastMCP(name="Protected API", auth=verifier)
+```
+
+This configuration validates tokens using a specific RSA or ECDSA public key. The key must correspond to the private key used by your token issuer. While less flexible than JWKS endpoints, this approach can be useful in development environments or when testing with fixed keys.
+## Opaque Token Verification
+
+Many authorization servers issue opaque tokens rather than self-contained JWTs. Opaque tokens are random strings that carry no information themselves - the authorization server maintains their state and validation requires querying the server. FastMCP supports opaque token validation through OAuth 2.0 Token Introspection (RFC 7662).
+
+### Understanding Opaque Tokens
+
+Opaque tokens differ fundamentally from JWTs in their verification model. Where JWTs carry signed claims that can be validated locally, opaque tokens require network calls to the issuing authorization server for validation. The authorization server maintains token state and can revoke tokens immediately, providing stronger security guarantees for sensitive operations.
+
+This approach trades performance (network latency on each validation) for security and flexibility. Authorization servers can revoke opaque tokens instantly, implement complex authorization logic, and maintain detailed audit logs of token usage. Many enterprise OAuth providers default to opaque tokens for these security advantages.
+
+### Token Introspection Protocol
+
+RFC 7662 standardizes how resource servers validate opaque tokens. The protocol defines an introspection endpoint where resource servers authenticate using client credentials and receive token metadata including active status, scopes, expiration, and subject identity.
+
+FastMCP implements this protocol through the `IntrospectionTokenVerifier` class, handling authentication, request formatting, and response parsing according to the specification.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
+
+# Configure introspection with your OAuth provider
+verifier = IntrospectionTokenVerifier(
+ introspection_url="https://auth.yourcompany.com/oauth/introspect",
+ client_id="mcp-resource-server",
+ client_secret="your-client-secret",
+ required_scopes=["api:read", "api:write"]
+)
+
+mcp = FastMCP(name="Protected API", auth=verifier)
+```
+
+The verifier authenticates to the introspection endpoint using client credentials and queries it whenever a bearer token arrives. FastMCP checks whether the token is active and has sufficient scopes before allowing access.
+
+Two standard client authentication methods are supported, both defined in RFC 6749:
+
+- **`client_secret_basic`** (default): Sends credentials via HTTP Basic Auth header
+- **`client_secret_post`**: Sends credentials in the POST request body
+
+Most OAuth providers support both methods, though some may require one specifically. Configure the authentication method with the `client_auth_method` parameter:
+
+```python
+# Use POST body authentication instead of Basic Auth
+verifier = IntrospectionTokenVerifier(
+ introspection_url="https://auth.yourcompany.com/oauth/introspect",
+ client_id="mcp-resource-server",
+ client_secret="your-client-secret",
+ client_auth_method="client_secret_post",
+ required_scopes=["api:read", "api:write"]
+)
+```
+
+## Development and Testing
+
+Development environments often need simpler token management without the complexity of full JWT infrastructure. FastMCP provides tools specifically designed for these scenarios.
+
+### Static Token Verification
+
+Static token verification enables rapid development by accepting predefined tokens with associated claims. This approach eliminates the need for token generation infrastructure during development and testing.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.jwt import StaticTokenVerifier
+
+# Define development tokens and their associated claims
+verifier = StaticTokenVerifier(
+ tokens={
+ "dev-alice-token": {
+ "client_id": "alice@company.com",
+ "scopes": ["read:data", "write:data", "admin:users"]
+ },
+ "dev-guest-token": {
+ "client_id": "guest-user",
+ "scopes": ["read:data"]
+ }
+ },
+ required_scopes=["read:data"]
+)
+
+mcp = FastMCP(name="Development Server", auth=verifier)
+```
+
+Clients can now authenticate using `Authorization: Bearer dev-alice-token` headers. The server will recognize the token and load the associated claims for authorization decisions. This approach enables immediate development without external dependencies.
+
+
+Static token verification stores tokens as plain text and should never be used in production environments. It's designed exclusively for development and testing scenarios.
+
+
+
+### Debug/Custom Token Verification
+
+
+
+The `DebugTokenVerifier` provides maximum flexibility for testing and special cases where standard token verification isn't applicable. It delegates validation to a user-provided callable, making it useful for prototyping, testing scenarios, or handling opaque tokens without introspection endpoints.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.debug import DebugTokenVerifier
+
+# Accept all tokens (useful for rapid development)
+verifier = DebugTokenVerifier()
+
+mcp = FastMCP(name="Development Server", auth=verifier)
+```
+
+By default, `DebugTokenVerifier` accepts any non-empty token as valid. This eliminates authentication barriers during early development, allowing you to focus on core functionality before adding security.
+
+For more controlled testing, provide custom validation logic:
+
+```python
+from fastmcp.server.auth.providers.debug import DebugTokenVerifier
+
+# Synchronous validation - check token prefix
+verifier = DebugTokenVerifier(
+ validate=lambda token: token.startswith("dev-"),
+ client_id="development-client",
+ scopes=["read", "write"]
+)
+
+mcp = FastMCP(name="Development Server", auth=verifier)
+```
+
+The validation callable can also be async, enabling database lookups or external service calls:
+
+```python
+from fastmcp.server.auth.providers.debug import DebugTokenVerifier
+
+# Asynchronous validation - check against cache
+async def validate_token(token: str) -> bool:
+ # Check if token exists in Redis, database, etc.
+ return await redis.exists(f"valid_tokens:{token}")
+
+verifier = DebugTokenVerifier(
+ validate=validate_token,
+ client_id="api-client",
+ scopes=["api:access"]
+)
+
+mcp = FastMCP(name="Custom API", auth=verifier)
+```
+
+**Use Cases:**
+
+- **Testing**: Accept any token during integration tests without setting up token infrastructure
+- **Prototyping**: Quickly validate concepts without authentication complexity
+- **Opaque tokens without introspection**: When you have tokens from an IDP that provides no introspection endpoint, and you're willing to accept tokens without validation (validation happens later at the upstream service)
+- **Custom token formats**: Implement validation for non-standard token formats or legacy systems
+
+
+`DebugTokenVerifier` bypasses standard security checks. Only use in controlled environments (development, testing) or when you fully understand the security implications. For production, use proper JWT or introspection-based verification.
+
+
+### Test Token Generation
+
+Test token generation helps when you need to test JWT verification without setting up complete identity infrastructure. FastMCP includes utilities for generating test key pairs and signed tokens.
+
+```python
+from fastmcp.server.auth.providers.jwt import JWTVerifier, RSAKeyPair
+
+# Generate a key pair for testing
+key_pair = RSAKeyPair.generate()
+
+# Configure your server with the public key
+verifier = JWTVerifier(
+ public_key=key_pair.public_key,
+ issuer="https://test.yourcompany.com",
+ audience="test-mcp-server"
+)
+
+# Generate a test token using the private key
+test_token = key_pair.create_token(
+ subject="test-user-123",
+ issuer="https://test.yourcompany.com",
+ audience="test-mcp-server",
+ scopes=["read", "write", "admin"]
+)
+
+print(f"Test token: {test_token}")
+```
+
+This pattern enables comprehensive testing of JWT validation logic without depending on external token issuers. The generated tokens are cryptographically valid and will pass all standard JWT validation checks.
+
+## HTTP Client Customization
+
+
+
+All token verifiers that make HTTP calls accept an optional `http_client` parameter. This lets you provide your own `httpx.AsyncClient` for connection pooling, custom TLS configuration, or proxy settings.
+
+### Connection Pooling
+
+By default, each token verification call creates a fresh HTTP client. Under high load, this means repeated TCP connections and TLS handshakes. Providing a shared client enables connection pooling across calls:
+
+```python
+import httpx
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
+
+# Create a shared client with connection pooling
+http_client = httpx.AsyncClient(
+ timeout=10,
+ limits=httpx.Limits(max_connections=20, max_keepalive_connections=10),
+)
+
+verifier = IntrospectionTokenVerifier(
+ introspection_url="https://auth.yourcompany.com/oauth/introspect",
+ client_id="mcp-resource-server",
+ client_secret="your-client-secret",
+ http_client=http_client,
+)
+
+mcp = FastMCP(name="Protected API", auth=verifier)
+```
+
+The same pattern works for `JWTVerifier` when using JWKS endpoints:
+
+```python
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+verifier = JWTVerifier(
+ jwks_uri="https://auth.yourcompany.com/.well-known/jwks.json",
+ issuer="https://auth.yourcompany.com",
+ http_client=http_client,
+)
+```
+
+
+`JWTVerifier` does not support `http_client` when `ssrf_safe=True`. SSRF-safe mode requires a hardened transport that validates DNS resolution and connection targets, which cannot be guaranteed with a user-provided client. Attempting to use both will raise a `ValueError`.
+
+
+
+When you provide an `http_client`, you are responsible for its lifecycle. The verifier will not close it. Use the server's `lifespan` to manage client cleanup:
+
+```python
+from contextlib import asynccontextmanager
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.introspection import IntrospectionTokenVerifier
+
+http_client = httpx.AsyncClient(timeout=10)
+
+verifier = IntrospectionTokenVerifier(
+ introspection_url="https://auth.example.com/introspect",
+ client_id="my-service",
+ client_secret="secret",
+ http_client=http_client,
+)
+
+@asynccontextmanager
+async def lifespan(app):
+ yield
+ await http_client.aclose()
+
+mcp = FastMCP(name="My API", auth=verifier, lifespan=lifespan)
+```
+
+
+The convenience providers (`GitHubProvider`, `GoogleProvider`, `DiscordProvider`, `WorkOSProvider`, `AzureProvider`) also accept `http_client` and pass it through to their internal token verifier.
+
+## Production Configuration
+
+For production deployments, load sensitive configuration from environment variables:
+
+```python
+import os
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+# Load configuration from environment variables
+# Parse comma-separated scopes if provided
+scopes_env = os.environ.get("JWT_REQUIRED_SCOPES")
+required_scopes = scopes_env.split(",") if scopes_env else None
+
+verifier = JWTVerifier(
+ jwks_uri=os.environ.get("JWT_JWKS_URI"),
+ issuer=os.environ.get("JWT_ISSUER"),
+ audience=os.environ.get("JWT_AUDIENCE"),
+ required_scopes=required_scopes,
+)
+
+mcp = FastMCP(name="Production API", auth=verifier)
+```
+
+This keeps configuration out of your codebase while maintaining explicit setup.
+
+This approach enables the same codebase to run across development, staging, and production environments with different authentication requirements. Development might use static tokens while production uses JWT verification, all controlled through environment configuration.
+
diff --git a/docs/v3/servers/authorization.mdx b/docs/v3/servers/authorization.mdx
new file mode 100644
index 000000000..a48d2a9e8
--- /dev/null
+++ b/docs/v3/servers/authorization.mdx
@@ -0,0 +1,384 @@
+---
+title: Authorization
+sidebarTitle: Authorization
+description: Control access to components using callable-based authorization checks that filter visibility and enforce permissions.
+icon: shield-halved
+tag: NEW
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Authorization controls what authenticated users can do with your FastMCP server. While [authentication](/servers/auth/authentication) verifies identity (who you are), authorization determines access (what you can do). FastMCP provides a callable-based authorization system that works at both the component level and globally via middleware.
+
+The authorization model centers on a simple concept: callable functions that receive context about the current request and return `True` to allow access or `False` to deny it. Multiple checks combine with AND logic, meaning all checks must pass for access to be granted.
+
+
+Authorization relies on OAuth tokens which are only available with HTTP transports (SSE, Streamable HTTP). In STDIO mode, there's no OAuth mechanism, so `get_access_token()` returns `None` and all auth checks are skipped.
+
+
+
+When an `AuthProvider` is configured, all requests to the MCP endpoint must carry a valid token—unauthenticated requests are rejected at the transport level before any auth checks run. Authorization checks therefore differentiate between authenticated users based on their scopes and claims, not between authenticated and unauthenticated users.
+
+
+## Auth Checks
+
+An auth check is any callable that accepts an `AuthContext` and returns a boolean. Auth checks can be synchronous or asynchronous, so checks that need to perform async operations (like reading server state or calling external services) work naturally.
+
+```python
+from fastmcp.server.auth import AuthContext
+
+def my_custom_check(ctx: AuthContext) -> bool:
+ # ctx.token is AccessToken | None
+ # ctx.component is the Tool, Resource, or Prompt being accessed
+ return ctx.token is not None and "special" in ctx.token.scopes
+```
+
+FastMCP provides two built-in auth checks that cover common authorization patterns.
+
+### require_scopes
+
+Scope-based authorization checks that the token contains all specified OAuth scopes. When multiple scopes are provided, all must be present (AND logic).
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import require_scopes
+
+mcp = FastMCP("Scoped Server")
+
+@mcp.tool(auth=require_scopes("admin"))
+def admin_operation() -> str:
+ """Requires the 'admin' scope."""
+ return "Admin action completed"
+
+@mcp.tool(auth=require_scopes("read", "write"))
+def read_write_operation() -> str:
+ """Requires both 'read' AND 'write' scopes."""
+ return "Read/write action completed"
+```
+
+### restrict_tag
+
+Tag-based restrictions apply scope requirements conditionally. If a component has the specified tag, the token must have the required scopes. Components without the tag are unaffected.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import restrict_tag
+from fastmcp.server.middleware import AuthMiddleware
+
+mcp = FastMCP(
+ "Tagged Server",
+ middleware=[
+ AuthMiddleware(auth=restrict_tag("admin", scopes=["admin"]))
+ ]
+)
+
+@mcp.tool(tags={"admin"})
+def admin_tool() -> str:
+ """Tagged 'admin', so requires 'admin' scope."""
+ return "Admin only"
+
+@mcp.tool(tags={"public"})
+def public_tool() -> str:
+ """Not tagged 'admin', so no scope required by the restriction."""
+ return "Anyone can access"
+```
+
+### Combining Checks
+
+Multiple auth checks can be combined by passing a list. All checks must pass for authorization to succeed (AND logic).
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import require_scopes
+
+mcp = FastMCP("Combined Auth Server")
+
+@mcp.tool(auth=[require_scopes("admin"), require_scopes("write")])
+def secure_admin_action() -> str:
+ """Requires both 'admin' AND 'write' scopes."""
+ return "Secure admin action"
+```
+
+### Custom Auth Checks
+
+Any callable that accepts `AuthContext` and returns `bool` can serve as an auth check. This enables authorization logic based on token claims, component metadata, or external systems.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import AuthContext
+
+mcp = FastMCP("Custom Auth Server")
+
+def require_premium_user(ctx: AuthContext) -> bool:
+ """Check for premium user status in token claims."""
+ if ctx.token is None:
+ return False
+ return ctx.token.claims.get("premium", False) is True
+
+def require_access_level(minimum_level: int):
+ """Factory function for level-based authorization."""
+ def check(ctx: AuthContext) -> bool:
+ if ctx.token is None:
+ return False
+ user_level = ctx.token.claims.get("level", 0)
+ return user_level >= minimum_level
+ return check
+
+@mcp.tool(auth=require_premium_user)
+def premium_feature() -> str:
+ """Only for premium users."""
+ return "Premium content"
+
+@mcp.tool(auth=require_access_level(5))
+def advanced_feature() -> str:
+ """Requires access level 5 or higher."""
+ return "Advanced feature"
+```
+
+### Async Auth Checks
+
+Auth checks can be `async` functions, which is useful when the authorization decision depends on asynchronous operations like reading server state or querying external services.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import AuthContext
+
+mcp = FastMCP("Async Auth Server")
+
+async def check_user_permissions(ctx: AuthContext) -> bool:
+ """Async auth check that reads server state."""
+ if ctx.token is None:
+ return False
+ user_id = ctx.token.claims.get("sub")
+ # Async operations work naturally in auth checks
+ permissions = await fetch_user_permissions(user_id)
+ return "admin" in permissions
+
+@mcp.tool(auth=check_user_permissions)
+def admin_tool() -> str:
+ return "Admin action completed"
+```
+
+Sync and async checks can be freely combined in a list — each check is handled according to its type.
+
+### Error Handling
+
+Auth checks can raise exceptions for explicit denial with custom messages:
+
+- **`AuthorizationError`**: Propagates with its custom message, useful for explaining why access was denied
+- **Other exceptions**: Masked for security (logged internally, treated as denial)
+
+```python
+from fastmcp.server.auth import AuthContext
+from fastmcp.exceptions import AuthorizationError
+
+def require_verified_email(ctx: AuthContext) -> bool:
+ """Require verified email with explicit denial message."""
+ if ctx.token is None:
+ raise AuthorizationError("Authentication required")
+ if not ctx.token.claims.get("email_verified"):
+ raise AuthorizationError("Email verification required")
+ return True
+```
+
+## Component-Level Authorization
+
+The `auth` parameter on decorators controls visibility and access for individual components. When auth checks fail for the current request, the component is hidden from list responses and direct access returns not-found.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import require_scopes
+
+mcp = FastMCP("Component Auth Server")
+
+@mcp.tool(auth=require_scopes("write"))
+def write_tool() -> str:
+ """Only visible to users with 'write' scope."""
+ return "Written"
+
+@mcp.resource("secret://data", auth=require_scopes("read"))
+def secret_resource() -> str:
+ """Only visible to users with 'read' scope."""
+ return "Secret data"
+
+@mcp.prompt(auth=require_scopes("admin"))
+def admin_prompt() -> str:
+ """Only visible to users with 'admin' scope."""
+ return "Admin prompt content"
+```
+
+
+Component-level `auth` controls both visibility (list filtering) and access (direct lookups return not-found for unauthorized requests). Additionally use `AuthMiddleware` to apply server-wide authorization rules and get explicit `AuthorizationError` responses on unauthorized execution attempts.
+
+
+## Server-Level Authorization
+
+For server-wide authorization enforcement, use `AuthMiddleware`. This middleware applies auth checks globally to all components—filtering list responses and blocking unauthorized execution with explicit `AuthorizationError` responses.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import require_scopes
+from fastmcp.server.middleware import AuthMiddleware
+
+mcp = FastMCP(
+ "Enforced Auth Server",
+ middleware=[AuthMiddleware(auth=require_scopes("api"))]
+)
+
+@mcp.tool
+def any_tool() -> str:
+ """Requires 'api' scope to see AND call."""
+ return "Protected"
+```
+
+### Component Auth + Middleware
+
+Component-level `auth` and `AuthMiddleware` work together as complementary layers. The middleware applies server-wide rules to all components, while component-level auth adds per-component requirements. Both layers are checked—all checks must pass.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import require_scopes, restrict_tag
+from fastmcp.server.middleware import AuthMiddleware
+
+mcp = FastMCP(
+ "Layered Auth Server",
+ middleware=[
+ AuthMiddleware(auth=restrict_tag("admin", scopes=["admin"]))
+ ]
+)
+
+# Requires "write" scope (component-level)
+# Also requires "admin" scope if tagged "admin" (middleware-level)
+@mcp.tool(auth=require_scopes("write"), tags={"admin"})
+def admin_write() -> str:
+ """Requires both 'write' AND 'admin' scopes."""
+ return "Admin write"
+
+# Requires "write" scope (component-level only)
+@mcp.tool(auth=require_scopes("write"))
+def user_write() -> str:
+ """Requires 'write' scope."""
+ return "User write"
+```
+
+### Tag-Based Global Authorization
+
+A common pattern uses `restrict_tag` with `AuthMiddleware` to apply scope requirements based on component tags.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import restrict_tag
+from fastmcp.server.middleware import AuthMiddleware
+
+mcp = FastMCP(
+ "Tag-Based Auth Server",
+ middleware=[
+ AuthMiddleware(auth=restrict_tag("admin", scopes=["admin"])),
+ AuthMiddleware(auth=restrict_tag("write", scopes=["write"])),
+ ]
+)
+
+@mcp.tool(tags={"admin"})
+def delete_all_data() -> str:
+ """Requires 'admin' scope."""
+ return "Deleted"
+
+@mcp.tool(tags={"write"})
+def update_record(id: str, data: str) -> str:
+ """Requires 'write' scope."""
+ return f"Updated {id}"
+
+@mcp.tool
+def read_record(id: str) -> str:
+ """No tag restrictions, accessible to all."""
+ return f"Record {id}"
+```
+
+## Accessing Tokens in Tools
+
+Tools can access the current authentication token using `get_access_token()` from `fastmcp.server.dependencies`. This enables tools to make decisions based on user identity or permissions beyond simple authorization checks.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.dependencies import get_access_token
+
+mcp = FastMCP("Token Access Server")
+
+@mcp.tool
+def personalized_greeting() -> str:
+ """Greet the user based on their token claims."""
+ token = get_access_token()
+
+ if token is None:
+ return "Hello, guest!"
+
+ name = token.claims.get("name", "user")
+ return f"Hello, {name}!"
+
+@mcp.tool
+def user_dashboard() -> dict:
+ """Return user-specific data based on token."""
+ token = get_access_token()
+
+ if token is None:
+ return {"error": "Not authenticated"}
+
+ return {
+ "client_id": token.client_id,
+ "scopes": token.scopes,
+ "claims": token.claims,
+ }
+```
+
+## Reference
+
+### AccessToken
+
+The `AccessToken` object contains information extracted from the OAuth token.
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `token` | `str` | The raw token string |
+| `client_id` | `str \| None` | OAuth client identifier |
+| `scopes` | `list[str]` | Granted OAuth scopes |
+| `expires_at` | `datetime \| None` | Token expiration time |
+| `claims` | `dict[str, Any]` | All JWT claims or custom token data |
+
+### AuthContext
+
+The `AuthContext` dataclass is passed to all auth check functions.
+
+| Property | Type | Description |
+|----------|------|-------------|
+| `token` | `AccessToken \| None` | Current access token, or `None` if unauthenticated |
+| `component` | `Tool \| Resource \| Prompt` | The component being accessed |
+
+Access to the component object enables authorization decisions based on metadata like tags, name, or custom properties.
+
+```python
+from fastmcp.server.auth import AuthContext
+
+def require_matching_tag(ctx: AuthContext) -> bool:
+ """Require a scope matching each of the component's tags."""
+ if ctx.token is None:
+ return False
+ user_scopes = set(ctx.token.scopes)
+ return ctx.component.tags.issubset(user_scopes)
+```
+
+### Imports
+
+```python
+from fastmcp.server.auth import (
+ AccessToken, # Token with .token, .client_id, .scopes, .expires_at, .claims
+ AuthContext, # Context with .token, .component
+ AuthCheck, # Type alias: sync or async Callable[[AuthContext], bool]
+ require_scopes, # Built-in: requires specific scopes
+ restrict_tag, # Built-in: tag-based scope requirements
+ run_auth_checks, # Utility: run checks with AND logic
+)
+
+from fastmcp.server.middleware import AuthMiddleware
+```
diff --git a/docs/v3/servers/composition.mdx b/docs/v3/servers/composition.mdx
new file mode 100644
index 000000000..42523a5cd
--- /dev/null
+++ b/docs/v3/servers/composition.mdx
@@ -0,0 +1,237 @@
+---
+title: Composing Servers
+sidebarTitle: Composition
+description: Combine multiple servers into one
+icon: puzzle-piece
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+As your application grows, you'll want to split it into focused servers — one for weather, one for calendar, one for admin — and combine them into a single server that clients connect to. That's what `mount()` does.
+
+When you mount a server, all its tools, resources, and prompts become available through the parent. The connection is live: add a tool to the child after mounting, and it's immediately visible through the parent.
+
+```python
+from fastmcp import FastMCP
+
+weather = FastMCP("Weather")
+
+@weather.tool
+def get_forecast(city: str) -> str:
+ """Get weather forecast for a city."""
+ return f"Sunny in {city}"
+
+@weather.resource("data://cities")
+def list_cities() -> list[str]:
+ """List supported cities."""
+ return ["London", "Paris", "Tokyo"]
+
+main = FastMCP("MainApp")
+main.mount(weather)
+
+# main now serves get_forecast and data://cities
+```
+
+## Mounting External Servers
+
+Mount remote HTTP servers or subprocess-based MCP servers using `create_proxy()`:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server import create_proxy
+
+mcp = FastMCP("Orchestrator")
+
+# Mount a remote HTTP server (URLs work directly)
+mcp.mount(create_proxy("http://api.example.com/mcp"), namespace="api")
+
+# Mount local Python scripts (file paths work directly)
+mcp.mount(create_proxy("./my_server.py"), namespace="local")
+```
+
+### Mounting npm/uvx Packages
+
+For npm packages or Python tools, use the config dict format:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server import create_proxy
+
+mcp = FastMCP("Orchestrator")
+
+# Mount npm package via config
+github_config = {
+ "mcpServers": {
+ "default": {
+ "command": "npx",
+ "args": ["-y", "@modelcontextprotocol/server-github"]
+ }
+ }
+}
+mcp.mount(create_proxy(github_config), namespace="github")
+
+# Mount Python tool via config
+sqlite_config = {
+ "mcpServers": {
+ "default": {
+ "command": "uvx",
+ "args": ["mcp-server-sqlite", "--db", "data.db"]
+ }
+ }
+}
+mcp.mount(create_proxy(sqlite_config), namespace="db")
+```
+
+Or use explicit transport classes:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server import create_proxy
+from fastmcp.client.transports import NpxStdioTransport, UvxStdioTransport
+
+mcp = FastMCP("Orchestrator")
+
+mcp.mount(
+ create_proxy(NpxStdioTransport(package="@modelcontextprotocol/server-github")),
+ namespace="github"
+)
+mcp.mount(
+ create_proxy(UvxStdioTransport(tool_name="mcp-server-sqlite", tool_args=["--db", "data.db"])),
+ namespace="db"
+)
+```
+
+For advanced configuration, see [Proxying](/servers/providers/proxy).
+
+## Namespacing
+
+
+
+When mounting multiple servers, use namespaces to avoid naming conflicts:
+
+```python
+weather = FastMCP("Weather")
+calendar = FastMCP("Calendar")
+
+@weather.tool
+def get_data() -> str:
+ return "Weather data"
+
+@calendar.tool
+def get_data() -> str:
+ return "Calendar data"
+
+main = FastMCP("Main")
+main.mount(weather, namespace="weather")
+main.mount(calendar, namespace="calendar")
+
+# Tools are now:
+# - weather_get_data
+# - calendar_get_data
+```
+
+### How Namespacing Works
+
+| Component Type | Without Namespace | With `namespace="api"` |
+|----------------|-------------------|------------------------|
+| Tool | `my_tool` | `api_my_tool` |
+| Prompt | `my_prompt` | `api_my_prompt` |
+| Resource | `data://info` | `data://api/info` |
+| Template | `data://{id}` | `data://api/{id}` |
+
+Namespacing uses [transforms](/servers/transforms/transforms) under the hood.
+
+## Dynamic Composition
+
+Because `mount()` creates a live link, you can add components to a child server after mounting and they'll be immediately available through the parent:
+
+```python
+main = FastMCP("Main")
+main.mount(dynamic_server, namespace="dynamic")
+
+# Add a tool AFTER mounting - it's accessible through main
+@dynamic_server.tool
+def added_later() -> str:
+ return "Added after mounting!"
+```
+
+## Tag Filtering
+
+
+
+Parent server tag filters apply recursively to mounted servers:
+
+```python
+api_server = FastMCP("API")
+
+@api_server.tool(tags={"production"})
+def prod_endpoint() -> str:
+ return "Production data"
+
+@api_server.tool(tags={"development"})
+def dev_endpoint() -> str:
+ return "Debug data"
+
+# Mount with production filter
+prod_app = FastMCP("Production")
+prod_app.mount(api_server, namespace="api")
+prod_app.enable(tags={"production"}, only=True)
+
+# Only prod_endpoint (namespaced as api_prod_endpoint) is visible
+```
+
+## Performance Considerations
+
+Operations like `list_tools()` on the parent are affected by the performance of all mounted servers. This is particularly noticeable with:
+
+- HTTP-based mounted servers (300-400ms vs 1-2ms for local tools)
+- Mounted servers with slow initialization
+- Deep mounting hierarchies
+
+If low latency is critical, consider implementing caching strategies or limiting mounting depth.
+
+## Custom Routes
+
+
+
+Custom HTTP routes defined with `@server.custom_route()` are also forwarded when mounting:
+
+```python
+subserver = FastMCP("Sub")
+
+@subserver.custom_route("/health", methods=["GET"])
+async def health_check():
+ return {"status": "ok"}
+
+main = FastMCP("Main")
+main.mount(subserver, namespace="sub")
+
+# /health is now accessible through main's HTTP app
+```
+
+## Conflict Resolution
+
+
+
+When mounting multiple servers with the same namespace (or no namespace), the **most recently mounted** server takes precedence for conflicting component names:
+
+```python
+server_a = FastMCP("A")
+server_b = FastMCP("B")
+
+@server_a.tool
+def shared_tool() -> str:
+ return "From A"
+
+@server_b.tool
+def shared_tool() -> str:
+ return "From B"
+
+main = FastMCP("Main")
+main.mount(server_a)
+main.mount(server_b)
+
+# shared_tool returns "From B" (most recently mounted)
+```
diff --git a/docs/v3/servers/context.mdx b/docs/v3/servers/context.mdx
new file mode 100644
index 000000000..d442743ab
--- /dev/null
+++ b/docs/v3/servers/context.mdx
@@ -0,0 +1,480 @@
+---
+title: MCP Context
+sidebarTitle: Context
+description: Access MCP capabilities like logging, progress, and resources within your MCP objects.
+icon: rectangle-code
+tag: NEW
+---
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+When defining FastMCP [tools](/servers/tools), [resources](/servers/resources), resource templates, or [prompts](/servers/prompts), your functions might need to interact with the underlying MCP session or access advanced server capabilities. FastMCP provides the `Context` object for this purpose.
+
+
+You access Context through FastMCP's dependency injection system. For other injectable values like HTTP requests, access tokens, and custom dependencies, see [Dependency Injection](/servers/dependency-injection).
+
+
+## What Is Context?
+
+The `Context` object provides a clean interface to access MCP features within your functions, including:
+
+- **Logging**: Send debug, info, warning, and error messages back to the client
+- **Progress Reporting**: Update the client on the progress of long-running operations
+- **Resource Access**: List and read data from resources registered with the server
+- **Prompt Access**: List and retrieve prompts registered with the server
+- **LLM Sampling**: Request the client's LLM to generate text based on provided messages
+- **User Elicitation**: Request structured input from users during tool execution
+- **Session State**: Store data that persists across requests within an MCP session
+- **Session Visibility**: [Control which components are visible](/servers/visibility#per-session-visibility) to the current session
+- **Request Information**: Access metadata about the current request
+- **Server Access**: When needed, access the underlying FastMCP server instance
+
+## Accessing the Context
+
+
+
+The preferred way to access context is using the `CurrentContext()` dependency:
+
+```python {1, 6}
+from fastmcp import FastMCP
+from fastmcp.dependencies import CurrentContext
+from fastmcp.server.context import Context
+
+mcp = FastMCP(name="Context Demo")
+
+@mcp.tool
+async def process_file(file_uri: str, ctx: Context = CurrentContext()) -> str:
+ """Processes a file, using context for logging and resource access."""
+ await ctx.info(f"Processing {file_uri}")
+ return "Processed file"
+```
+
+This works with tools, resources, and prompts:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.dependencies import CurrentContext
+from fastmcp.server.context import Context
+
+mcp = FastMCP(name="Context Demo")
+
+@mcp.resource("resource://user-data")
+async def get_user_data(ctx: Context = CurrentContext()) -> dict:
+ await ctx.debug("Fetching user data")
+ return {"user_id": "example"}
+
+@mcp.prompt
+async def data_analysis_request(dataset: str, ctx: Context = CurrentContext()) -> str:
+ return f"Please analyze the following dataset: {dataset}"
+```
+
+**Key Points:**
+
+- Dependency parameters are automatically excluded from the MCP schema—clients never see them.
+- Context methods are async, so your function usually needs to be async as well.
+- **Each MCP request receives a new context object.** Context is scoped to a single request; state or data set in one request will not be available in subsequent requests.
+- Context is only available during a request; attempting to use context methods outside a request will raise errors.
+
+### Legacy Type-Hint Injection
+
+For backwards compatibility, you can still access context by simply adding a parameter with the `Context` type hint. FastMCP will automatically inject the context instance:
+
+```python {1, 6}
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP(name="Context Demo")
+
+@mcp.tool
+async def process_file(file_uri: str, ctx: Context) -> str:
+ """Processes a file, using context for logging and resource access."""
+ # Context is injected automatically based on the type hint
+ return "Processed file"
+```
+
+This approach still works for tools, resources, and prompts. The parameter name doesn't matter—only the `Context` type hint is important. The type hint can also be a union (`Context | None`) or use `Annotated[]`.
+
+### Via `get_context()` Function
+
+
+
+For code nested deeper within your function calls where passing context through parameters is inconvenient, use `get_context()` to retrieve the active context from anywhere within a request's execution flow:
+
+```python {2,9}
+from fastmcp import FastMCP
+from fastmcp.server.dependencies import get_context
+
+mcp = FastMCP(name="Dependency Demo")
+
+# Utility function that needs context but doesn't receive it as a parameter
+async def process_data(data: list[float]) -> dict:
+ # Get the active context - only works when called within a request
+ ctx = get_context()
+ await ctx.info(f"Processing {len(data)} data points")
+
+@mcp.tool
+async def analyze_dataset(dataset_name: str) -> dict:
+ # Call utility function that uses context internally
+ data = load_data(dataset_name)
+ await process_data(data)
+```
+
+**Important Notes:**
+
+- The `get_context()` function should only be used within the context of a server request. Calling it outside of a request will raise a `RuntimeError`.
+- The `get_context()` function is server-only and should not be used in client code.
+
+## Context Capabilities
+
+FastMCP provides several advanced capabilities through the context object. Each capability has dedicated documentation with comprehensive examples and best practices:
+
+### Logging
+
+Send debug, info, warning, and error messages back to the MCP client for visibility into function execution.
+
+```python
+await ctx.debug("Starting analysis")
+await ctx.info(f"Processing {len(data)} items")
+await ctx.warning("Deprecated parameter used")
+await ctx.error("Processing failed")
+```
+
+See [Server Logging](/servers/logging) for complete documentation and examples.
+### Client Elicitation
+
+
+
+Request structured input from clients during tool execution, enabling interactive workflows and progressive disclosure. This is a new feature in the 6/18/2025 MCP spec.
+
+```python
+result = await ctx.elicit("Enter your name:", response_type=str)
+if result.action == "accept":
+ name = result.data
+```
+
+See [User Elicitation](/servers/elicitation) for detailed examples and supported response types.
+
+### LLM Sampling
+
+
+
+Request the client's LLM to generate text based on provided messages, useful for leveraging AI capabilities within your tools.
+
+```python
+response = await ctx.sample("Analyze this data", temperature=0.7)
+```
+
+See [LLM Sampling](/servers/sampling) for comprehensive usage and advanced techniques.
+
+
+### Progress Reporting
+
+Update clients on the progress of long-running operations, enabling progress indicators and better user experience.
+
+```python
+await ctx.report_progress(progress=50, total=100) # 50% complete
+```
+
+See [Progress Reporting](/servers/progress) for detailed patterns and examples.
+
+### Resource Access
+
+List and read data from resources registered with your FastMCP server, allowing access to files, configuration, or dynamic content.
+
+```python
+# List available resources
+resources = await ctx.list_resources()
+
+# Read a specific resource
+content_list = await ctx.read_resource("resource://config")
+content = content_list[0].content
+```
+
+**Method signatures:**
+- **`ctx.list_resources() -> list[MCPResource]`**: Returns list of all available resources
+- **`ctx.read_resource(uri: str | AnyUrl) -> list[ReadResourceContents]`**: Returns a list of resource content parts
+
+### Prompt Access
+
+
+
+List and retrieve prompts registered with your FastMCP server, allowing tools and middleware to discover and use available prompts programmatically.
+
+```python
+# List available prompts
+prompts = await ctx.list_prompts()
+
+# Get a specific prompt with arguments
+result = await ctx.get_prompt("analyze_data", {"dataset": "users"})
+messages = result.messages
+```
+
+**Method signatures:**
+- **`ctx.list_prompts() -> list[MCPPrompt]`**: Returns list of all available prompts
+- **`ctx.get_prompt(name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult`**: Get a specific prompt with optional arguments
+
+### Session State
+
+
+
+Store data that persists across multiple requests within the same MCP session. Session state is automatically keyed by the client's session, ensuring isolation between different clients.
+
+```python
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP("stateful-app")
+
+@mcp.tool
+async def increment_counter(ctx: Context) -> int:
+ """Increment a counter that persists across tool calls."""
+ count = await ctx.get_state("counter") or 0
+ await ctx.set_state("counter", count + 1)
+ return count + 1
+
+@mcp.tool
+async def get_counter(ctx: Context) -> int:
+ """Get the current counter value."""
+ return await ctx.get_state("counter") or 0
+```
+
+Each client session has its own isolated state—two different clients calling `increment_counter` will each have their own counter.
+
+**Method signatures:**
+- **`await ctx.set_state(key, value, *, serializable=True)`**: Store a value in session state
+- **`await ctx.get_state(key)`**: Retrieve a value (returns None if not found)
+- **`await ctx.delete_state(key)`**: Remove a value from session state
+
+
+State methods are async and require `await`. State expires after 1 day to prevent unbounded memory growth.
+
+
+#### Non-Serializable Values
+
+By default, state values must be JSON-serializable (dicts, lists, strings, numbers, etc.) so they can be persisted across requests. For non-serializable values like HTTP clients or database connections, pass `serializable=False`:
+
+```python
+@mcp.tool
+async def my_tool(ctx: Context) -> str:
+ # This object can't be JSON-serialized
+ client = SomeHTTPClient(base_url="https://api.example.com")
+ await ctx.set_state("client", client, serializable=False)
+
+ # Retrieve it later in the same request
+ client = await ctx.get_state("client")
+ return await client.fetch("/data")
+```
+
+Values stored with `serializable=False` only live for the current MCP request (a single tool call, resource read, or prompt render). They will not be available in subsequent requests within the session.
+
+#### Custom Storage Backends
+
+By default, session state uses an in-memory store suitable for single-server deployments. For distributed or serverless deployments, provide a custom storage backend:
+
+```python
+from key_value.aio.stores.redis import RedisStore
+
+# Use Redis for distributed state
+mcp = FastMCP("distributed-app", session_state_store=RedisStore(...))
+```
+
+Any backend compatible with the [py-key-value-aio](https://github.com/strawgate/py-key-value) `AsyncKeyValue` protocol works. See [Storage Backends](/servers/storage-backends) for more options including Redis, DynamoDB, and MongoDB.
+
+#### State and Mounted Servers
+
+Each `FastMCP` instance has its own session state store. When you `mount()` a child server, state set on the parent is not visible to tools on the child, and vice versa:
+
+```python
+from fastmcp import FastMCP, Context
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+
+parent = FastMCP("Parent")
+child = FastMCP("Child")
+parent.mount(child, namespace="child")
+
+class Stasher(Middleware):
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
+ await context.fastmcp_context.set_state("user", "alice")
+ return await call_next(context)
+
+parent.add_middleware(Stasher())
+
+@child.tool
+async def whoami(ctx: Context) -> str:
+ return await ctx.get_state("user") or "unknown" # returns "unknown"
+```
+
+To share state across the mount boundary, pass the same store to both servers:
+
+```python
+from key_value.aio.stores.memory import MemoryStore
+
+store = MemoryStore()
+parent = FastMCP("Parent", session_state_store=store)
+child = FastMCP("Child", session_state_store=store)
+parent.mount(child, namespace="child")
+```
+
+Alternatively, state set with `serializable=False` lives on the request context and is inherited by mounted children automatically — use it when the value is request-scoped and does not need to persist across tool calls.
+
+#### State During Initialization
+
+State set during `on_initialize` middleware persists to subsequent tool calls when using the same session object (STDIO, SSE, single-server HTTP). For distributed/serverless HTTP deployments where different machines handle init and tool calls, state is isolated by the `mcp-session-id` header.
+
+### Session Visibility
+
+
+
+Tools can customize which components are visible to their current session using `ctx.enable_components()`, `ctx.disable_components()`, and `ctx.reset_visibility()`. These methods apply visibility rules that affect only the calling session, leaving other sessions unchanged. See [Per-Session Visibility](/servers/visibility#per-session-visibility) for complete documentation, filter criteria, and patterns like namespace activation.
+
+### Change Notifications
+
+
+
+FastMCP automatically sends list change notifications when components (such as tools, resources, or prompts) are added, removed, enabled, or disabled. In rare cases where you need to manually trigger these notifications, you can use the context's notification methods:
+
+```python
+import mcp.types
+
+@mcp.tool
+async def custom_tool_management(ctx: Context) -> str:
+ """Example of manual notification after custom tool changes."""
+ await ctx.send_notification(mcp.types.ToolListChangedNotification())
+ await ctx.send_notification(mcp.types.ResourceListChangedNotification())
+ await ctx.send_notification(mcp.types.PromptListChangedNotification())
+ return "Notifications sent"
+```
+
+These methods are primarily used internally by FastMCP's automatic notification system and most users will not need to invoke them directly.
+
+### FastMCP Server
+
+To access the underlying FastMCP server instance, you can use the `ctx.fastmcp` property:
+
+```python
+@mcp.tool
+async def my_tool(ctx: Context) -> None:
+ # Access the FastMCP server instance
+ server_name = ctx.fastmcp.name
+ ...
+```
+
+### Transport
+
+
+
+The `ctx.transport` property indicates which transport is being used to run the server. This is useful when your tool needs to behave differently depending on whether the server is running over STDIO, SSE, or Streamable HTTP. For example, you might want to return shorter responses over STDIO or adjust timeout behavior based on transport characteristics.
+
+The transport type is set once when the server starts and remains constant for the server's lifetime. It returns `None` when called outside of a server context (for example, in unit tests or when running code outside of an MCP request).
+
+```python
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP("example")
+
+@mcp.tool
+def connection_info(ctx: Context) -> str:
+ if ctx.transport == "stdio":
+ return "Connected via STDIO"
+ elif ctx.transport == "sse":
+ return "Connected via SSE"
+ elif ctx.transport == "streamable-http":
+ return "Connected via Streamable HTTP"
+ else:
+ return "Transport unknown"
+```
+
+**Property signature:** `ctx.transport -> Literal["stdio", "sse", "streamable-http"] | None`
+
+### MCP Request
+
+Access metadata about the current request and client.
+
+```python
+@mcp.tool
+async def request_info(ctx: Context) -> dict:
+ """Return information about the current request."""
+ return {
+ "request_id": ctx.request_id,
+ "client_id": ctx.client_id or "Unknown client"
+ }
+```
+
+**Available Properties:**
+
+- **`ctx.request_id -> str`**: Get the unique ID for the current MCP request
+- **`ctx.client_id -> str | None`**: Get the ID of the client making the request, if provided during initialization
+- **`ctx.session_id -> str`**: Get the MCP session ID for session-based data sharing. Raises `RuntimeError` if the MCP session is not yet established.
+
+#### Request Context Availability
+
+
+
+The `ctx.request_context` property provides access to the underlying MCP request context, but returns `None` when the MCP session has not been established yet. This typically occurs:
+
+- During middleware execution in the `on_request` hook before the MCP handshake completes
+- During the initialization phase of client connections
+
+The MCP request context is distinct from the HTTP request. For HTTP transports, HTTP request data may be available even when the MCP session is not yet established.
+
+To safely access the request context in situations where it may not be available:
+
+```python
+from fastmcp import FastMCP, Context
+from fastmcp.server.dependencies import get_http_request
+
+mcp = FastMCP(name="Session Aware Demo")
+
+@mcp.tool
+async def session_info(ctx: Context) -> dict:
+ """Return session information when available."""
+
+ # Check if MCP session is available
+ if ctx.request_context:
+ # MCP session available - can access MCP-specific attributes
+ return {
+ "session_id": ctx.session_id,
+ "request_id": ctx.request_id,
+ "has_meta": ctx.request_context.meta is not None
+ }
+ else:
+ # MCP session not available - use HTTP helpers for request data (if using HTTP transport)
+ request = get_http_request()
+ return {
+ "message": "MCP session not available",
+ "user_agent": request.headers.get("user-agent", "Unknown")
+ }
+```
+
+For HTTP request access that works regardless of MCP session availability (when using HTTP transports), use the [HTTP request helpers](/servers/dependency-injection#http-request) like `get_http_request()` and `get_http_headers()`.
+
+#### Client Metadata
+
+
+
+Clients can send contextual information with their requests using the `meta` parameter. This metadata is accessible through `ctx.request_context.meta` and is available for all MCP operations (tools, resources, prompts).
+
+The `meta` field is `None` when clients don't provide metadata. When provided, metadata is accessible via attribute access (e.g., `meta.user_id`) rather than dictionary access. The structure of metadata is determined by the client making the request.
+
+```python
+@mcp.tool
+def send_email(to: str, subject: str, body: str, ctx: Context) -> str:
+ """Send an email, logging metadata about the request."""
+
+ # Access client-provided metadata
+ meta = ctx.request_context.meta
+
+ if meta:
+ # Meta is accessed as an object with attribute access
+ user_id = meta.user_id if hasattr(meta, 'user_id') else None
+ trace_id = meta.trace_id if hasattr(meta, 'trace_id') else None
+
+ # Use metadata for logging, observability, etc.
+ if trace_id:
+ log_with_trace(f"Sending email for user {user_id}", trace_id)
+
+ # Send the email...
+ return f"Email sent to {to}"
+```
+
+
+The MCP request is part of the low-level MCP SDK and intended for advanced use cases. Most users will not need to use it directly.
+
+
diff --git a/docs/v3/servers/dependency-injection.mdx b/docs/v3/servers/dependency-injection.mdx
new file mode 100644
index 000000000..40fc7b65b
--- /dev/null
+++ b/docs/v3/servers/dependency-injection.mdx
@@ -0,0 +1,433 @@
+---
+title: Dependency Injection
+sidebarTitle: Dependencies
+description: Inject runtime values like HTTP requests, access tokens, and custom dependencies into your MCP components.
+icon: syringe
+tag: NEW
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx";
+
+FastMCP uses dependency injection to provide runtime values to your tools, resources, and prompts. Instead of passing context through every layer of your code, you declare what you need as parameter defaults—FastMCP resolves them automatically when your function runs.
+
+The dependency injection system is powered by [Docket](https://github.com/chrisguidry/docket) and its dependency system [uncalled-for](https://github.com/chrisguidry/uncalled-for). Core DI features like `Depends()` and `CurrentContext()` work without installing Docket. For background tasks and advanced task-related dependencies, install `fastmcp[tasks]`. For comprehensive coverage of dependency patterns, see the [Docket dependency documentation](https://docket.lol/en/latest/dependency-injection/).
+
+
+Dependency parameters are automatically excluded from the MCP schema—clients never see them as callable parameters. This separation keeps your function signatures clean while giving you access to the runtime context you need.
+
+
+## How Dependency Injection Works
+
+Dependency injection in FastMCP follows a simple pattern: declare a parameter with a recognized type annotation or a dependency default value, and FastMCP injects the resolved value at runtime.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.context import Context
+
+mcp = FastMCP("Demo")
+
+
+@mcp.tool
+async def my_tool(query: str, ctx: Context) -> str:
+ await ctx.info(f"Processing: {query}")
+ return f"Results for: {query}"
+```
+
+When a client calls `my_tool`, they only see `query` as a parameter. The `ctx` parameter is injected automatically because it has a `Context` type annotation—FastMCP recognizes this and provides the active context for the request.
+
+This works identically for tools, resources, resource templates, and prompts.
+
+### Explicit Dependencies with CurrentContext
+
+For more explicit code, you can use `CurrentContext()` as a default value instead of relying on the type annotation:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.dependencies import CurrentContext
+from fastmcp.server.context import Context
+
+mcp = FastMCP("Demo")
+
+
+@mcp.tool
+async def my_tool(query: str, ctx: Context = CurrentContext()) -> str:
+ await ctx.info(f"Processing: {query}")
+ return f"Results for: {query}"
+```
+
+Both approaches work identically. The type-annotation approach is more concise; the explicit `CurrentContext()` approach makes the dependency injection visible in the signature.
+
+## Built-in Dependencies
+
+### MCP Context
+
+The MCP Context provides logging, progress reporting, resource access, and other request-scoped operations. See [MCP Context](/servers/context) for the full API.
+
+**Dependency injection:** Use a `Context` type annotation (FastMCP injects automatically) or `CurrentContext()`:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.context import Context
+
+mcp = FastMCP("Demo")
+
+
+@mcp.tool
+async def process_data(data: str, ctx: Context) -> str:
+ await ctx.info(f"Processing: {data}")
+ return "Done"
+
+
+# Or explicitly with CurrentContext()
+from fastmcp.dependencies import CurrentContext
+
+@mcp.tool
+async def process_data(data: str, ctx: Context = CurrentContext()) -> str:
+ ...
+```
+
+**Function:** Use `get_context()` in helper functions or middleware:
+
+```python
+from fastmcp.server.dependencies import get_context
+
+async def log_something(message: str):
+ ctx = get_context()
+ await ctx.info(message)
+```
+
+### Server Instance
+
+
+
+Access the FastMCP server instance for introspection or server-level configuration.
+
+**Dependency injection:** Use `CurrentFastMCP()`:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.dependencies import CurrentFastMCP
+
+mcp = FastMCP("Demo")
+
+
+@mcp.tool
+async def server_info(server: FastMCP = CurrentFastMCP()) -> str:
+ return f"Server: {server.name}"
+```
+
+**Function:** Use `get_server()`:
+
+```python
+from fastmcp.server.dependencies import get_server
+
+def get_server_name() -> str:
+ return get_server().name
+```
+
+### HTTP Request
+
+
+
+Access the Starlette Request when running over HTTP transports (SSE or Streamable HTTP).
+
+**Dependency injection:** Use `CurrentRequest()`:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.dependencies import CurrentRequest
+from starlette.requests import Request
+
+mcp = FastMCP("Demo")
+
+
+@mcp.tool
+async def client_info(request: Request = CurrentRequest()) -> dict:
+ return {
+ "user_agent": request.headers.get("user-agent", "Unknown"),
+ "client_ip": request.client.host if request.client else "Unknown",
+ }
+```
+
+**Function:** Use `get_http_request()`:
+
+```python
+from fastmcp.server.dependencies import get_http_request
+
+def get_client_ip() -> str:
+ request = get_http_request()
+ return request.client.host if request.client else "Unknown"
+```
+
+
+Both raise `RuntimeError` when called outside an HTTP context (e.g., STDIO transport).
+For background tasks created from an HTTP request, FastMCP restores a minimal request
+backed by the originating request's snapshotted headers. Use HTTP Headers if you need
+graceful fallback.
+
+
+### HTTP Headers
+
+
+
+Access HTTP headers with graceful fallback. When a background task originates from an
+HTTP request, FastMCP restores the originating headers inside the worker. When no HTTP
+request is available, this returns an empty dictionary, making it safe for code that
+might run over any transport.
+
+**Dependency injection:** Use `CurrentHeaders()`:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.dependencies import CurrentHeaders
+
+mcp = FastMCP("Demo")
+
+
+@mcp.tool
+async def get_auth_type(headers: dict = CurrentHeaders()) -> str:
+ auth = headers.get("authorization", "")
+ return "Bearer" if auth.startswith("Bearer ") else "None"
+```
+
+**Function:** Use `get_http_headers()`:
+
+```python
+from fastmcp.server.dependencies import get_http_headers
+
+def get_user_agent() -> str:
+ headers = get_http_headers()
+ return headers.get("user-agent", "Unknown")
+```
+
+By default, problematic headers like `host` and `content-length` are excluded. Use `get_http_headers(include_all=True)` to include all headers.
+
+### Access Token
+
+
+
+Access the authenticated user's token when your server uses authentication.
+
+**Dependency injection:** Use `CurrentAccessToken()` (raises if not authenticated):
+
+```python
+from fastmcp import FastMCP
+from fastmcp.dependencies import CurrentAccessToken
+from fastmcp.server.auth import AccessToken
+
+mcp = FastMCP("Demo")
+
+
+@mcp.tool
+async def get_user_id(token: AccessToken = CurrentAccessToken()) -> str:
+ return token.claims.get("sub", "unknown")
+```
+
+**Function:** Use `get_access_token()` (returns `None` if not authenticated):
+
+```python
+from fastmcp.server.dependencies import get_access_token
+
+@mcp.tool
+async def get_user_info() -> dict:
+ token = get_access_token()
+ if token is None:
+ return {"authenticated": False}
+ return {"authenticated": True, "user": token.claims.get("sub")}
+```
+
+The `AccessToken` object provides:
+
+- **`client_id`**: The OAuth client identifier
+- **`scopes`**: List of granted permission scopes
+- **`expires_at`**: Token expiration timestamp (if available)
+- **`claims`**: Dictionary of all token claims (JWT claims or provider-specific data)
+
+### Token Claims
+
+When you need just one specific value from the token—like a user ID or tenant identifier—`TokenClaim()` extracts it directly without needing the full token object.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.dependencies import TokenClaim
+
+mcp = FastMCP("Demo")
+
+
+@mcp.tool
+async def add_expense(
+ amount: float,
+ user_id: str = TokenClaim("oid"), # Azure object ID
+) -> dict:
+ await db.insert({"user_id": user_id, "amount": amount})
+ return {"status": "created", "user_id": user_id}
+```
+
+`TokenClaim()` raises a `RuntimeError` if the claim doesn't exist, listing available claims to help with debugging.
+
+Common claims vary by identity provider:
+
+| Provider | User ID Claim | Email Claim | Name Claim |
+|----------|--------------|-------------|------------|
+| Azure/Entra | `oid` | `email` | `name` |
+| GitHub | `sub` | `email` | `name` |
+| Google | `sub` | `email` | `name` |
+| Auth0 | `sub` | `email` | `name` |
+
+### Background Task Dependencies
+
+
+
+For background task execution, FastMCP provides dependencies that integrate with [Docket](https://github.com/chrisguidry/docket). These require installing `fastmcp[tasks]`.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.dependencies import CurrentDocket, CurrentWorker, Progress
+
+mcp = FastMCP("Task Demo")
+
+
+@mcp.tool(task=True)
+async def long_running_task(
+ data: str,
+ docket=CurrentDocket(),
+ worker=CurrentWorker(),
+ progress=Progress(),
+) -> str:
+ await progress.set_total(100)
+
+ for i in range(100):
+ # Process chunk...
+ await progress.increment()
+ await progress.set_message(f"Processing chunk {i + 1}")
+
+ return "Complete"
+```
+
+- **`CurrentDocket()`**: Access the Docket instance for scheduling additional background work
+- **`CurrentWorker()`**: Access the worker processing tasks (name, concurrency settings)
+- **`Progress()`**: Track task progress with atomic updates
+
+
+Task dependencies require `pip install 'fastmcp[tasks]'`. They're only available within task-enabled components (`task=True`). For comprehensive task patterns, see the [Docket documentation](https://chrisguidry.github.io/docket/dependencies/).
+
+
+## Custom Dependencies
+
+Beyond the built-in dependencies, you can create your own to inject configuration, database connections, API clients, or any other values your functions need.
+
+### Using Depends()
+
+The `Depends()` function wraps any callable and injects its return value. This works with synchronous functions, async functions, and async context managers.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.dependencies import Depends
+
+mcp = FastMCP("Custom Deps Demo")
+
+
+def get_config() -> dict:
+ return {"api_url": "https://api.example.com", "timeout": 30}
+
+
+async def get_user_id() -> int:
+ # Could fetch from database, external service, etc.
+ return 42
+
+
+@mcp.tool
+async def fetch_data(
+ query: str,
+ config: dict = Depends(get_config),
+ user_id: int = Depends(get_user_id),
+) -> str:
+ return f"User {user_id} fetching '{query}' from {config['api_url']}"
+```
+
+### Caching
+
+Dependencies are cached per-request. If multiple parameters use the same dependency, or if nested dependencies share a common dependency, it's resolved once and the same instance is reused.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.dependencies import Depends
+
+mcp = FastMCP("Caching Demo")
+
+
+def get_db_connection():
+ print("Connecting to database...") # Only printed once per request
+ return {"connection": "active"}
+
+
+def get_user_repo(db=Depends(get_db_connection)):
+ return {"db": db, "type": "user"}
+
+
+def get_order_repo(db=Depends(get_db_connection)):
+ return {"db": db, "type": "order"}
+
+
+@mcp.tool
+async def process_order(
+ order_id: str,
+ users=Depends(get_user_repo),
+ orders=Depends(get_order_repo),
+) -> str:
+ # Both repos share the same db connection
+ return f"Processed order {order_id}"
+```
+
+### Resource Management
+
+For dependencies that need cleanup—database connections, file handles, HTTP clients—use an async context manager. The cleanup code runs after your function completes, even if an error occurs.
+
+```python
+from contextlib import asynccontextmanager
+
+from fastmcp import FastMCP
+from fastmcp.dependencies import Depends
+
+mcp = FastMCP("Resource Demo")
+
+
+@asynccontextmanager
+async def get_database():
+ db = await connect_to_database()
+ try:
+ yield db
+ finally:
+ await db.close()
+
+
+@mcp.tool
+async def query_users(sql: str, db=Depends(get_database)) -> list:
+ return await db.execute(sql)
+```
+
+### Nested Dependencies
+
+Dependencies can depend on other dependencies. FastMCP resolves them in the correct order and applies caching across the dependency tree.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.dependencies import Depends
+
+mcp = FastMCP("Nested Demo")
+
+
+def get_base_url() -> str:
+ return "https://api.example.com"
+
+
+def get_api_client(base_url: str = Depends(get_base_url)) -> dict:
+ return {"base_url": base_url, "version": "v1"}
+
+
+@mcp.tool
+async def call_api(endpoint: str, client: dict = Depends(get_api_client)) -> str:
+ return f"Calling {client['base_url']}/{client['version']}/{endpoint}"
+```
+
+For advanced dependency patterns—like `TaskArgument()` for accessing task parameters, or custom `Dependency` subclasses—see the [Docket dependency documentation](https://chrisguidry.github.io/docket/dependencies/).
diff --git a/docs/v3/servers/elicitation.mdx b/docs/v3/servers/elicitation.mdx
new file mode 100644
index 000000000..923e704c6
--- /dev/null
+++ b/docs/v3/servers/elicitation.mdx
@@ -0,0 +1,379 @@
+---
+title: User Elicitation
+sidebarTitle: Elicitation
+description: Request structured input from users during tool execution through the MCP context.
+icon: message-question
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+User elicitation allows MCP servers to request structured input from users during tool execution. Instead of requiring all inputs upfront, tools can interactively ask for missing parameters, clarification, or additional context as needed.
+
+Elicitation enables tools to pause execution and request specific information from users:
+
+- **Missing parameters**: Ask for required information not provided initially
+- **Clarification requests**: Get user confirmation or choices for ambiguous scenarios
+- **Progressive disclosure**: Collect complex information step-by-step
+- **Dynamic workflows**: Adapt tool behavior based on user responses
+
+For example, a file management tool might ask "Which directory should I create?" or a data analysis tool might request "What date range should I analyze?"
+
+## Overview
+
+Use the `ctx.elicit()` method within any tool function to request user input. Specify the message to display and the type of response you expect.
+
+```python
+from fastmcp import FastMCP, Context
+from dataclasses import dataclass
+
+mcp = FastMCP("Elicitation Server")
+
+@dataclass
+class UserInfo:
+ name: str
+ age: int
+
+@mcp.tool
+async def collect_user_info(ctx: Context) -> str:
+ """Collect user information through interactive prompts."""
+ result = await ctx.elicit(
+ message="Please provide your information",
+ response_type=UserInfo
+ )
+
+ if result.action == "accept":
+ user = result.data
+ return f"Hello {user.name}, you are {user.age} years old"
+ elif result.action == "decline":
+ return "Information not provided"
+ else: # cancel
+ return "Operation cancelled"
+```
+
+The elicitation result contains an `action` field indicating how the user responded:
+
+| Action | Description |
+|--------|-------------|
+| `accept` | User provided valid input—data is available in the `data` field |
+| `decline` | User chose not to provide the requested information |
+| `cancel` | User cancelled the entire operation |
+
+FastMCP also provides typed result classes for pattern matching:
+
+```python
+from fastmcp.server.elicitation import (
+ AcceptedElicitation,
+ DeclinedElicitation,
+ CancelledElicitation,
+)
+
+@mcp.tool
+async def pattern_example(ctx: Context) -> str:
+ result = await ctx.elicit("Enter your name:", response_type=str)
+
+ match result:
+ case AcceptedElicitation(data=name):
+ return f"Hello {name}!"
+ case DeclinedElicitation():
+ return "No name provided"
+ case CancelledElicitation():
+ return "Operation cancelled"
+```
+
+### Multi-Turn Elicitation
+
+Tools can make multiple elicitation calls to gather information progressively:
+
+```python
+@mcp.tool
+async def plan_meeting(ctx: Context) -> str:
+ """Plan a meeting by gathering details step by step."""
+
+ title_result = await ctx.elicit("What's the meeting title?", response_type=str)
+ if title_result.action != "accept":
+ return "Meeting planning cancelled"
+
+ duration_result = await ctx.elicit("Duration in minutes?", response_type=int)
+ if duration_result.action != "accept":
+ return "Meeting planning cancelled"
+
+ priority_result = await ctx.elicit(
+ "Is this urgent?",
+ response_type=["yes", "no"]
+ )
+ if priority_result.action != "accept":
+ return "Meeting planning cancelled"
+
+ urgent = priority_result.data == "yes"
+ return f"Meeting '{title_result.data}' for {duration_result.data} minutes (Urgent: {urgent})"
+```
+
+### Client Requirements
+
+Elicitation requires the client to implement an elicitation handler. If a client doesn't support elicitation, calls to `ctx.elicit()` will raise an error indicating that elicitation is not supported.
+
+See [Client Elicitation](/clients/elicitation) for details on how clients handle these requests.
+
+## Schema and Response Types
+
+The server must send a schema to the client indicating the type of data it expects in response to the elicitation request. The MCP spec only supports a limited subset of JSON Schema types for elicitation responses—specifically JSON **objects** with **primitive** properties including `string`, `number` (or `integer`), `boolean`, and `enum` fields.
+
+FastMCP makes it easy to request a broader range of types, including scalars (e.g. `str`) or no response at all, by automatically wrapping them in MCP-compatible object schemas.
+
+### Scalar Types
+
+You can request simple scalar data types for basic input, such as a string, integer, or boolean. When you request a scalar type, FastMCP automatically wraps it in an object schema for MCP spec compatibility. Clients will see a schema requesting a single "value" field of the requested type. Once clients respond, the provided object is "unwrapped" and the scalar value is returned directly in the `data` field.
+
+
+```python title="String"
+@mcp.tool
+async def get_user_name(ctx: Context) -> str:
+ result = await ctx.elicit("What's your name?", response_type=str)
+
+ if result.action == "accept":
+ return f"Hello, {result.data}!"
+ return "No name provided"
+```
+```python title="Integer"
+@mcp.tool
+async def pick_a_number(ctx: Context) -> str:
+ result = await ctx.elicit("Pick a number!", response_type=int)
+
+ if result.action == "accept":
+ return f"You picked {result.data}"
+ return "No number provided"
+```
+```python title="Boolean"
+@mcp.tool
+async def pick_a_boolean(ctx: Context) -> str:
+ result = await ctx.elicit("True or false?", response_type=bool)
+
+ if result.action == "accept":
+ return f"You picked {result.data}"
+ return "No boolean provided"
+```
+
+
+#### Customizing the Field Label
+
+
+
+When FastMCP wraps a scalar, `Literal`, `Enum`, or one of the constrained-option shorthands, the wrapper's `value` property is labelled `"Value"` by default — and some clients (including VS Code) render that label directly in the UI. Pass `response_title` and `response_description` to override it:
+
+```python
+@mcp.tool
+async def confirm_purchase(ctx: Context) -> str:
+ result = await ctx.elicit(
+ "Buy 1x Baguette?",
+ response_type=bool,
+ response_title="Confirm purchase",
+ response_description="Approve this transaction?",
+ )
+ if result.action == "accept":
+ return "Purchased" if result.data else "Declined"
+ return "No response"
+```
+
+These arguments only apply when FastMCP is adding the wrapper. For structured responses (`BaseModel`, dataclass, `TypedDict`), set the metadata on the individual fields via `Field(title=..., description=...)` — passing `response_title` or `response_description` alongside a model type raises `TypeError`.
+
+### No Response
+
+Sometimes, the goal of an elicitation is to simply get a user to approve or reject an action. Pass `None` as the response type to indicate that no data is expected. The `data` field will be `None` when the user accepts.
+
+```python
+@mcp.tool
+async def approve_action(ctx: Context) -> str:
+ result = await ctx.elicit("Approve this action?", response_type=None)
+
+ if result.action == "accept":
+ return do_action()
+ else:
+ raise ValueError("Action rejected")
+```
+
+### Constrained Options
+
+Constrain the user's response to a specific set of values using a `Literal` type, Python enum, or a list of strings as a convenient shortcut.
+
+
+```python title="List of strings"
+@mcp.tool
+async def set_priority(ctx: Context) -> str:
+ result = await ctx.elicit(
+ "What priority level?",
+ response_type=["low", "medium", "high"],
+ )
+
+ if result.action == "accept":
+ return f"Priority set to: {result.data}"
+```
+```python title="Literal type"
+from typing import Literal
+
+@mcp.tool
+async def set_priority(ctx: Context) -> str:
+ result = await ctx.elicit(
+ "What priority level?",
+ response_type=Literal["low", "medium", "high"]
+ )
+
+ if result.action == "accept":
+ return f"Priority set to: {result.data}"
+ return "No priority set"
+```
+```python title="Python enum"
+from enum import Enum
+
+class Priority(Enum):
+ LOW = "low"
+ MEDIUM = "medium"
+ HIGH = "high"
+
+@mcp.tool
+async def set_priority(ctx: Context) -> str:
+ result = await ctx.elicit("What priority level?", response_type=Priority)
+
+ if result.action == "accept":
+ return f"Priority set to: {result.data.value}"
+ return "No priority set"
+```
+
+
+### Multi-Select
+
+
+
+Enable multi-select by wrapping your choices in an additional list level. This allows users to select multiple values from the available options.
+
+
+```python title="List of strings"
+@mcp.tool
+async def select_tags(ctx: Context) -> str:
+ result = await ctx.elicit(
+ "Choose tags",
+ response_type=[["bug", "feature", "documentation"]] # Note: list of a list
+ )
+
+ if result.action == "accept":
+ tags = result.data
+ return f"Selected tags: {', '.join(tags)}"
+```
+```python title="list[Enum] type"
+from enum import Enum
+
+class Tag(Enum):
+ BUG = "bug"
+ FEATURE = "feature"
+ DOCS = "documentation"
+
+@mcp.tool
+async def select_tags(ctx: Context) -> str:
+ result = await ctx.elicit(
+ "Choose tags",
+ response_type=list[Tag]
+ )
+ if result.action == "accept":
+ tags = [tag.value for tag in result.data]
+ return f"Selected: {', '.join(tags)}"
+```
+
+
+### Titled Options
+
+
+
+For better UI display, provide human-readable titles for enum options. FastMCP generates SEP-1330 compliant schemas using the `oneOf` pattern with `const` and `title` fields.
+
+```python
+@mcp.tool
+async def set_priority(ctx: Context) -> str:
+ result = await ctx.elicit(
+ "What priority level?",
+ response_type={
+ "low": {"title": "Low Priority"},
+ "medium": {"title": "Medium Priority"},
+ "high": {"title": "High Priority"}
+ }
+ )
+
+ if result.action == "accept":
+ return f"Priority set to: {result.data}"
+```
+
+For multi-select with titles, wrap the dict in a list:
+
+```python
+@mcp.tool
+async def select_priorities(ctx: Context) -> str:
+ result = await ctx.elicit(
+ "Choose priorities",
+ response_type=[{
+ "low": {"title": "Low Priority"},
+ "medium": {"title": "Medium Priority"},
+ "high": {"title": "High Priority"}
+ }]
+ )
+
+ if result.action == "accept":
+ return f"Selected: {', '.join(result.data)}"
+```
+
+### Structured Responses
+
+Request structured data with multiple fields by using a dataclass, typed dict, or Pydantic model as the response type. Note that the MCP spec only supports shallow objects with scalar (string, number, boolean) or enum properties.
+
+```python
+from dataclasses import dataclass
+from typing import Literal
+
+@dataclass
+class TaskDetails:
+ title: str
+ description: str
+ priority: Literal["low", "medium", "high"]
+ due_date: str
+
+@mcp.tool
+async def create_task(ctx: Context) -> str:
+ result = await ctx.elicit(
+ "Please provide task details",
+ response_type=TaskDetails
+ )
+
+ if result.action == "accept":
+ task = result.data
+ return f"Created task: {task.title} (Priority: {task.priority})"
+ return "Task creation cancelled"
+```
+
+### Default Values
+
+
+
+Provide default values for elicitation fields using Pydantic's `Field(default=...)`. Clients will pre-populate form fields with these defaults. Fields with default values are automatically marked as optional.
+
+```python
+from pydantic import BaseModel, Field
+from enum import Enum
+
+class Priority(Enum):
+ LOW = "low"
+ MEDIUM = "medium"
+ HIGH = "high"
+
+class TaskDetails(BaseModel):
+ title: str = Field(description="Task title")
+ description: str = Field(default="", description="Task description")
+ priority: Priority = Field(default=Priority.MEDIUM, description="Task priority")
+
+@mcp.tool
+async def create_task(ctx: Context) -> str:
+ result = await ctx.elicit("Please provide task details", response_type=TaskDetails)
+ if result.action == "accept":
+ return f"Created: {result.data.title}"
+ return "Task creation cancelled"
+```
+
+Default values are supported for strings, integers, numbers, booleans, and enums.
diff --git a/docs/v3/servers/icons.mdx b/docs/v3/servers/icons.mdx
new file mode 100644
index 000000000..c9b558094
--- /dev/null
+++ b/docs/v3/servers/icons.mdx
@@ -0,0 +1,151 @@
+---
+title: Icons
+description: Add visual icons to your servers, tools, resources, and prompts
+icon: image
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Icons provide visual representations for your MCP servers and components, helping client applications present better user interfaces. When displayed in MCP clients, icons help users quickly identify and navigate your server's capabilities.
+
+## Icon Format
+
+Icons use the standard MCP Icon type from the MCP protocol specification. Each icon specifies a source URL or data URI, and optionally includes MIME type and size information.
+
+```python
+from mcp.types import Icon
+
+icon = Icon(
+ src="https://example.com/icon.png",
+ mimeType="image/png",
+ sizes=["48x48"]
+)
+```
+
+The fields serve different purposes:
+
+- **src**: URL or data URI pointing to the icon image
+- **mimeType** (optional): MIME type of the image (e.g., "image/png", "image/svg+xml")
+- **sizes** (optional): Array of size descriptors (e.g., ["48x48"], ["any"])
+
+## Server Icons
+
+Add icons and a website URL to your server for display in client applications. Multiple icons at different sizes help clients choose the best resolution for their display context.
+
+```python
+from fastmcp import FastMCP
+from mcp.types import Icon
+
+mcp = FastMCP(
+ name="WeatherService",
+ website_url="https://weather.example.com",
+ icons=[
+ Icon(
+ src="https://weather.example.com/icon-48.png",
+ mimeType="image/png",
+ sizes=["48x48"]
+ ),
+ Icon(
+ src="https://weather.example.com/icon-96.png",
+ mimeType="image/png",
+ sizes=["96x96"]
+ ),
+ ]
+)
+```
+
+Server icons appear in MCP client interfaces to help users identify your server among others they may have installed.
+
+## Component Icons
+
+Icons can be added to individual tools, resources, resource templates, and prompts. This helps users visually distinguish between different component types and purposes.
+
+### Tool Icons
+
+```python
+from mcp.types import Icon
+
+@mcp.tool(
+ icons=[Icon(src="https://example.com/calculator-icon.png")]
+)
+def calculate_sum(a: int, b: int) -> int:
+ """Add two numbers together."""
+ return a + b
+```
+
+### Resource Icons
+
+```python
+@mcp.resource(
+ "config://settings",
+ icons=[Icon(src="https://example.com/config-icon.png")]
+)
+def get_settings() -> dict:
+ """Retrieve application settings."""
+ return {"theme": "dark", "language": "en"}
+```
+
+### Resource Template Icons
+
+```python
+@mcp.resource(
+ "user://{user_id}/profile",
+ icons=[Icon(src="https://example.com/user-icon.png")]
+)
+def get_user_profile(user_id: str) -> dict:
+ """Get a user's profile."""
+ return {"id": user_id, "name": f"User {user_id}"}
+```
+
+### Prompt Icons
+
+```python
+@mcp.prompt(
+ icons=[Icon(src="https://example.com/prompt-icon.png")]
+)
+def analyze_code(code: str):
+ """Create a prompt for code analysis."""
+ return f"Please analyze this code:\n\n{code}"
+```
+
+## Using Data URIs
+
+For small icons or when you want to embed the icon directly without external dependencies, use data URIs. This approach eliminates the need for hosting and ensures the icon is always available.
+
+```python
+from mcp.types import Icon
+from fastmcp.utilities.types import Image
+
+# SVG icon as data URI
+svg_icon = Icon(
+ src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCI+PHBhdGggZD0iTTEyIDJDNi40OCAyIDIgNi40OCAyIDEyczQuNDggMTAgMTAgMTAgMTAtNC40OCAxMC0xMFMxNy41MiAyIDEyIDJ6Ii8+PC9zdmc+",
+ mimeType="image/svg+xml"
+)
+
+@mcp.tool(icons=[svg_icon])
+def my_tool() -> str:
+ """A tool with an embedded SVG icon."""
+ return "result"
+```
+
+### Generating Data URIs from Files
+
+FastMCP provides the `Image` utility class to convert local image files into data URIs.
+
+```python
+from mcp.types import Icon
+from fastmcp.utilities.types import Image
+
+# Generate a data URI from a local image file
+img = Image(path="./assets/brand/favicon.png")
+icon = Icon(src=img.to_data_uri())
+
+@mcp.tool(icons=[icon])
+def file_icon_tool() -> str:
+ """A tool with an icon generated from a local file."""
+ return "result"
+```
+
+This approach is useful when you have local image assets and want to embed them directly in your server definition.
diff --git a/docs/v3/servers/lifespan.mdx b/docs/v3/servers/lifespan.mdx
new file mode 100644
index 000000000..822e4f8a1
--- /dev/null
+++ b/docs/v3/servers/lifespan.mdx
@@ -0,0 +1,148 @@
+---
+title: Lifespans
+sidebarTitle: Lifespan
+description: Server-level setup and teardown with composable lifespans
+icon: heart-pulse
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Lifespans let you run code once when the server starts and clean up when it stops. Unlike per-session handlers, lifespans run exactly once regardless of how many clients connect.
+
+## Basic Usage
+
+Use the `@lifespan` decorator to define a lifespan:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.lifespan import lifespan
+
+@lifespan
+async def app_lifespan(server):
+ # Setup: runs once when server starts
+ print("Starting up...")
+ try:
+ yield {"started_at": "2024-01-01"}
+ finally:
+ # Teardown: runs when server stops
+ print("Shutting down...")
+
+mcp = FastMCP("MyServer", lifespan=app_lifespan)
+```
+
+The dict you yield becomes the **lifespan context**, accessible from tools.
+
+
+Always use `try/finally` for cleanup code to ensure it runs even if the server is cancelled.
+
+
+## Accessing Lifespan Context
+
+Access the lifespan context in tools via `ctx.lifespan_context`:
+
+```python
+from fastmcp import FastMCP, Context
+from fastmcp.server.lifespan import lifespan
+
+@lifespan
+async def app_lifespan(server):
+ # Initialize shared state
+ data = {"users": ["alice", "bob"]}
+ yield {"data": data}
+
+mcp = FastMCP("MyServer", lifespan=app_lifespan)
+
+@mcp.tool
+def list_users(ctx: Context) -> list[str]:
+ data = ctx.lifespan_context["data"]
+ return data["users"]
+```
+
+## Composing Lifespans
+
+Compose multiple lifespans with the `|` operator:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.lifespan import lifespan
+
+@lifespan
+async def config_lifespan(server):
+ config = {"debug": True, "version": "1.0"}
+ yield {"config": config}
+
+@lifespan
+async def data_lifespan(server):
+ data = {"items": []}
+ yield {"data": data}
+
+# Compose with |
+mcp = FastMCP("MyServer", lifespan=config_lifespan | data_lifespan)
+```
+
+Composed lifespans:
+- Enter in order (left to right)
+- Exit in reverse order (right to left)
+- Merge their context dicts (later values overwrite earlier on conflict)
+
+## Backwards Compatibility
+
+Existing `@asynccontextmanager` lifespans still work when passed directly to FastMCP:
+
+```python
+from contextlib import asynccontextmanager
+from fastmcp import FastMCP
+
+@asynccontextmanager
+async def legacy_lifespan(server):
+ yield {"key": "value"}
+
+mcp = FastMCP("MyServer", lifespan=legacy_lifespan)
+```
+
+To compose an `@asynccontextmanager` function with `@lifespan` functions, wrap it with `ContextManagerLifespan`:
+
+```python
+from contextlib import asynccontextmanager
+from fastmcp.server.lifespan import lifespan, ContextManagerLifespan
+
+@asynccontextmanager
+async def legacy_lifespan(server):
+ yield {"legacy": True}
+
+@lifespan
+async def new_lifespan(server):
+ yield {"new": True}
+
+# Wrap the legacy lifespan explicitly for composition
+combined = ContextManagerLifespan(legacy_lifespan) | new_lifespan
+```
+
+## With FastAPI
+
+When mounting FastMCP into FastAPI, use `combine_lifespans` to run both your app's lifespan and the MCP server's lifespan:
+
+```python
+from contextlib import asynccontextmanager
+
+from fastapi import FastAPI
+from fastmcp import FastMCP
+from fastmcp.utilities.lifespan import combine_lifespans
+
+@asynccontextmanager
+async def app_lifespan(app):
+ print("FastAPI starting...")
+ yield
+ print("FastAPI shutting down...")
+
+mcp = FastMCP("Tools")
+mcp_app = mcp.http_app()
+
+app = FastAPI(lifespan=combine_lifespans(app_lifespan, mcp_app.lifespan))
+app.mount("/mcp", mcp_app)
+```
+
+See the [FastAPI integration guide](/integrations/fastapi#combining-lifespans) for full details.
diff --git a/docs/v3/servers/logging.mdx b/docs/v3/servers/logging.mdx
new file mode 100644
index 000000000..e01fdd875
--- /dev/null
+++ b/docs/v3/servers/logging.mdx
@@ -0,0 +1,87 @@
+---
+title: Client Logging
+sidebarTitle: Logging
+description: Send log messages back to MCP clients through the context.
+icon: receipt
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+This documentation covers **MCP client logging**—sending messages from your server to MCP clients. For standard server-side logging (e.g., writing to files, console), use `fastmcp.utilities.logging.get_logger()` or Python's built-in `logging` module.
+
+
+Server logging allows MCP tools to send debug, info, warning, and error messages back to the client. Unlike standard Python logging, MCP server logging sends messages directly to the client, making them visible in the client's interface or logs.
+
+## Basic Usage
+
+Use the context logging methods within any tool function:
+
+```python
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP("LoggingDemo")
+
+@mcp.tool
+async def analyze_data(data: list[float], ctx: Context) -> dict:
+ """Analyze numerical data with comprehensive logging."""
+ await ctx.debug("Starting analysis of numerical data")
+ await ctx.info(f"Analyzing {len(data)} data points")
+
+ try:
+ if not data:
+ await ctx.warning("Empty data list provided")
+ return {"error": "Empty data list"}
+
+ result = sum(data) / len(data)
+ await ctx.info(f"Analysis complete, average: {result}")
+ return {"average": result, "count": len(data)}
+
+ except Exception as e:
+ await ctx.error(f"Analysis failed: {str(e)}")
+ raise
+```
+
+## Log Levels
+
+| Level | Use Case |
+|-------|----------|
+| `ctx.debug()` | Detailed execution information for diagnosing problems |
+| `ctx.info()` | General information about normal program execution |
+| `ctx.warning()` | Potentially harmful situations that don't prevent execution |
+| `ctx.error()` | Error events that might still allow the application to continue |
+
+## Structured Logging
+
+All logging methods accept an `extra` parameter for sending structured data to the client. This is useful for creating rich, queryable logs.
+
+```python
+@mcp.tool
+async def process_transaction(transaction_id: str, amount: float, ctx: Context):
+ await ctx.info(
+ f"Processing transaction {transaction_id}",
+ extra={
+ "transaction_id": transaction_id,
+ "amount": amount,
+ "currency": "USD"
+ }
+ )
+```
+
+## Server-Side Logs
+
+Messages sent to clients via `ctx.log()` and its convenience methods are also logged to the server's log at `DEBUG` level. Enable debug logging on the `fastmcp.server.context.to_client` logger to see these messages:
+
+```python
+import logging
+from fastmcp.utilities.logging import get_logger
+
+to_client_logger = get_logger(name="fastmcp.server.context.to_client")
+to_client_logger.setLevel(level=logging.DEBUG)
+```
+
+## Client Handling
+
+Log messages are sent to the client through the MCP protocol. How clients handle these messages depends on their implementation—development clients may display logs in real-time, production clients may store them for analysis, and integration clients may forward them to external logging systems.
+
+See [Client Logging](/clients/logging) for details on how clients handle server log messages.
diff --git a/docs/v3/servers/middleware.mdx b/docs/v3/servers/middleware.mdx
new file mode 100644
index 000000000..b974bb0f5
--- /dev/null
+++ b/docs/v3/servers/middleware.mdx
@@ -0,0 +1,959 @@
+---
+title: Middleware
+sidebarTitle: Middleware
+description: Add cross-cutting functionality to your MCP server with middleware that intercepts and modifies requests and responses.
+icon: layer-group
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+Middleware adds behavior that applies across multiple operations—authentication, logging, rate limiting, or request transformation—without modifying individual tools or resources.
+
+
+MCP middleware is a FastMCP-specific concept and is not part of the official MCP protocol specification.
+
+
+## Overview
+
+MCP middleware forms a pipeline around your server's operations. When a request arrives, it flows through each middleware in order—each can inspect, modify, or reject the request before passing it along. After the operation completes, the response flows back through the same middleware in reverse order.
+
+```
+Request → Middleware A → Middleware B → Handler → Middleware B → Middleware A → Response
+```
+
+This bidirectional flow means middleware can:
+- **Pre-process**: Validate authentication, log incoming requests, check rate limits
+- **Post-process**: Transform responses, record timing metrics, handle errors consistently
+
+The key decision point is `call_next(context)`. Calling it continues the chain; not calling it stops processing entirely.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+
+class LoggingMiddleware(Middleware):
+ async def on_message(self, context: MiddlewareContext, call_next):
+ print(f"→ {context.method}")
+ result = await call_next(context)
+ print(f"← {context.method}")
+ return result
+
+mcp = FastMCP("MyServer")
+mcp.add_middleware(LoggingMiddleware())
+```
+
+### Execution Order
+
+Middleware executes in the order added to the server. The first middleware runs first on the way in and last on the way out:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
+from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware
+from fastmcp.server.middleware.logging import LoggingMiddleware
+
+mcp = FastMCP("MyServer")
+mcp.add_middleware(ErrorHandlingMiddleware()) # 1st in, last out
+mcp.add_middleware(RateLimitingMiddleware()) # 2nd in, 2nd out
+mcp.add_middleware(LoggingMiddleware()) # 3rd in, first out
+```
+
+This ordering matters. Place error handling early so it catches exceptions from all subsequent middleware. Place logging late so it records the actual execution after other middleware has processed the request.
+
+### Server Composition
+
+When using [mounted servers](/servers/composition), middleware behavior follows a clear hierarchy:
+
+- **Parent middleware** runs for all requests, including those routed to mounted servers
+- **Mounted server middleware** only runs for requests handled by that specific server
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.middleware.logging import LoggingMiddleware
+
+parent = FastMCP("Parent")
+parent.add_middleware(AuthMiddleware()) # Runs for ALL requests
+
+child = FastMCP("Child")
+child.add_middleware(LoggingMiddleware()) # Only runs for child's tools
+
+parent.mount(child, namespace="child")
+```
+
+Requests to `child_tool` flow through the parent's `AuthMiddleware` first, then through the child's `LoggingMiddleware`.
+
+Middleware-stored state does not automatically cross mount boundaries. If `AuthMiddleware` on the parent calls `ctx.set_state("user_id", ...)`, a tool on the child server calling `ctx.get_state("user_id")` will get `None` — each `FastMCP` instance owns its own session state store. To share state across the mount, either pass the same `session_state_store` to both servers or use `serializable=False` for request-scoped values. See [State and Mounted Servers](/servers/context#state-and-mounted-servers) for details.
+
+## Hooks
+
+Rather than processing every message identically, FastMCP provides specialized hooks at different levels of specificity. Multiple hooks fire for a single request, going from general to specific:
+
+| Level | Hooks | Purpose |
+|-------|-------|---------|
+| Message | `on_message` | All MCP traffic (requests and notifications) |
+| Type | `on_request`, `on_notification` | Requests expecting responses vs fire-and-forget |
+| Operation | `on_call_tool`, `on_read_resource`, `on_get_prompt`, etc. | Specific MCP operations |
+
+When a client calls a tool, the middleware chain processes `on_message` first, then `on_request`, then `on_call_tool`. This hierarchy lets you target exactly the right scope—use `on_message` for logging everything, `on_request` for authentication, and `on_call_tool` for tool-specific behavior.
+
+### Hook Signature
+
+Every hook follows the same pattern:
+
+```python
+async def hook_name(self, context: MiddlewareContext, call_next) -> result_type:
+ # Pre-processing
+ result = await call_next(context)
+ # Post-processing
+ return result
+```
+
+**Parameters:**
+- `context` — `MiddlewareContext` containing request information
+- `call_next` — Async function to continue the middleware chain
+
+**Returns:** The appropriate result type for the hook (varies by operation).
+
+### MiddlewareContext
+
+The `context` parameter provides access to request details:
+
+| Attribute | Type | Description |
+|-----------|------|-------------|
+| `method` | `str` | MCP method name (e.g., `"tools/call"`) |
+| `source` | `str` | Origin: `"client"` or `"server"` |
+| `type` | `str` | Message type: `"request"` or `"notification"` |
+| `message` | `object` | The MCP message data |
+| `timestamp` | `datetime` | When the request was received |
+| `fastmcp_context` | `Context` | FastMCP context object (if available) |
+
+### Message Hooks
+
+#### on_message
+
+Called for every MCP message—both requests and notifications.
+
+```python
+async def on_message(self, context: MiddlewareContext, call_next):
+ result = await call_next(context)
+ return result
+```
+
+Use for: Logging, metrics, or any cross-cutting concern that applies to all traffic.
+
+#### on_request
+
+Called for MCP requests that expect a response.
+
+```python
+async def on_request(self, context: MiddlewareContext, call_next):
+ result = await call_next(context)
+ return result
+```
+
+Use for: Authentication, authorization, request validation.
+
+#### on_notification
+
+Called for fire-and-forget MCP notifications.
+
+```python
+async def on_notification(self, context: MiddlewareContext, call_next):
+ await call_next(context)
+ # Notifications don't return values
+```
+
+Use for: Event logging, async side effects.
+
+### Operation Hooks
+
+#### on_call_tool
+
+Called when a tool is executed. The `context.message` contains `name` (tool name) and `arguments` (dict).
+
+```python
+async def on_call_tool(self, context: MiddlewareContext, call_next):
+ tool_name = context.message.name
+ args = context.message.arguments
+ result = await call_next(context)
+ return result
+```
+
+**Returns:** Tool execution result or raises `ToolError`.
+
+#### on_read_resource
+
+Called when a resource is read. The `context.message` contains `uri` (resource URI).
+
+```python
+async def on_read_resource(self, context: MiddlewareContext, call_next):
+ uri = context.message.uri
+ result = await call_next(context)
+ return result
+```
+
+**Returns:** Resource content.
+
+#### on_get_prompt
+
+Called when a prompt is retrieved. The `context.message` contains `name` (prompt name) and `arguments` (dict).
+
+```python
+async def on_get_prompt(self, context: MiddlewareContext, call_next):
+ prompt_name = context.message.name
+ result = await call_next(context)
+ return result
+```
+
+**Returns:** Prompt messages.
+
+#### on_list_tools
+
+Called when listing available tools. Returns a list of FastMCP `Tool` objects before MCP conversion.
+
+```python
+async def on_list_tools(self, context: MiddlewareContext, call_next):
+ tools = await call_next(context)
+ # Filter or modify the tool list
+ return tools
+```
+
+**Returns:** `list[Tool]` — Can be filtered before returning to client.
+
+#### on_list_resources
+
+Called when listing available resources. Returns FastMCP `Resource` objects.
+
+```python
+async def on_list_resources(self, context: MiddlewareContext, call_next):
+ resources = await call_next(context)
+ return resources
+```
+
+**Returns:** `list[Resource]`
+
+#### on_list_resource_templates
+
+Called when listing resource templates.
+
+```python
+async def on_list_resource_templates(self, context: MiddlewareContext, call_next):
+ templates = await call_next(context)
+ return templates
+```
+
+**Returns:** `list[ResourceTemplate]`
+
+#### on_list_prompts
+
+Called when listing available prompts.
+
+```python
+async def on_list_prompts(self, context: MiddlewareContext, call_next):
+ prompts = await call_next(context)
+ return prompts
+```
+
+**Returns:** `list[Prompt]`
+
+#### on_initialize
+
+
+
+Called when a client connects and initializes the session. This hook cannot modify the initialization response.
+
+```python
+from mcp import McpError
+from mcp.types import ErrorData
+
+async def on_initialize(self, context: MiddlewareContext, call_next):
+ client_info = context.message.params.get("clientInfo", {})
+ client_name = client_info.get("name", "unknown")
+
+ # Reject before call_next to send error to client
+ if client_name == "blocked-client":
+ raise McpError(ErrorData(code=-32000, message="Client not supported"))
+
+ await call_next(context)
+ print(f"Client {client_name} initialized")
+```
+
+**Returns:** `None` — The initialization response is handled internally by the MCP protocol.
+
+
+Raising `McpError` after `call_next()` will only log the error, not send it to the client. The response has already been sent. Always reject **before** `call_next()`.
+
+
+### Raw Handler
+
+For complete control over all messages, override `__call__` instead of individual hooks:
+
+```python
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+
+class RawMiddleware(Middleware):
+ async def __call__(self, context: MiddlewareContext, call_next):
+ print(f"Processing: {context.method}")
+ result = await call_next(context)
+ print(f"Completed: {context.method}")
+ return result
+```
+
+This bypasses the hook dispatch system entirely. Use when you need uniform handling regardless of message type.
+
+### Session Availability
+
+
+
+The MCP session may not be available during certain phases like initialization. Check before accessing session-specific attributes:
+
+```python
+async def on_request(self, context: MiddlewareContext, call_next):
+ ctx = context.fastmcp_context
+
+ if ctx.request_context:
+ # MCP session available
+ session_id = ctx.session_id
+ request_id = ctx.request_id
+ else:
+ # Session not yet established (e.g., during initialization)
+ # Use HTTP helpers if needed
+ from fastmcp.server.dependencies import get_http_headers
+ headers = get_http_headers()
+
+ return await call_next(context)
+```
+
+For HTTP-specific data (headers, client IP) when using HTTP transports, see [HTTP Requests](/servers/context#http-requests).
+
+## Built-in Middleware
+
+FastMCP includes production-ready middleware for common server concerns.
+
+### Logging
+
+```python
+from fastmcp.server.middleware.logging import LoggingMiddleware, StructuredLoggingMiddleware
+```
+
+`LoggingMiddleware` provides human-readable request and response logging. `StructuredLoggingMiddleware` outputs JSON-formatted logs for aggregation tools like Datadog or Splunk.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.middleware.logging import LoggingMiddleware
+
+mcp = FastMCP("MyServer")
+mcp.add_middleware(LoggingMiddleware(
+ include_payloads=True,
+ max_payload_length=1000
+))
+```
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `include_payloads` | `bool` | `False` | Log request/response content |
+| `max_payload_length` | `int` | `500` | Truncate payloads beyond this length |
+| `logger` | `Logger` | module logger | Custom logger instance |
+
+### Timing
+
+```python
+from fastmcp.server.middleware.timing import TimingMiddleware, DetailedTimingMiddleware
+```
+
+`TimingMiddleware` logs execution duration for all requests. `DetailedTimingMiddleware` provides per-operation timing with separate tracking for tools, resources, and prompts.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.middleware.timing import TimingMiddleware
+
+mcp = FastMCP("MyServer")
+mcp.add_middleware(TimingMiddleware())
+```
+
+### Caching
+
+```python
+from fastmcp.server.middleware.caching import ResponseCachingMiddleware
+```
+
+Caches tool calls, resource reads, and list operations with TTL-based expiration.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.middleware.caching import ResponseCachingMiddleware
+
+mcp = FastMCP("MyServer")
+mcp.add_middleware(ResponseCachingMiddleware())
+```
+
+Each operation type can be configured independently using settings classes:
+
+```python
+from fastmcp.server.middleware.caching import (
+ ResponseCachingMiddleware,
+ CallToolSettings,
+ ListToolsSettings,
+ ReadResourceSettings
+)
+
+mcp.add_middleware(ResponseCachingMiddleware(
+ list_tools_settings=ListToolsSettings(ttl=30),
+ call_tool_settings=CallToolSettings(included_tools=["expensive_tool"]),
+ read_resource_settings=ReadResourceSettings(enabled=False)
+))
+```
+
+| Settings Class | Configures |
+|----------------|------------|
+| `ListToolsSettings` | `on_list_tools` caching |
+| `CallToolSettings` | `on_call_tool` caching |
+| `ListResourcesSettings` | `on_list_resources` caching |
+| `ReadResourceSettings` | `on_read_resource` caching |
+| `ListPromptsSettings` | `on_list_prompts` caching |
+| `GetPromptSettings` | `on_get_prompt` caching |
+
+Each settings class accepts:
+- `enabled` — Enable/disable caching for this operation
+- `ttl` — Time-to-live in seconds
+- `included_*` / `excluded_*` — Whitelist or blacklist specific items
+
+For persistence or distributed deployments, configure a different storage backend:
+
+```python
+from pathlib import Path
+from fastmcp.server.middleware.caching import ResponseCachingMiddleware
+from key_value.aio.stores.filetree import (
+ FileTreeStore,
+ FileTreeV1KeySanitizationStrategy,
+ FileTreeV1CollectionSanitizationStrategy,
+)
+
+cache_dir = Path("cache")
+mcp.add_middleware(ResponseCachingMiddleware(
+ cache_storage=FileTreeStore(
+ data_directory=cache_dir,
+ key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(cache_dir),
+ collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(cache_dir),
+ )
+))
+```
+
+See [Storage Backends](/servers/storage-backends) for complete options.
+
+
+Cache keys are based on the operation name and arguments only — they do not include user or session identity. If your tools return user-specific data derived from auth context (e.g., headers or session state) rather than from the request arguments, you should either disable caching for those tools or ensure user identity is part of the tool arguments.
+
+
+### Rate Limiting
+
+```python
+from fastmcp.server.middleware.rate_limiting import (
+ RateLimitingMiddleware,
+ SlidingWindowRateLimitingMiddleware
+)
+```
+
+`RateLimitingMiddleware` uses a token bucket algorithm allowing controlled bursts. `SlidingWindowRateLimitingMiddleware` provides precise time-window rate limiting without burst allowance.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware
+
+mcp = FastMCP("MyServer")
+mcp.add_middleware(RateLimitingMiddleware(
+ max_requests_per_second=10.0,
+ burst_capacity=20
+))
+```
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `max_requests_per_second` | `float` | `10.0` | Sustained request rate |
+| `burst_capacity` | `int` | `20` | Maximum burst size |
+| `get_client_id` | `Callable` | `None` | Custom client identification |
+
+For sliding window rate limiting:
+
+```python
+from fastmcp.server.middleware.rate_limiting import SlidingWindowRateLimitingMiddleware
+
+mcp.add_middleware(SlidingWindowRateLimitingMiddleware(
+ max_requests=100,
+ window_minutes=1
+))
+```
+
+### Error Handling
+
+```python
+from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware, RetryMiddleware
+```
+
+`ErrorHandlingMiddleware` provides centralized error logging and transformation. `RetryMiddleware` automatically retries with exponential backoff for transient failures.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
+
+mcp = FastMCP("MyServer")
+mcp.add_middleware(ErrorHandlingMiddleware(
+ include_traceback=True,
+ transform_errors=True,
+ error_callback=my_error_callback
+))
+```
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `include_traceback` | `bool` | `False` | Include stack traces in logs |
+| `transform_errors` | `bool` | `False` | Convert exceptions to MCP errors |
+| `error_callback` | `Callable` | `None` | Custom callback on errors |
+
+For automatic retries:
+
+```python
+from fastmcp.server.middleware.error_handling import RetryMiddleware
+
+mcp.add_middleware(RetryMiddleware(
+ max_retries=3,
+ retry_exceptions=(ConnectionError, TimeoutError)
+))
+```
+
+### Ping
+
+
+
+```python
+from fastmcp.server.middleware import PingMiddleware
+```
+
+Keeps long-lived connections alive by sending periodic pings.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.middleware import PingMiddleware
+
+mcp = FastMCP("MyServer")
+mcp.add_middleware(PingMiddleware(interval_ms=5000))
+```
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `interval_ms` | `int` | `30000` | Ping interval in milliseconds |
+
+The ping task starts on the first message and stops automatically when the session ends. Most useful for stateful HTTP connections; has no effect on stateless connections.
+
+### Response Limiting
+
+
+
+```python
+from fastmcp.server.middleware.response_limiting import ResponseLimitingMiddleware
+```
+
+Large tool responses can overwhelm LLM context windows or cause memory issues. You can add response-limiting middleware to enforce size constraints on tool outputs.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.middleware.response_limiting import ResponseLimitingMiddleware
+
+mcp = FastMCP("MyServer")
+
+# Limit all tool responses to 500KB
+mcp.add_middleware(ResponseLimitingMiddleware(max_size=500_000))
+
+@mcp.tool
+def search(query: str) -> str:
+ # This could return a very large result
+ return "x" * 1_000_000 # 1MB response
+
+# When called, the response will be truncated to ~500KB with:
+# "...\n\n[Response truncated due to size limit]"
+```
+
+When a response exceeds the limit, the middleware extracts all text content, joins it together, truncates to fit within the limit, and returns a single `TextContent` block. For non-text responses, the serialized JSON is used as the text source.
+
+
+If a tool defines an `output_schema`, truncated responses will no longer conform to that schema — the client will receive a plain `TextContent` block instead of the expected structured output. Keep this in mind when setting size limits for tools with structured responses.
+
+
+```python
+# Limit only specific tools
+mcp.add_middleware(ResponseLimitingMiddleware(
+ max_size=100_000,
+ tools=["search", "fetch_data"],
+))
+```
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `max_size` | `int` | `1_000_000` | Maximum response size in bytes (1MB default) |
+| `truncation_suffix` | `str` | `"\n\n[Response truncated due to size limit]"` | Suffix appended to truncated responses |
+| `tools` | `list[str] \| None` | `None` | Limit only these tools (None = all tools) |
+
+### Combining Middleware
+
+Order matters. Place middleware that should run first (on the way in) earliest:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.middleware.error_handling import ErrorHandlingMiddleware
+from fastmcp.server.middleware.rate_limiting import RateLimitingMiddleware
+from fastmcp.server.middleware.timing import TimingMiddleware
+from fastmcp.server.middleware.logging import LoggingMiddleware
+
+mcp = FastMCP("Production Server")
+
+mcp.add_middleware(ErrorHandlingMiddleware()) # Catch all errors
+mcp.add_middleware(RateLimitingMiddleware(max_requests_per_second=50))
+mcp.add_middleware(TimingMiddleware())
+mcp.add_middleware(LoggingMiddleware())
+
+@mcp.tool
+def my_tool(data: str) -> str:
+ return f"Processed: {data}"
+```
+
+## Custom Middleware
+
+When the built-in middleware doesn't fit your needs—custom authentication schemes, domain-specific logging, or request transformation—subclass `Middleware` and override the hooks you need.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+
+class CustomMiddleware(Middleware):
+ async def on_request(self, context: MiddlewareContext, call_next):
+ # Pre-processing
+ print(f"→ {context.method}")
+
+ result = await call_next(context)
+
+ # Post-processing
+ print(f"← {context.method}")
+ return result
+
+mcp = FastMCP("MyServer")
+mcp.add_middleware(CustomMiddleware())
+```
+
+Override only the hooks relevant to your use case. Unoverridden hooks pass through automatically.
+
+### Denying Requests
+
+Raise the appropriate error type to stop processing and return an error to the client.
+
+```python
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+from fastmcp.exceptions import ToolError
+
+class AuthMiddleware(Middleware):
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
+ tool_name = context.message.name
+
+ if tool_name in ["delete_all", "admin_config"]:
+ raise ToolError("Access denied: requires admin privileges")
+
+ return await call_next(context)
+```
+
+| Operation | Error Type |
+|-----------|------------|
+| Tool calls | `ToolError` |
+| Resource reads | `ResourceError` |
+| Prompt retrieval | `PromptError` |
+| General requests | `McpError` |
+
+Do not return error values or skip `call_next()` to indicate errors—raise exceptions for proper error propagation.
+
+### Modifying Requests
+
+Change the message before passing it down the chain.
+
+```python
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+
+class InputSanitizer(Middleware):
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
+ if context.message.name == "search":
+ # Normalize search query
+ query = context.message.arguments.get("query", "")
+ context.message.arguments["query"] = query.strip().lower()
+
+ return await call_next(context)
+```
+
+### Modifying Responses
+
+Transform results after the handler executes.
+
+```python
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+
+class ResponseEnricher(Middleware):
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
+ result = await call_next(context)
+
+ if context.message.name == "get_data" and result.structured_content:
+ result.structured_content["processed_by"] = "enricher"
+
+ return result
+```
+
+For more complex tool transformations, consider [Transforms](/servers/transforms/transforms) instead.
+
+### Filtering Lists
+
+List operations return FastMCP objects that you can filter before they reach the client. When filtering list results, also block execution in the corresponding operation hook to maintain consistency:
+
+```python
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+from fastmcp.exceptions import ToolError
+
+class PrivateToolFilter(Middleware):
+ async def on_list_tools(self, context: MiddlewareContext, call_next):
+ tools = await call_next(context)
+ return [tool for tool in tools if "private" not in tool.tags]
+
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
+ if context.fastmcp_context:
+ tool = await context.fastmcp_context.fastmcp.get_tool(context.message.name)
+ if "private" in tool.tags:
+ raise ToolError("Tool not found")
+
+ return await call_next(context)
+```
+
+### Accessing Component Metadata
+
+During execution hooks, component metadata (like tags) isn't directly available. Look up the component through the server:
+
+```python
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+from fastmcp.exceptions import ToolError
+
+class TagBasedAuth(Middleware):
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
+ if context.fastmcp_context:
+ try:
+ tool = await context.fastmcp_context.fastmcp.get_tool(context.message.name)
+
+ if "requires-auth" in tool.tags:
+ # Check authentication here
+ pass
+
+ except Exception:
+ pass # Let execution handle missing tools
+
+ return await call_next(context)
+```
+
+The same pattern works for resources and prompts:
+
+```python
+resource = await context.fastmcp_context.fastmcp.get_resource(context.message.uri)
+prompt = await context.fastmcp_context.fastmcp.get_prompt(context.message.name)
+```
+
+### Storing State
+
+
+
+Middleware can store state that tools access later through the FastMCP context.
+
+```python
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+
+class UserMiddleware(Middleware):
+ async def on_request(self, context: MiddlewareContext, call_next):
+ # Extract user from headers (HTTP transport)
+ from fastmcp.server.dependencies import get_http_headers
+ headers = get_http_headers() or {}
+ user_id = headers.get("x-user-id", "anonymous")
+
+ # Store for tools to access
+ if context.fastmcp_context:
+ context.fastmcp_context.set_state("user_id", user_id)
+
+ return await call_next(context)
+```
+
+Tools retrieve the state:
+
+```python
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP("MyServer")
+
+@mcp.tool
+def get_user_data(ctx: Context) -> str:
+ user_id = ctx.get_state("user_id")
+ return f"Data for user: {user_id}"
+```
+
+See [Context State Management](/servers/context#state-management) for details.
+
+### Constructor Parameters
+
+Initialize middleware with configuration:
+
+```python
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+
+class ConfigurableMiddleware(Middleware):
+ def __init__(self, api_key: str, rate_limit: int = 100):
+ self.api_key = api_key
+ self.rate_limit = rate_limit
+ self.request_counts = {}
+
+ async def on_request(self, context: MiddlewareContext, call_next):
+ # Use self.api_key, self.rate_limit, etc.
+ return await call_next(context)
+
+mcp.add_middleware(ConfigurableMiddleware(
+ api_key="secret",
+ rate_limit=50
+))
+```
+
+### Error Handling in Custom Middleware
+
+Wrap `call_next()` to handle errors from downstream middleware and handlers.
+
+```python
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+
+class ErrorLogger(Middleware):
+ async def on_request(self, context: MiddlewareContext, call_next):
+ try:
+ return await call_next(context)
+ except Exception as e:
+ print(f"Error in {context.method}: {type(e).__name__}: {e}")
+ raise # Re-raise to let error propagate
+```
+
+Catching and not re-raising suppresses the error entirely. Usually you want to log and re-raise.
+
+### Audit and Event Records
+
+A common need is to emit one structured record per tool call — for audit logs, policy decisions, or offline analysis — without wrapping individual tools or storing raw payloads. `on_call_tool` is the right place: it sees the call start, the resolved `ToolResult` (so it can detect empty or error results), the duration, and can deny the call before it runs.
+
+Use [OpenTelemetry](/servers/telemetry) when the goal is to *export* spans to an observability backend. Reach for a record like this when you want a self-contained, redacted audit trail — or to drive runtime decisions from the result.
+
+```python
+import hashlib
+import json
+from datetime import datetime
+
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+from fastmcp.exceptions import ToolError
+
+
+def _schema_hash(arguments: dict | None) -> str:
+ """Stable hash of the argument shape — detects schema drift without storing values."""
+ shape = sorted(arguments or {})
+ return hashlib.sha256(json.dumps(shape).encode()).hexdigest()[:12]
+
+
+def _redact(arguments: dict | None) -> dict:
+ """Keep keys, drop values — raw inputs stay out of the default path."""
+ return {key: "" for key in (arguments or {})}
+
+
+def _call_id(context: MiddlewareContext) -> str | None:
+ """Request id when an MCP session is active (see Session Availability above)."""
+ ctx = context.fastmcp_context
+ if ctx is not None and ctx.request_context:
+ return ctx.request_id
+ return None
+
+
+class AuditMiddleware(Middleware):
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
+ record = {
+ "tool": context.message.name,
+ "call_id": _call_id(context),
+ "schema_hash": _schema_hash(context.message.arguments),
+ "arguments": _redact(context.message.arguments),
+ "received_at": context.timestamp.isoformat(),
+ }
+
+ try:
+ result = await call_next(context)
+ except Exception as exc:
+ record["status"] = "failed"
+ record["error"] = type(exc).__name__
+ self.emit(record)
+ raise
+
+ empty = not result.content and result.structured_content is None
+ record["status"] = "error" if result.is_error else "empty" if empty else "completed"
+ now = datetime.now(context.timestamp.tzinfo)
+ record["duration_ms"] = round((now - context.timestamp).total_seconds() * 1000, 2)
+ self.emit(record)
+ return result
+
+ def emit(self, record: dict) -> None:
+ # Swap in your sink: structured logger, queue, audit store, etc.
+ print(json.dumps(record))
+```
+
+Each record carries the fields downstream tools tend to need — tool name, call id, input schema hash, redacted arguments, result class (`completed` / `empty` / `error` / `failed`), and duration — while raw inputs and outputs stay out by default.
+
+To make this a policy layer, deny inside the same hook before calling `call_next`:
+
+```python
+async def on_call_tool(self, context: MiddlewareContext, call_next):
+ if not self.is_allowed(context.message.name, context.message.arguments):
+ self.emit({"tool": context.message.name, "status": "denied", "reason": "policy"})
+ raise ToolError("Call blocked by policy")
+ return await call_next(context)
+```
+
+### Complete Example
+
+Authentication middleware checking API keys for specific tools:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+from fastmcp.server.dependencies import get_http_headers
+from fastmcp.exceptions import ToolError
+
+class ApiKeyAuth(Middleware):
+ def __init__(self, valid_keys: set[str], protected_tools: set[str]):
+ self.valid_keys = valid_keys
+ self.protected_tools = protected_tools
+
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
+ tool_name = context.message.name
+
+ if tool_name not in self.protected_tools:
+ return await call_next(context)
+
+ headers = get_http_headers() or {}
+ api_key = headers.get("x-api-key")
+
+ if api_key not in self.valid_keys:
+ raise ToolError(f"Invalid API key for protected tool: {tool_name}")
+
+ return await call_next(context)
+
+mcp = FastMCP("Secure Server")
+mcp.add_middleware(ApiKeyAuth(
+ valid_keys={"key-1", "key-2"},
+ protected_tools={"delete_user", "admin_panel"}
+))
+
+@mcp.tool
+def delete_user(user_id: str) -> str:
+ return f"Deleted user {user_id}"
+
+@mcp.tool
+def get_user(user_id: str) -> str:
+ return f"User {user_id}" # Not protected
+```
diff --git a/docs/v3/servers/pagination.mdx b/docs/v3/servers/pagination.mdx
new file mode 100644
index 000000000..97ad2c7a2
--- /dev/null
+++ b/docs/v3/servers/pagination.mdx
@@ -0,0 +1,93 @@
+---
+title: Pagination
+sidebarTitle: Pagination
+description: Control how servers return large lists of components to clients.
+icon: page
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+When a server exposes many tools, resources, or prompts, returning them all in a single response can be impractical. MCP supports pagination for list operations, allowing servers to return results in manageable chunks that clients can fetch incrementally.
+
+## Server Configuration
+
+By default, FastMCP servers return all components in a single response for backward compatibility. To enable pagination, set the `list_page_size` parameter when creating your server. This value determines the maximum number of items returned per page across all list operations.
+
+```python
+from fastmcp import FastMCP
+
+# Enable pagination with 50 items per page
+server = FastMCP("ComponentRegistry", list_page_size=50)
+
+# Register tools (in practice, these might come from a database or config)
+@server.tool
+def search(query: str) -> str:
+ return f"Results for: {query}"
+
+@server.tool
+def analyze(data: str) -> dict:
+ return {"status": "analyzed", "data": data}
+
+# ... many more tools, resources, prompts
+```
+
+When `list_page_size` is configured, the `tools/list`, `resources/list`, `resources/templates/list`, and `prompts/list` endpoints all paginate their responses. Each response includes a `nextCursor` field when more results exist, which clients use to fetch subsequent pages.
+
+### Cursor Format
+
+Cursors are opaque base64-encoded strings per the MCP specification. Clients should treat them as black boxes, passing them unchanged between requests. The cursor encodes the offset into the result set, but this is an implementation detail that may change.
+
+## Client Behavior
+
+The FastMCP Client handles pagination transparently. Convenience methods like `list_tools()`, `list_resources()`, `list_resource_templates()`, and `list_prompts()` automatically fetch all pages and return the complete list. Existing code continues to work without modification.
+
+```python
+from fastmcp import Client
+
+async with Client(server) as client:
+ # Returns all 200 tools, fetching pages automatically
+ tools = await client.list_tools()
+ print(f"Total tools: {len(tools)}") # 200
+```
+
+### Manual Pagination
+
+For scenarios where you want to process results incrementally (memory-constrained environments, progress reporting, or early termination), use the `_mcp` variants with explicit cursor handling.
+
+```python
+from fastmcp import Client
+
+async with Client(server) as client:
+ # Fetch first page
+ result = await client.list_tools_mcp()
+ print(f"Page 1: {len(result.tools)} tools")
+
+ # Continue fetching while more pages exist
+ while result.nextCursor:
+ result = await client.list_tools_mcp(cursor=result.nextCursor)
+ print(f"Next page: {len(result.tools)} tools")
+```
+
+The `_mcp` methods return the raw MCP protocol objects, which include both the items and the `nextCursor` for the next page. When `nextCursor` is `None`, you've reached the end of the result set.
+
+All four list operations support manual pagination:
+
+| Operation | Convenience Method | Manual Method |
+|-----------|-------------------|---------------|
+| Tools | `list_tools()` | `list_tools_mcp(cursor=...)` |
+| Resources | `list_resources()` | `list_resources_mcp(cursor=...)` |
+| Resource Templates | `list_resource_templates()` | `list_resource_templates_mcp(cursor=...)` |
+| Prompts | `list_prompts()` | `list_prompts_mcp(cursor=...)` |
+
+## When to Use Pagination
+
+Pagination becomes valuable when your server exposes a large number of components. Consider enabling it when:
+
+- Your server dynamically generates many components (e.g., from a database or file system)
+- Memory usage is a concern for clients
+- You want to reduce initial response latency
+
+For servers with a fixed, modest number of components (fewer than 100), pagination adds complexity without meaningful benefit. The default behavior of returning everything in one response is simpler and efficient for typical use cases.
diff --git a/docs/v3/servers/progress.mdx b/docs/v3/servers/progress.mdx
new file mode 100644
index 000000000..9600a05ce
--- /dev/null
+++ b/docs/v3/servers/progress.mdx
@@ -0,0 +1,51 @@
+---
+title: Progress Reporting
+sidebarTitle: Progress
+description: Update clients on the progress of long-running operations through the MCP context.
+icon: chart-line
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+Progress reporting allows MCP tools to notify clients about the progress of long-running operations. Clients can display progress indicators and provide better user experience during time-consuming tasks.
+
+## Basic Usage
+
+Use `ctx.report_progress()` to send progress updates to the client. The method accepts a `progress` value representing how much work is complete, and an optional `total` representing the full scope of work.
+
+```python
+from fastmcp import FastMCP, Context
+import asyncio
+
+mcp = FastMCP("ProgressDemo")
+
+@mcp.tool
+async def process_items(items: list[str], ctx: Context) -> dict:
+ """Process a list of items with progress updates."""
+ total = len(items)
+ results = []
+
+ for i, item in enumerate(items):
+ await ctx.report_progress(progress=i, total=total)
+ await asyncio.sleep(0.1)
+ results.append(item.upper())
+
+ await ctx.report_progress(progress=total, total=total)
+ return {"processed": len(results), "results": results}
+```
+
+## Progress Patterns
+
+| Pattern | Description | Example |
+|---------|-------------|---------|
+| Percentage | Progress as 0-100 percentage | `progress=75, total=100` |
+| Absolute | Completed items of a known count | `progress=3, total=10` |
+| Indeterminate | Progress without known endpoint | `progress=files_found` (no total) |
+
+For multi-stage operations, map each stage to a portion of the total progress range. A four-stage operation might allocate 0-25% to validation, 25-60% to export, 60-80% to transform, and 80-100% to import.
+
+## Client Requirements
+
+Progress reporting requires clients to support progress handling. Clients must send a `progressToken` in the initial request to receive progress updates. If no progress token is provided, progress calls have no effect (they don't error).
+
+See [Client Progress](/clients/progress) for details on implementing client-side progress handling.
diff --git a/docs/v3/servers/prompts.mdx b/docs/v3/servers/prompts.mdx
new file mode 100644
index 000000000..b5cf2f6e9
--- /dev/null
+++ b/docs/v3/servers/prompts.mdx
@@ -0,0 +1,481 @@
+---
+title: Prompts
+sidebarTitle: Prompts
+description: Create reusable, parameterized prompt templates for MCP clients.
+icon: message-lines
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+Prompts are reusable message templates that help LLMs generate structured, purposeful responses. FastMCP simplifies defining these templates, primarily using the `@mcp.prompt` decorator.
+
+## What Are Prompts?
+
+Prompts provide parameterized message templates for LLMs. When a client requests a prompt:
+
+1. FastMCP finds the corresponding prompt definition.
+2. If it has parameters, they are validated against your function signature.
+3. Your function executes with the validated inputs.
+4. The generated message(s) are returned to the LLM to guide its response.
+
+This allows you to define consistent, reusable templates that LLMs can use across different clients and contexts.
+
+## Prompts
+
+### The `@prompt` Decorator
+
+The most common way to define a prompt is by decorating a Python function. The decorator uses the function name as the prompt's identifier.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.prompts import Message
+
+mcp = FastMCP(name="PromptServer")
+
+# Basic prompt returning a string (converted to user message automatically)
+@mcp.prompt
+def ask_about_topic(topic: str) -> str:
+ """Generates a user message asking for an explanation of a topic."""
+ return f"Can you please explain the concept of '{topic}'?"
+
+# Prompt returning multiple messages
+@mcp.prompt
+def generate_code_request(language: str, task_description: str) -> list[Message]:
+ """Generates a conversation for code generation."""
+ return [
+ Message(f"Write a {language} function that performs the following task: {task_description}"),
+ Message("I'll help you write that function.", role="assistant"),
+ ]
+```
+
+**Key Concepts:**
+
+* **Name:** By default, the prompt name is taken from the function name.
+* **Parameters:** The function parameters define the inputs needed to generate the prompt.
+* **Inferred Metadata:** By default:
+ * Prompt Name: Taken from the function name (`ask_about_topic`).
+ * Prompt Description: Taken from the summary of the function's docstring. If the docstring includes parameter descriptions (Google, NumPy, or Sphinx style), they populate each prompt argument's description in the MCP protocol (see [Argument Descriptions](#argument-descriptions)).
+
+Functions with `*args` or `**kwargs` are not supported as prompts. This restriction exists because FastMCP needs to generate a complete parameter schema for the MCP protocol, which isn't possible with variable argument lists.
+
+
+#### Decorator Arguments
+
+While FastMCP infers the name and description from your function, you can override these and add additional metadata using arguments to the `@mcp.prompt` decorator:
+
+```python
+@mcp.prompt(
+ name="analyze_data_request", # Custom prompt name
+ description="Creates a request to analyze data with specific parameters", # Custom description
+ tags={"analysis", "data"}, # Optional categorization tags
+ meta={"version": "1.1", "author": "data-team"} # Custom metadata
+)
+def data_analysis_prompt(
+ data_uri: str = Field(description="The URI of the resource containing the data."),
+ analysis_type: str = Field(default="summary", description="Type of analysis.")
+) -> str:
+ """This docstring is ignored when description is provided."""
+ return f"Please perform a '{analysis_type}' analysis on the data found at {data_uri}."
+```
+
+
+
+ Sets the explicit prompt name exposed via MCP. If not provided, uses the function name
+
+
+
+ A human-readable title for the prompt
+
+
+
+ Provides the description exposed via MCP. If set, the function's docstring is ignored for the prompt description, though docstring-derived argument descriptions still apply (see [Argument Descriptions](#argument-descriptions)).
+
+
+
+ A set of strings used to categorize the prompt. These can be used by the server and, in some cases, by clients to filter or group available prompts.
+
+
+
+ Deprecated in v3.0.0. Use `mcp.enable()` / `mcp.disable()` at the server level instead.
+ A boolean to enable or disable the prompt. See [Component Visibility](#component-visibility) for the recommended approach.
+
+
+
+
+
+ Optional list of icon representations for this prompt. See [Icons](/servers/icons) for detailed examples
+
+
+
+
+
+ Optional meta information about the prompt. This data is passed through to the MCP client as the `meta` field of the client-side prompt object and can be used for custom metadata, versioning, or other application-specific purposes.
+
+
+
+
+
+ Optional version identifier for this prompt. See [Versioning](/servers/versioning) for details.
+
+
+
+#### Using with Methods
+
+For decorating instance or class methods, use the standalone `@prompt` decorator and register the bound method. See [Tools: Using with Methods](/servers/tools#using-with-methods) for the pattern.
+
+### Argument Types
+
+
+
+The MCP specification requires that all prompt arguments be passed as strings, but FastMCP allows you to use typed annotations for better developer experience. When you use complex types like `list[int]` or `dict[str, str]`, FastMCP:
+
+1. **Automatically converts** string arguments from MCP clients to the expected types
+2. **Generates helpful descriptions** showing the exact JSON string format needed
+3. **Preserves direct usage** - you can still call prompts with properly typed arguments
+
+Since the MCP specification only allows string arguments, clients need to know what string format to use for complex types. FastMCP solves this by automatically enhancing the argument descriptions with JSON schema information, making it clear to both humans and LLMs how to format their arguments.
+
+
+
+```python Python Code
+@mcp.prompt
+def analyze_data(
+ numbers: list[int],
+ metadata: dict[str, str],
+ threshold: float
+) -> str:
+ """Analyze numerical data."""
+ avg = sum(numbers) / len(numbers)
+ return f"Average: {avg}, above threshold: {avg > threshold}"
+```
+
+```json Resulting MCP Prompt
+{
+ "name": "analyze_data",
+ "description": "Analyze numerical data.",
+ "arguments": [
+ {
+ "name": "numbers",
+ "description": "Provide as a JSON string matching the following schema: {\"items\":{\"type\":\"integer\"},\"type\":\"array\"}",
+ "required": true
+ },
+ {
+ "name": "metadata",
+ "description": "Provide as a JSON string matching the following schema: {\"additionalProperties\":{\"type\":\"string\"},\"type\":\"object\"}",
+ "required": true
+ },
+ {
+ "name": "threshold",
+ "description": "Provide as a JSON string matching the following schema: {\"type\":\"number\"}",
+ "required": true
+ }
+ ]
+}
+```
+
+
+
+**MCP clients will call this prompt with string arguments:**
+```json
+{
+ "numbers": "[1, 2, 3, 4, 5]",
+ "metadata": "{\"source\": \"api\", \"version\": \"1.0\"}",
+ "threshold": "2.5"
+}
+```
+
+**But you can still call it directly with proper types:**
+```python
+# This also works for direct calls
+result = await prompt.render({
+ "numbers": [1, 2, 3, 4, 5],
+ "metadata": {"source": "api", "version": "1.0"},
+ "threshold": 2.5
+})
+```
+
+
+Keep your type annotations simple when using this feature. Complex nested types or custom classes may not convert reliably from JSON strings. The automatically generated schema descriptions are the only guidance users receive about the expected format.
+
+Good choices: `list[int]`, `dict[str, str]`, `float`, `bool`
+Avoid: Complex Pydantic models, deeply nested structures, custom classes
+
+
+### Argument Descriptions
+
+
+
+FastMCP parses your function's docstring to extract the prompt description and per-argument descriptions. Google, NumPy, and Sphinx styles are all supported:
+
+```python
+@mcp.prompt
+def analyze_data(dataset: str, method: str = "summary") -> str:
+ """Generate an analysis prompt for a dataset.
+
+ Args:
+ dataset: URI or identifier of the dataset to analyze.
+ method: Type of analysis to perform (summary, detailed, etc).
+ """
+ return f"Please perform a '{method}' analysis on {dataset}."
+```
+
+The free-form text above the `Args` section — whether a single line or multiple paragraphs — becomes the prompt description, and each argument's docstring entry becomes the description on the corresponding `PromptArgument` in the MCP protocol. Sections like `Returns`, `Raises`, and `Example` are excluded from the description but otherwise ignored.
+
+If an argument already has an explicit description — via `Annotated[x, "..."]` or `Field(description=...)` — that description takes precedence over the docstring. This makes it safe to adopt docstring-based descriptions incrementally: existing annotations keep working, and docstrings fill in the gaps.
+
+### Return Values
+
+Prompt functions must return one of these types:
+
+- **`str`**: Sent as a single user message.
+- **`list[Message | str]`**: A sequence of messages (a conversation). Strings are auto-converted to user Messages.
+- **`PromptResult`**: Full control over messages, description, and metadata. See [PromptResult](#promptresult) below.
+
+```python
+from fastmcp.prompts import Message
+
+@mcp.prompt
+def roleplay_scenario(character: str, situation: str) -> list[Message]:
+ """Sets up a roleplaying scenario with initial messages."""
+ return [
+ Message(f"Let's roleplay. You are {character}. The situation is: {situation}"),
+ Message("Okay, I understand. I am ready. What happens next?", role="assistant")
+ ]
+```
+
+#### Message
+
+
+
+`Message` provides a user-friendly wrapper for prompt messages with automatic serialization.
+
+```python
+from fastmcp.prompts import Message
+
+# String content (user role by default)
+Message("Hello, world!")
+
+# Explicit role
+Message("I can help with that.", role="assistant")
+
+# Auto-serialized to JSON text
+Message({"key": "value"})
+Message(["item1", "item2"])
+```
+
+`Message` accepts two fields:
+
+**`content`** - The message content. Strings pass through directly. Other types (dict, list, BaseModel) are automatically JSON-serialized to text.
+
+**`role`** - The message role, either `"user"` (default) or `"assistant"`.
+
+
+
+ The content data. Strings pass through directly. Other types (dict, list, BaseModel) are automatically JSON-serialized.
+
+
+ The message role.
+
+
+
+#### PromptResult
+
+
+
+`PromptResult` gives you explicit control over prompt responses: multiple messages, roles, and metadata at both the message and result level.
+
+```python test="skip"
+from fastmcp import FastMCP
+from fastmcp.prompts import PromptResult, Message
+
+mcp = FastMCP(name="PromptServer")
+
+@mcp.prompt
+def code_review(code: str) -> PromptResult:
+ """Returns a code review prompt with metadata."""
+ return PromptResult(
+ messages=[
+ Message(f"Please review this code:\n\n```\n{code}\n```"),
+ Message("I'll analyze this code for issues.", role="assistant"),
+ ],
+ description="Code review prompt",
+ meta={"review_type": "security", "priority": "high"}
+ )
+```
+
+For simple cases, you can pass a string directly to `PromptResult`:
+
+```python
+return PromptResult("Please help me with this task") # auto-converts to single Message
+```
+
+
+
+ Messages to return. Strings are wrapped as a single user Message.
+
+
+ Optional description of the prompt result. If not provided, defaults to the prompt's docstring.
+
+
+ Result-level metadata, included in the MCP response's `_meta` field. Use this for runtime metadata like categorization, priority, or other client-specific data.
+
+
+
+
+The `meta` field in `PromptResult` is for runtime metadata specific to this render response. This is separate from the `meta` parameter in `@mcp.prompt(meta={...})`, which provides static metadata about the prompt definition itself (returned when listing prompts).
+
+
+You can still return plain `str` or `list[Message | str]` from your prompt functions—`PromptResult` is opt-in for when you need to include metadata.
+
+### Required vs. Optional Parameters
+
+Parameters in your function signature are considered **required** unless they have a default value.
+
+```python
+@mcp.prompt
+def data_analysis_prompt(
+ data_uri: str, # Required - no default value
+ analysis_type: str = "summary", # Optional - has default value
+ include_charts: bool = False # Optional - has default value
+) -> str:
+ """Creates a request to analyze data with specific parameters."""
+ prompt = f"Please perform a '{analysis_type}' analysis on the data found at {data_uri}."
+ if include_charts:
+ prompt += " Include relevant charts and visualizations."
+ return prompt
+```
+
+In this example, the client *must* provide `data_uri`. If `analysis_type` or `include_charts` are omitted, their default values will be used.
+
+### Component Visibility
+
+
+
+You can control which prompts are enabled for clients using server-level enabled control. Disabled prompts don't appear in `list_prompts` and can't be called.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("MyServer")
+
+@mcp.prompt(tags={"public"})
+def public_prompt(topic: str) -> str:
+ return f"Discuss: {topic}"
+
+@mcp.prompt(tags={"internal"})
+def internal_prompt() -> str:
+ return "Internal system prompt"
+
+# Disable specific prompts by key
+mcp.disable(keys={"prompt:internal_prompt"})
+
+# Disable prompts by tag
+mcp.disable(tags={"internal"})
+
+# Or use allowlist mode - only enable prompts with specific tags
+mcp.enable(tags={"public"}, only=True)
+```
+
+See [Visibility](/servers/visibility) for the complete visibility control API including key formats, tag-based filtering, and provider-level control.
+
+### Async Prompts
+
+FastMCP supports both standard (`def`) and asynchronous (`async def`) functions as prompts. Synchronous functions automatically run in a threadpool to avoid blocking the event loop.
+
+```python
+# Synchronous prompt (runs in threadpool)
+@mcp.prompt
+def simple_question(question: str) -> str:
+ """Generates a simple question to ask the LLM."""
+ return f"Question: {question}"
+
+# Asynchronous prompt
+@mcp.prompt
+async def data_based_prompt(data_id: str) -> str:
+ """Generates a prompt based on data that needs to be fetched."""
+ # In a real scenario, you might fetch data from a database or API
+ async with aiohttp.ClientSession() as session:
+ async with session.get(f"https://api.example.com/data/{data_id}") as response:
+ data = await response.json()
+ return f"Analyze this data: {data['content']}"
+```
+
+Use `async def` when your prompt function performs I/O operations like network requests or database queries, since async is more efficient than threadpool dispatch.
+
+### Accessing MCP Context
+
+
+
+Prompts can access additional MCP information and features through the `Context` object. To access it, add a parameter to your prompt function with a type annotation of `Context`:
+
+```python {6}
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP(name="PromptServer")
+
+@mcp.prompt
+async def generate_report_request(report_type: str, ctx: Context) -> str:
+ """Generates a request for a report."""
+ return f"Please create a {report_type} report. Request ID: {ctx.request_id}"
+```
+
+For full documentation on the Context object and all its capabilities, see the [Context documentation](/servers/context).
+
+### Notifications
+
+
+
+FastMCP automatically sends `notifications/prompts/list_changed` notifications to connected clients when prompts are added, enabled, or disabled. This allows clients to stay up-to-date with the current prompt set without manually polling for changes.
+
+```python
+@mcp.prompt
+def example_prompt() -> str:
+ return "Hello!"
+
+# These operations trigger notifications:
+mcp.add_prompt(example_prompt) # Sends prompts/list_changed notification
+mcp.disable(keys={"prompt:example_prompt"}) # Sends prompts/list_changed notification
+mcp.enable(keys={"prompt:example_prompt"}) # Sends prompts/list_changed notification
+```
+
+Notifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications.
+
+Clients can handle these notifications using a [message handler](/clients/notifications) to automatically refresh their prompt lists or update their interfaces.
+
+## Server Behavior
+
+### Duplicate Prompts
+
+
+
+You can configure how the FastMCP server handles attempts to register multiple prompts with the same name. Use the `on_duplicate_prompts` setting during `FastMCP` initialization.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP(
+ name="PromptServer",
+ on_duplicate_prompts="error" # Raise an error if a prompt name is duplicated
+)
+
+@mcp.prompt
+def greeting(): return "Hello, how can I help you today?"
+
+# This registration attempt will raise a ValueError because
+# "greeting" is already registered and the behavior is "error".
+# @mcp.prompt
+# def greeting(): return "Hi there! What can I do for you?"
+```
+
+The duplicate behavior options are:
+
+- `"warn"` (default): Logs a warning, and the new prompt replaces the old one.
+- `"error"`: Raises a `ValueError`, preventing the duplicate registration.
+- `"replace"`: Silently replaces the existing prompt with the new one.
+- `"ignore"`: Keeps the original prompt and ignores the new registration attempt.
+
+## Versioning
+
+
+
+Prompts support versioning, allowing you to maintain multiple implementations under the same name while clients automatically receive the highest version. See [Versioning](/servers/versioning) for complete documentation on version comparison, retrieval, and migration patterns.
diff --git a/docs/v3/servers/providers/custom.mdx b/docs/v3/servers/providers/custom.mdx
new file mode 100644
index 000000000..f5673c683
--- /dev/null
+++ b/docs/v3/servers/providers/custom.mdx
@@ -0,0 +1,245 @@
+---
+title: Custom Providers
+sidebarTitle: Custom
+description: Build providers that source components from any data source
+icon: code
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Custom providers let you source components from anywhere - databases, APIs, configuration systems, or dynamic runtime logic. If you can write Python code to fetch or generate a component, you can wrap it in a provider.
+
+## When to Build Custom
+
+The built-in providers handle common cases: decorators (`LocalProvider`), composition (`FastMCPProvider`), and proxying (`ProxyProvider`). Build a custom provider when your components come from somewhere else:
+
+- **Database-backed tools**: Admin users define tools in a database, and your server exposes them dynamically
+- **API-backed resources**: Resources that fetch content from external services on demand
+- **Configuration-driven components**: Components loaded from YAML/JSON config files at startup
+- **Multi-tenant systems**: Different users see different tools based on their permissions
+- **Plugin systems**: Third-party code registers components at runtime
+
+## Providers vs Middleware
+
+Both providers and [middleware](/servers/middleware) can influence what components a client sees, but they work at different levels.
+
+**Providers** are objects that source components. They make it easy to reason about where tools, resources, and prompts come from - a database, another server, an API.
+
+**Middleware** intercepts individual requests. It's well-suited for request-specific decisions like logging, rate limiting, or authentication.
+
+You *could* use middleware to dynamically add tools based on request context. But it's often cleaner to have a provider source all possible tools, then use middleware or [visibility controls](/servers/visibility) to filter what each request can see. This separation makes it easier to reason about how components are sourced and how they interact with other server machinery.
+
+## The Provider Interface
+
+A provider implements protected `_list_*` methods that return available components. The public `list_*` methods handle transforms automatically - you override the underscore-prefixed versions:
+
+```python
+from collections.abc import Sequence
+from fastmcp.server.providers import Provider
+from fastmcp.tools import Tool
+from fastmcp.resources import Resource
+from fastmcp.prompts import Prompt
+
+class MyProvider(Provider):
+ async def _list_tools(self) -> Sequence[Tool]:
+ """Return all tools this provider offers."""
+ return []
+
+ async def _list_resources(self) -> Sequence[Resource]:
+ """Return all resources this provider offers."""
+ return []
+
+ async def _list_prompts(self) -> Sequence[Prompt]:
+ """Return all prompts this provider offers."""
+ return []
+```
+
+You only need to implement the methods for component types you provide. The base class returns empty sequences by default.
+
+The `_get_*` methods (`_get_tool`, `_get_resource`, `_get_prompt`) have default implementations that search through the list results. Override them only if you can fetch individual components more efficiently than iterating the full list.
+
+## What Providers Return
+
+Providers return component objects that are ready to use. When a client calls a tool, FastMCP invokes the tool's function - your provider isn't involved in execution. This means the `Tool`, `Resource`, or `Prompt` you return must actually work.
+
+The easiest way to create components is from functions:
+
+```python
+from fastmcp.tools import Tool
+
+def add(a: int, b: int) -> int:
+ """Add two numbers."""
+ return a + b
+
+tool = Tool.from_function(add)
+```
+
+The function's type hints become the input schema, and the docstring becomes the description. You can override these:
+
+```python
+tool = Tool.from_function(
+ add,
+ name="calculator_add",
+ description="Add two integers together"
+)
+```
+
+Similar `from_function` methods exist for `Resource` and `Prompt`.
+
+## Registering Providers
+
+Add providers when creating the server:
+
+```python
+mcp = FastMCP(
+ "MyServer",
+ providers=[
+ DatabaseProvider(db_url),
+ ConfigProvider(config_path),
+ ]
+)
+```
+
+Or add them after creation:
+
+```python
+mcp = FastMCP("MyServer")
+mcp.add_provider(DatabaseProvider(db_url))
+```
+
+## A Simple Provider
+
+Here's a minimal provider that serves tools from a dictionary:
+
+```python
+from collections.abc import Callable, Sequence
+from fastmcp import FastMCP
+from fastmcp.server.providers import Provider
+from fastmcp.tools import Tool
+
+class DictProvider(Provider):
+ def __init__(self, tools: dict[str, Callable]):
+ super().__init__()
+ self._tools = [
+ Tool.from_function(fn, name=name)
+ for name, fn in tools.items()
+ ]
+
+ async def _list_tools(self) -> Sequence[Tool]:
+ return self._tools
+```
+
+Use it like this:
+
+```python
+def add(a: int, b: int) -> int:
+ """Add two numbers."""
+ return a + b
+
+def multiply(a: int, b: int) -> int:
+ """Multiply two numbers."""
+ return a * b
+
+mcp = FastMCP("Calculator", providers=[
+ DictProvider({"add": add, "multiply": multiply})
+])
+```
+
+## Lifecycle Management
+
+Providers often need to set up connections when the server starts and clean them up when it stops. Override the `lifespan` method:
+
+```python
+from contextlib import asynccontextmanager
+from collections.abc import AsyncIterator, Sequence
+
+class DatabaseProvider(Provider):
+ def __init__(self, db_url: str):
+ super().__init__()
+ self.db_url = db_url
+ self.db = None
+
+ @asynccontextmanager
+ async def lifespan(self) -> AsyncIterator[None]:
+ self.db = await connect_database(self.db_url)
+ try:
+ yield
+ finally:
+ await self.db.close()
+
+ async def _list_tools(self) -> Sequence[Tool]:
+ rows = await self.db.fetch("SELECT * FROM tools")
+ return [self._make_tool(row) for row in rows]
+```
+
+FastMCP calls your provider's `lifespan` during server startup and shutdown. The connection is available to your methods while the server runs.
+
+## Full Example: API-Backed Resources
+
+Here's a complete provider that fetches resources from an external REST API:
+
+```python
+from contextlib import asynccontextmanager
+from collections.abc import AsyncIterator, Sequence
+from fastmcp.server.providers import Provider
+from fastmcp.resources import Resource
+import httpx
+
+class ApiResourceProvider(Provider):
+ """Provides resources backed by an external API."""
+
+ def __init__(self, base_url: str, api_key: str):
+ super().__init__()
+ self.base_url = base_url
+ self.api_key = api_key
+ self.client = None
+
+ @asynccontextmanager
+ async def lifespan(self) -> AsyncIterator[None]:
+ self.client = httpx.AsyncClient(
+ base_url=self.base_url,
+ headers={"Authorization": f"Bearer {self.api_key}"}
+ )
+ try:
+ yield
+ finally:
+ await self.client.aclose()
+
+ async def _list_resources(self) -> Sequence[Resource]:
+ response = await self.client.get("/resources")
+ response.raise_for_status()
+ return [
+ self._make_resource(item)
+ for item in response.json()["items"]
+ ]
+
+ def _make_resource(self, data: dict) -> Resource:
+ resource_id = data["id"]
+
+ async def read_content() -> str:
+ response = await self.client.get(
+ f"/resources/{resource_id}/content"
+ )
+ return response.text
+
+ return Resource.from_function(
+ read_content,
+ uri=f"api://resources/{resource_id}",
+ name=data["name"],
+ description=data.get("description", ""),
+ mime_type=data.get("mime_type", "text/plain")
+ )
+```
+
+Register it like any other provider:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("API Resources", providers=[
+ ApiResourceProvider("https://api.example.com", "my-api-key")
+])
+```
diff --git a/docs/v3/servers/providers/filesystem.mdx b/docs/v3/servers/providers/filesystem.mdx
new file mode 100644
index 000000000..353a671d5
--- /dev/null
+++ b/docs/v3/servers/providers/filesystem.mdx
@@ -0,0 +1,256 @@
+---
+title: Filesystem Provider
+sidebarTitle: Filesystem
+description: Automatic component discovery from Python files
+icon: folder-tree
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+`FileSystemProvider` scans a directory for Python files and automatically registers functions decorated with `@tool`, `@resource`, or `@prompt`. This enables a file-based organization pattern similar to Next.js routing, where your project structure becomes your component registry.
+
+## Why Filesystem Discovery
+
+Traditional FastMCP servers require coordination between files. Either your tool files import the server to call `@server.tool()`, or your server file imports all the tool modules. Both approaches create coupling that some developers prefer to avoid.
+
+`FileSystemProvider` eliminates this coordination. Each file is self-contained—it uses standalone decorators (`@tool`, `@resource`, `@prompt`) that don't require access to a server instance. The provider discovers these files at startup, so you can add new tools without modifying your server file.
+
+This is a convention some teams prefer, not necessarily better for all projects. The tradeoffs:
+
+- **No coordination**: Files don't import the server; server doesn't import files
+- **Predictable naming**: Function names become component names (unless overridden)
+- **Development mode**: Optionally re-scan files on every request for rapid iteration
+
+## Quick Start
+
+Create a provider pointing to your components directory, then pass it to your server. Use `Path(__file__).parent` to make the path relative to your server file.
+
+```python
+from pathlib import Path
+
+from fastmcp import FastMCP
+from fastmcp.server.providers import FileSystemProvider
+
+mcp = FastMCP("MyServer", providers=[FileSystemProvider(Path(__file__).parent / "components")])
+```
+
+In your `components/` directory, create Python files with decorated functions.
+
+```python
+# components/tools/greet.py
+from fastmcp.tools import tool
+
+@tool
+def greet(name: str) -> str:
+ """Greet someone by name."""
+ return f"Hello, {name}!"
+```
+
+When the server starts, `FileSystemProvider` scans the directory, imports all Python files, and registers any decorated functions it finds.
+
+## Decorators
+
+FastMCP provides standalone decorators that mark functions for discovery: `@tool` from `fastmcp.tools`, `@resource` from `fastmcp.resources`, and `@prompt` from `fastmcp.prompts`. These support the full syntax of server-bound decorators—all the same parameters work identically.
+
+### @tool
+
+Mark a function as a tool. The function name becomes the tool name by default.
+
+```python
+from fastmcp.tools import tool
+
+@tool
+def calculate_sum(a: float, b: float) -> float:
+ """Add two numbers together."""
+ return a + b
+```
+
+Customize the tool with optional parameters.
+
+```python
+from fastmcp.tools import tool
+
+@tool(
+ name="add-numbers",
+ description="Add two numbers together.",
+ tags={"math", "arithmetic"},
+)
+def add(a: float, b: float) -> float:
+ return a + b
+```
+
+The decorator supports all standard tool options: `name`, `title`, `description`, `icons`, `tags`, `output_schema`, `annotations`, and `meta`.
+
+### @resource
+
+Mark a function as a resource. Unlike `@tool`, the `@resource` decorator requires a URI argument.
+
+```python
+from fastmcp.resources import resource
+
+@resource("config://app")
+def get_app_config() -> str:
+ """Get application configuration."""
+ return '{"version": "1.0"}'
+```
+
+URIs with template parameters create resource templates. The provider automatically detects whether to register a static resource or a template based on whether the URI contains `{parameters}` or the function has arguments.
+
+```python
+from fastmcp.resources import resource
+
+@resource("users://{user_id}/profile")
+def get_user_profile(user_id: str) -> str:
+ """Get a user's profile by ID."""
+ return f'{{"id": "{user_id}", "name": "User"}}'
+```
+
+The decorator supports: `uri` (required), `name`, `title`, `description`, `icons`, `mime_type`, `tags`, `annotations`, and `meta`.
+
+### @prompt
+
+Mark a function as a prompt template.
+
+```python test="skip"
+from fastmcp.prompts import prompt
+
+@prompt
+def code_review(code: str, language: str = "python") -> str:
+ """Generate a code review prompt."""
+ return f"Please review this {language} code:\n\n```{language}\n{code}\n```"
+```
+
+```python
+from fastmcp.prompts import prompt
+
+@prompt(name="explain-concept", tags={"education"})
+def explain(topic: str) -> str:
+ """Generate an explanation prompt."""
+ return f"Explain {topic} using clear examples and analogies."
+```
+
+The decorator supports: `name`, `title`, `description`, `icons`, `tags`, and `meta`.
+
+## Directory Structure
+
+The directory structure is purely organizational. The provider recursively scans all `.py` files regardless of which subdirectory they're in. Subdirectories like `tools/`, `resources/`, and `prompts/` are optional conventions that help you organize code.
+
+```
+components/
+├── tools/
+│ ├── greeting.py # @tool functions
+│ └── calculator.py # @tool functions
+├── resources/
+│ └── config.py # @resource functions
+└── prompts/
+ └── assistant.py # @prompt functions
+```
+
+You can also put all components in a single file or organize by feature rather than type.
+
+```
+components/
+├── user_management.py # @tool, @resource, @prompt for users
+├── billing.py # @tool, @resource for billing
+└── analytics.py # @tool for analytics
+```
+
+## Discovery Rules
+
+The provider follows these rules when scanning:
+
+| Rule | Behavior |
+|------|----------|
+| File extensions | Only `.py` files are scanned |
+| `__init__.py` | Skipped (used for package structure, not components) |
+| `__pycache__` | Skipped |
+| Private functions | Functions starting with `_` are ignored, even if decorated |
+| No decorators | Files without `@tool`, `@resource`, or `@prompt` are silently skipped |
+| Multiple components | A single file can contain any number of decorated functions |
+
+### Package Imports
+
+If your directory contains an `__init__.py` file, the provider imports files as proper Python package members. This means relative imports work correctly within your components directory.
+
+```python
+# components/__init__.py exists
+
+# components/tools/greeting.py
+from ..helpers import format_name # Relative imports work
+
+@tool
+def greet(name: str) -> str:
+ return f"Hello, {format_name(name)}!"
+```
+
+Without `__init__.py`, files are imported directly using `importlib.util.spec_from_file_location`.
+
+## Reload Mode
+
+During development, you may want changes to component files to take effect without restarting the server. Enable reload mode to re-scan the directory on every request.
+
+```python
+from pathlib import Path
+
+from fastmcp.server.providers import FileSystemProvider
+
+provider = FileSystemProvider(Path(__file__).parent / "components", reload=True)
+```
+
+With `reload=True`, the provider:
+
+1. Re-discovers all Python files on each request
+2. Re-imports modules that have changed
+3. Updates the component registry with any new, modified, or removed components
+
+
+Reload mode adds overhead to every request. Use it only during development, not in production.
+
+
+## Error Handling
+
+When a file fails to import (syntax error, missing dependency, etc.), the provider logs a warning and continues scanning other files. Failed imports don't prevent the server from starting.
+
+```
+WARNING - Failed to import /path/to/broken.py: No module named 'missing_dep'
+```
+
+The provider tracks which files have failed and only re-logs warnings when the file's modification time changes. This prevents log spam when a broken file is repeatedly scanned in reload mode.
+
+## Example Project
+
+A complete example is available in the repository at `examples/filesystem-provider/`. The structure demonstrates the recommended organization.
+
+```
+examples/filesystem-provider/
+├── server.py # Server entry point
+└── components/
+ ├── tools/
+ │ ├── greeting.py # greet, farewell tools
+ │ └── calculator.py # add, multiply tools
+ ├── resources/
+ │ └── config.py # Static and templated resources
+ └── prompts/
+ └── assistant.py # code_review, explain prompts
+```
+
+The server entry point is minimal.
+
+```python
+from pathlib import Path
+
+from fastmcp import FastMCP
+from fastmcp.server.providers import FileSystemProvider
+
+provider = FileSystemProvider(
+ root=Path(__file__).parent / "components",
+ reload=True,
+)
+
+mcp = FastMCP("FilesystemDemo", providers=[provider])
+```
+
+Run with `fastmcp run examples/filesystem-provider/server.py` or inspect with `fastmcp inspect examples/filesystem-provider/server.py`.
diff --git a/docs/v3/servers/providers/local.mdx b/docs/v3/servers/providers/local.mdx
new file mode 100644
index 000000000..86726655a
--- /dev/null
+++ b/docs/v3/servers/providers/local.mdx
@@ -0,0 +1,161 @@
+---
+title: Local Provider
+sidebarTitle: Local
+description: The default provider for decorator-registered components
+icon: house
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+`LocalProvider` stores components that you define directly on your server. When you use `@mcp.tool`, `@mcp.resource`, or `@mcp.prompt`, you're adding components to your server's `LocalProvider`.
+
+## How It Works
+
+Every FastMCP server has a `LocalProvider` as its first provider. Components registered via decorators or direct methods are stored here:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("MyServer")
+
+# These are stored in the server's `LocalProvider`
+@mcp.tool
+def greet(name: str) -> str:
+ """Greet someone by name."""
+ return f"Hello, {name}!"
+
+@mcp.resource("data://config")
+def get_config() -> str:
+ """Return configuration data."""
+ return '{"version": "1.0"}'
+
+@mcp.prompt
+def analyze(topic: str) -> str:
+ """Create an analysis prompt."""
+ return f"Please analyze: {topic}"
+```
+
+The `LocalProvider` is always queried first when clients request components, ensuring that your directly-defined components take precedence over those from mounted or proxied servers.
+
+## Component Registration
+
+### Using Decorators
+
+The most common way to register components:
+
+```python
+@mcp.tool
+def my_tool(x: int) -> str:
+ return str(x)
+
+@mcp.resource("data://info")
+def my_resource() -> str:
+ return "info"
+
+@mcp.prompt
+def my_prompt(topic: str) -> str:
+ return f"Discuss: {topic}"
+```
+
+### Using Direct Methods
+
+You can also add pre-built component objects:
+
+```python
+from fastmcp.tools import Tool
+
+# Create a tool object
+my_tool = Tool.from_function(some_function, name="custom_tool")
+
+# Add it to the server
+mcp.add_tool(my_tool)
+mcp.add_resource(my_resource)
+mcp.add_prompt(my_prompt)
+```
+
+### Removing Components
+
+Remove components by name or URI:
+
+```python
+mcp.local_provider.remove_tool("my_tool")
+mcp.local_provider.remove_resource("data://info")
+mcp.local_provider.remove_prompt("my_prompt")
+```
+
+## Duplicate Handling
+
+When you try to add a component that already exists, the behavior depends on the `on_duplicate` setting:
+
+| Mode | Behavior |
+|------|----------|
+| `"error"` (default) | Raise `ValueError` |
+| `"warn"` | Log warning and replace |
+| `"replace"` | Silently replace |
+| `"ignore"` | Keep existing component |
+
+Configure this when creating the server:
+
+```python
+mcp = FastMCP("MyServer", on_duplicate="warn")
+```
+
+## Component Visibility
+
+
+
+Components can be dynamically enabled or disabled at runtime. Disabled components don't appear in listings and can't be called.
+
+```python
+@mcp.tool(tags={"admin"})
+def delete_all() -> str:
+ """Delete everything."""
+ return "Deleted"
+
+@mcp.tool
+def get_status() -> str:
+ """Get system status."""
+ return "OK"
+
+# Disable admin tools
+mcp.disable(tags={"admin"})
+
+# Or only enable specific tools
+mcp.enable(keys={"tool:get_status"}, only=True)
+```
+
+See [Visibility](/servers/visibility) for the full documentation on keys, tags, allowlist mode, and provider-level control.
+
+## Standalone LocalProvider
+
+You can create a LocalProvider independently and attach it to multiple servers:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.providers import LocalProvider
+
+# Create a reusable provider
+shared_tools = LocalProvider()
+
+@shared_tools.tool
+def greet(name: str) -> str:
+ return f"Hello, {name}!"
+
+@shared_tools.resource("data://version")
+def get_version() -> str:
+ return "1.0.0"
+
+# Attach to multiple servers
+server1 = FastMCP("Server1", providers=[shared_tools])
+server2 = FastMCP("Server2", providers=[shared_tools])
+```
+
+This is useful for:
+- Sharing components across servers
+- Testing components in isolation
+- Building reusable component libraries
+
+Standalone providers also support visibility control with `enable()` and `disable()`. See [Visibility](/servers/visibility) for details.
diff --git a/docs/v3/servers/providers/overview.mdx b/docs/v3/servers/providers/overview.mdx
new file mode 100644
index 000000000..d3e3e4e5f
--- /dev/null
+++ b/docs/v3/servers/providers/overview.mdx
@@ -0,0 +1,81 @@
+---
+title: Providers
+sidebarTitle: Overview
+description: How FastMCP sources tools, resources, and prompts
+icon: layer-group
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Every FastMCP server has one or more component providers. A provider is a source of tools, resources, and prompts - it's what makes components available to clients.
+
+## What Is a Provider?
+
+When a client connects to your server and asks "what tools do you have?", FastMCP asks each provider that question and combines the results. When a client calls a specific tool, FastMCP finds which provider has it and delegates the call.
+
+You're already using providers. When you write `@mcp.tool`, you're adding a tool to your server's `LocalProvider` - the default provider that stores components you define directly in code. You just don't have to think about it for simple servers.
+
+Providers become important when your components come from multiple sources: another FastMCP server to include, a remote MCP server to proxy, or a database where tools are defined dynamically. Each source gets its own provider, and FastMCP queries them all seamlessly.
+
+## Why Providers?
+
+The provider abstraction solves a common problem: as servers grow, you need to organize components across multiple sources without tangling everything together.
+
+**Composition**: Break a large server into focused modules. A "weather" server and a "calendar" server can each be developed independently, then mounted into a main server. Each mounted server becomes a `FastMCPProvider`.
+
+**Proxying**: Expose a remote MCP server through your local server. Maybe you're bridging transports (remote HTTP to local stdio) or aggregating multiple backends. Remote connections become `ProxyProvider` instances.
+
+**Dynamic sources**: Load tools from a database, generate them from an OpenAPI spec, or create them based on user permissions. Custom providers let components come from anywhere.
+
+## Built-in Providers
+
+FastMCP includes providers for common patterns:
+
+| Provider | What it does | How you use it |
+|----------|--------------|----------------|
+| `LocalProvider` | Stores components you define in code | `@mcp.tool`, `mcp.add_tool()` |
+| `FastMCPProvider` | Wraps another FastMCP server | `mcp.mount(server)` |
+| `ProxyProvider` | Connects to remote MCP servers | `create_proxy(client)` |
+
+Most users only interact with `LocalProvider` (through decorators) and occasionally mount or proxy other servers. The provider abstraction stays invisible until you need it.
+
+## Transforms
+
+[Transforms](/servers/transforms/transforms) modify components as they flow from providers to clients. Each transform sits in a chain, intercepting queries and modifying results before passing them along.
+
+| Transform | Purpose |
+|-----------|---------|
+| `Namespace` | Prefixes names to avoid conflicts |
+| `ToolTransform` | Modifies tool schemas (rename, description, arguments) |
+
+The most common use is namespacing mounted servers to prevent name collisions. When you call `mount(server, namespace="api")`, FastMCP creates a `Namespace` transform automatically.
+
+Transforms can be added to individual providers (affecting just that source) or to the server itself (affecting all components). See [Transforms](/servers/transforms/transforms) for the full picture.
+
+## Provider Order
+
+When a client requests a tool, FastMCP queries providers in registration order. The first provider that has the tool handles the request.
+
+`LocalProvider` is always first, so your decorator-defined tools take precedence. Additional providers are queried in the order you added them. This means if two providers have a tool with the same name, the first one wins.
+
+## When to Care About Providers
+
+**You can ignore providers entirely** if you're building a simple server with decorators. Just use `@mcp.tool`, `@mcp.resource`, and `@mcp.prompt` - FastMCP handles the rest.
+
+**Learn about providers when** you want to:
+- [Mount another server](/servers/composition) into yours
+- [Proxy a remote server](/servers/providers/proxy) through yours
+- [Control visibility state](/servers/visibility) of components
+- [Build dynamic sources](/servers/providers/custom) like database-backed tools
+
+## Next Steps
+
+- [Local](/servers/providers/local) - How decorators work
+- [Mounting](/servers/composition) - Compose servers together
+- [Proxying](/servers/providers/proxy) - Connect to remote servers
+- [Transforms](/servers/transforms/transforms) - Namespace, rename, and modify components
+- [Visibility](/servers/visibility) - Control which components clients can access
+- [Custom](/servers/providers/custom) - Build your own providers
diff --git a/docs/v3/servers/providers/proxy.mdx b/docs/v3/servers/providers/proxy.mdx
new file mode 100644
index 000000000..9d64b6148
--- /dev/null
+++ b/docs/v3/servers/providers/proxy.mdx
@@ -0,0 +1,353 @@
+---
+title: MCP Proxy Provider
+sidebarTitle: MCP Proxy
+description: Source components from other MCP servers
+icon: arrows-retweet
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+The Proxy Provider sources components from another MCP server through a client connection. This lets you expose any MCP server's tools, resources, and prompts through your own server, whether the source is local or accessed over the network.
+
+## Why Use Proxy Provider
+
+The Proxy Provider enables:
+
+- **Bridge transports**: Make an HTTP server available via stdio, or vice versa
+- **Aggregate servers**: Combine multiple source servers into one unified server
+- **Add security**: Act as a controlled gateway with authentication and authorization
+- **Simplify access**: Provide a stable endpoint even if backend servers change
+
+```mermaid
+sequenceDiagram
+ participant Client as Your Client
+ participant Proxy as FastMCP Proxy
+ participant Backend as Source Server
+
+ Client->>Proxy: MCP Request (stdio)
+ Proxy->>Backend: MCP Request (HTTP/stdio/SSE)
+ Backend-->>Proxy: MCP Response
+ Proxy-->>Client: MCP Response
+```
+
+## Quick Start
+
+
+
+Create a proxy using `create_proxy()`:
+
+```python
+from fastmcp.server import create_proxy
+
+# create_proxy() accepts URLs, file paths, and transports directly
+proxy = create_proxy("http://example.com/mcp", name="MyProxy")
+
+if __name__ == "__main__":
+ proxy.run()
+```
+
+This gives you:
+
+- Safe concurrent request handling
+- Automatic forwarding of MCP features (sampling, elicitation, etc.)
+- Session isolation to prevent context mixing
+
+
+To mount a proxy inside another FastMCP server, see [Mounting External Servers](/servers/composition#mounting-external-servers).
+
+
+## Connection Semantics
+
+FastMCP proxies are lazy bridges. Creating the proxy object and starting the local server do not contact the upstream server. The upstream connection begins when an MCP client sends an `initialize` request to the proxy.
+
+During initialization, the proxy initializes the upstream server before responding locally. If the upstream server is unavailable, the URL does not point to an MCP endpoint, or upstream authentication cannot complete, the proxy initialization fails. This keeps the local proxy's connection status aligned with the upstream server it represents.
+
+After initialization, the proxy forwards MCP requests such as `ping`, `tools/list`, `resources/list`, `prompts/list`, tool calls, resource reads, sampling, elicitation, logging, and progress through the upstream client.
+
+## Transport Bridging
+
+A common use case is bridging transports between servers:
+
+```python
+from fastmcp.server import create_proxy
+
+# Bridge HTTP server to local stdio
+http_proxy = create_proxy("http://example.com/mcp/sse", name="HTTP-to-stdio")
+
+# Run locally via stdio for Claude Desktop
+if __name__ == "__main__":
+ http_proxy.run() # Defaults to stdio
+```
+
+Or expose a local server via HTTP:
+
+```python
+from fastmcp.server import create_proxy
+
+# Bridge local server to HTTP
+local_proxy = create_proxy("local_server.py", name="stdio-to-HTTP")
+
+if __name__ == "__main__":
+ local_proxy.run(transport="http", host="0.0.0.0", port=8080)
+```
+
+## Session Isolation
+
+
+
+`create_proxy()` provides session isolation - each request gets its own isolated backend session:
+
+```python
+from fastmcp.server import create_proxy
+
+# Each request creates a fresh backend session (recommended)
+proxy = create_proxy("backend_server.py")
+
+# Multiple clients can use this proxy simultaneously:
+# - Client A calls a tool → gets isolated session
+# - Client B calls a tool → gets different session
+# - No context mixing
+```
+
+### Shared Sessions
+
+If you pass an already-connected client, the proxy reuses that session:
+
+```python
+from fastmcp import Client
+from fastmcp.server import create_proxy
+
+async with Client("backend_server.py") as connected_client:
+ # This proxy reuses the connected session
+ proxy = create_proxy(connected_client)
+
+ # ⚠️ Warning: All requests share the same session
+```
+
+
+Shared sessions may cause context mixing in concurrent scenarios. Use only in single-threaded situations or with explicit synchronization.
+
+
+## MCP Feature Forwarding
+
+
+
+Proxies automatically forward MCP protocol features:
+
+| Feature | Description |
+|---------|-------------|
+| Roots | Filesystem root access requests |
+| Sampling | LLM completion requests |
+| Elicitation | User input requests |
+| Logging | Log messages from backend |
+| Progress | Progress notifications |
+
+```python
+from fastmcp.server import create_proxy
+
+# All features forwarded automatically
+proxy = create_proxy("advanced_backend.py")
+
+# When the backend:
+# - Requests LLM sampling → forwarded to your client
+# - Logs messages → appear in your client
+# - Reports progress → shown in your client
+```
+
+### Disabling Features
+
+Selectively disable forwarding:
+
+```python
+from fastmcp.server.providers.proxy import ProxyClient
+
+backend = ProxyClient(
+ "backend_server.py",
+ sampling_handler=None, # Disable LLM sampling
+ log_handler=None # Disable log forwarding
+)
+```
+
+## Configuration-Based Proxies
+
+
+
+Create proxies from configuration dictionaries:
+
+```python
+from fastmcp.server import create_proxy
+
+config = {
+ "mcpServers": {
+ "default": {
+ "url": "https://example.com/mcp",
+ "transport": "http"
+ }
+ }
+}
+
+proxy = create_proxy(config, name="Config-Based Proxy")
+```
+
+### Multi-Server Proxies
+
+Combine multiple servers with automatic namespacing:
+
+```python
+from fastmcp.server import create_proxy
+
+config = {
+ "mcpServers": {
+ "weather": {
+ "url": "https://weather-api.example.com/mcp",
+ "transport": "http"
+ },
+ "calendar": {
+ "url": "https://calendar-api.example.com/mcp",
+ "transport": "http"
+ }
+ }
+}
+
+# Creates unified proxy with prefixed components:
+# - weather_get_forecast
+# - calendar_add_event
+composite = create_proxy(config, name="Composite")
+```
+
+## Component Prefixing
+
+Proxied components follow standard prefixing rules:
+
+| Component Type | Pattern |
+|----------------|---------|
+| Tools | `{prefix}_{tool_name}` |
+| Prompts | `{prefix}_{prompt_name}` |
+| Resources | `protocol://{prefix}/path` |
+| Templates | `protocol://{prefix}/...` |
+
+## Mirrored Components
+
+
+
+Components from a proxy server are "mirrored" - they reflect the remote server's state and cannot be modified directly.
+
+To modify a proxied component (like disabling it), create a local copy:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server import create_proxy
+
+proxy = create_proxy("backend_server.py")
+
+# Get mirrored tool
+mirrored_tool = await proxy.get_tool("useful_tool")
+
+# Create modifiable local copy
+local_tool = mirrored_tool.copy()
+
+# Add to your own server
+my_server = FastMCP("MyServer")
+my_server.add_tool(local_tool)
+
+# Now you can control enabled state
+my_server.disable(keys={local_tool.key})
+```
+
+## Performance Considerations
+
+Proxying introduces network latency:
+
+| Operation | Local | Proxied (HTTP) |
+|-----------|-------|----------------|
+| `list_tools()` | 1-2ms | 300-400ms |
+| `call_tool()` | 1-2ms | 200-500ms |
+
+When mounting proxy servers, this latency affects all operations on the parent server.
+
+### Component List Caching
+
+
+
+`ProxyProvider` caches the backend's component lists (tools, resources, templates, prompts) so that individual lookups — like resolving a tool by name during `call_tool` — don't require a separate backend connection. The cache stores raw component metadata and is shared across all proxy sessions; per-session visibility, auth, and transforms are still applied after cache lookup by the server layer. The cache refreshes whenever an explicit `list_*` call is made, and entries expire after a configurable TTL (default 300 seconds).
+
+For backends whose component lists change dynamically, disable caching by setting `cache_ttl=0`.
+
+```python
+from fastmcp.server.providers.proxy import ProxyProvider, ProxyClient
+
+# Default 300s TTL
+provider = ProxyProvider(lambda: ProxyClient("http://backend/mcp"))
+
+# Custom TTL
+provider = ProxyProvider(lambda: ProxyClient("http://backend/mcp"), cache_ttl=60)
+
+# Disable caching
+provider = ProxyProvider(lambda: ProxyClient("http://backend/mcp"), cache_ttl=0)
+```
+
+### Session Reuse for Stateless Backends
+
+By default, each tool call opens a fresh MCP session to the backend. This is the safe default because it prevents state from leaking between requests. However, for stateless HTTP backends where there's no session state to protect, this overhead is unnecessary.
+
+You can reuse a single backend session by providing a client factory that returns the same client instance:
+
+```python
+from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient
+
+base_client = ProxyClient("http://backend:8000/mcp")
+shared_client = base_client.new()
+
+proxy = FastMCPProxy(
+ client_factory=lambda: shared_client,
+ name="ReusedSessionProxy",
+)
+```
+
+This eliminates the MCP initialization handshake on every call, which can dramatically reduce latency under load. The `Client` uses reference counting for its session lifecycle, so concurrent callers sharing the same instance is safe.
+
+
+Only reuse sessions when you know the backend is stateless (e.g. stateless HTTP). For stateful backends (stdio processes, servers that track session state), use the default fresh-session behavior to avoid context mixing.
+
+
+## Advanced Usage
+
+### FastMCPProxy Class
+
+For explicit session control, use `FastMCPProxy` directly:
+
+```python
+from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient
+
+# Custom session factory
+def create_client():
+ return ProxyClient("backend_server.py")
+
+proxy = FastMCPProxy(client_factory=create_client)
+```
+
+This gives you full control over session creation and reuse strategies.
+
+### Adding Proxied Components to Existing Server
+
+Mount a proxy to add components from another server:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server import create_proxy
+
+server = FastMCP("My Server")
+
+# Add local tools
+@server.tool
+def local_tool() -> str:
+ return "Local result"
+
+# Mount proxied tools from another server
+external = create_proxy("http://external-server/mcp")
+server.mount(external)
+
+# Now server has both local and proxied tools
+```
diff --git a/docs/v3/servers/providers/skills.mdx b/docs/v3/servers/providers/skills.mdx
new file mode 100644
index 000000000..3c810b3f2
--- /dev/null
+++ b/docs/v3/servers/providers/skills.mdx
@@ -0,0 +1,301 @@
+---
+title: Skills Provider
+sidebarTitle: Skills
+description: Expose agent skills as MCP resources
+icon: wand-magic-sparkles
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Agent skills are directories containing instructions and supporting files that teach an AI assistant how to perform specific tasks. Tools like Claude Code, Cursor, and VS Code Copilot each have their own skills directories where users can add custom capabilities. The Skills Provider exposes these skill directories as MCP resources, making skills discoverable and shareable across different AI tools and clients.
+
+## Why Skills as Resources
+
+Skills live in platform-specific directories (`~/.claude/skills/`, `~/.cursor/skills/`, etc.) and typically contain a main instruction file plus supporting reference materials. When you want to share skills between tools or access them from a custom client, you need a way to discover and retrieve these files programmatically.
+
+The Skills Provider solves this by exposing each skill as a set of MCP resources. A client can list available skills, read the main instruction file, check the manifest to see what supporting files exist, and fetch any file it needs. This transforms local skill directories into a standardized API that works with any MCP client.
+
+## Quick Start
+
+Create a provider pointing to your skills directory, then add it to your server.
+
+```python
+from pathlib import Path
+
+from fastmcp import FastMCP
+from fastmcp.server.providers.skills import SkillsDirectoryProvider
+
+mcp = FastMCP("Skills Server")
+mcp.add_provider(SkillsDirectoryProvider(roots=Path.home() / ".claude" / "skills"))
+```
+
+Each subdirectory containing a `SKILL.md` file becomes a discoverable skill. Clients can then list resources to see available skills and read them as needed.
+
+```python
+from fastmcp import Client
+
+async with Client(mcp) as client:
+ # List all skill resources
+ resources = await client.list_resources()
+ for r in resources:
+ print(r.uri) # skill://my-skill/SKILL.md, skill://my-skill/_manifest, ...
+
+ # Read a skill's main instruction file
+ result = await client.read_resource("skill://my-skill/SKILL.md")
+ print(result[0].text)
+```
+
+## Skill Structure
+
+A skill is a directory containing a main instruction file (default: `SKILL.md`) and optionally supporting files. The directory name becomes the skill's identifier.
+
+```
+~/.claude/skills/
+├── pdf-processing/
+│ ├── SKILL.md # Main instructions
+│ ├── reference.md # Supporting documentation
+│ └── examples/
+│ └── sample.pdf
+└── code-review/
+ └── SKILL.md
+```
+
+The main file can include YAML frontmatter to provide metadata. If no frontmatter exists, the provider extracts a description from the first meaningful line of content.
+
+```markdown
+---
+description: Process and extract information from PDF documents
+---
+
+# PDF Processing
+
+Instructions for handling PDFs...
+```
+
+## Resource URIs
+
+Each skill exposes three types of resources, all using the `skill://` URI scheme.
+
+The main instruction file contains the primary skill content. This is the resource clients read to understand what a skill does and how to use it.
+
+```
+skill://pdf-processing/SKILL.md
+```
+
+The manifest is a synthetic JSON resource listing all files in the skill directory with their sizes and SHA256 hashes. Clients use this to discover supporting files and verify content integrity.
+
+```
+skill://pdf-processing/_manifest
+```
+
+Reading the manifest returns structured file information.
+
+```json
+{
+ "skill": "pdf-processing",
+ "files": [
+ {"path": "SKILL.md", "size": 1234, "hash": "sha256:abc123..."},
+ {"path": "reference.md", "size": 567, "hash": "sha256:def456..."},
+ {"path": "examples/sample.pdf", "size": 89012, "hash": "sha256:ghi789..."}
+ ]
+}
+```
+
+Supporting files are any additional files in the skill directory. These might be reference documentation, code examples, or binary assets.
+
+```
+skill://pdf-processing/reference.md
+skill://pdf-processing/examples/sample.pdf
+```
+
+## Provider Architecture
+
+The Skills Provider uses a two-layer architecture to handle both single skills and skill directories.
+
+### SkillProvider
+
+`SkillProvider` handles a single skill directory. It loads the main file, parses any frontmatter, scans for supporting files, and creates the appropriate resources.
+
+```python
+from pathlib import Path
+
+from fastmcp import FastMCP
+from fastmcp.server.providers.skills import SkillProvider
+
+mcp = FastMCP("Single Skill")
+mcp.add_provider(SkillProvider(Path.home() / ".claude" / "skills" / "pdf-processing"))
+```
+
+Use `SkillProvider` when you want to expose exactly one skill, or when you need fine-grained control over individual skill configuration.
+
+### SkillsDirectoryProvider
+
+`SkillsDirectoryProvider` scans one or more root directories and creates a `SkillProvider` for each valid skill folder it finds. A folder is considered a valid skill if it contains the main file (default: `SKILL.md`).
+
+```python
+from pathlib import Path
+
+from fastmcp import FastMCP
+from fastmcp.server.providers.skills import SkillsDirectoryProvider
+
+mcp = FastMCP("Skills")
+mcp.add_provider(SkillsDirectoryProvider(roots=Path.home() / ".claude" / "skills"))
+```
+
+When scanning multiple root directories, provide them as a list. The first directory takes precedence if the same skill name appears in multiple roots.
+
+```python
+from pathlib import Path
+
+from fastmcp import FastMCP
+from fastmcp.server.providers.skills import SkillsDirectoryProvider
+
+mcp = FastMCP("Skills")
+mcp.add_provider(SkillsDirectoryProvider(roots=[
+ Path.cwd() / ".claude" / "skills", # Project-level skills first
+ Path.home() / ".claude" / "skills", # User-level fallback
+]))
+```
+
+## Vendor Providers
+
+FastMCP includes pre-configured providers for popular AI coding tools. Each vendor provider extends `SkillsDirectoryProvider` with the appropriate default directory for that platform.
+
+| Provider | Default Directory |
+|----------|-------------------|
+| `ClaudeSkillsProvider` | `~/.claude/skills/` |
+| `CursorSkillsProvider` | `~/.cursor/skills/` |
+| `VSCodeSkillsProvider` | `~/.copilot/skills/` |
+| `CodexSkillsProvider` | `/etc/codex/skills/` and `~/.codex/skills/` |
+| `GeminiSkillsProvider` | `~/.gemini/skills/` |
+| `GooseSkillsProvider` | `~/.config/agents/skills/` |
+| `CopilotSkillsProvider` | `~/.copilot/skills/` |
+| `OpenCodeSkillsProvider` | `~/.config/opencode/skills/` |
+
+Vendor providers accept the same configuration options as `SkillsDirectoryProvider` (except for `roots`, which is locked to the platform default).
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.providers.skills import ClaudeSkillsProvider
+
+mcp = FastMCP("Claude Skills")
+mcp.add_provider(ClaudeSkillsProvider()) # Uses ~/.claude/skills/
+```
+
+`CodexSkillsProvider` scans both system-level (`/etc/codex/skills/`) and user-level (`~/.codex/skills/`) directories, with system skills taking precedence.
+
+## Supporting Files Disclosure
+
+The `supporting_files` parameter controls how supporting files (everything except the main file and manifest) appear to clients.
+
+### Template Mode (Default)
+
+With `supporting_files="template"`, supporting files are accessed through a `ResourceTemplate` rather than being listed as individual resources. Clients see only the main file and manifest in `list_resources()`, then discover supporting files by reading the manifest.
+
+```python
+from pathlib import Path
+
+from fastmcp.server.providers.skills import SkillsDirectoryProvider
+
+# Default behavior - supporting files hidden from list_resources()
+provider = SkillsDirectoryProvider(
+ roots=Path.home() / ".claude" / "skills",
+ supporting_files="template", # This is the default
+)
+```
+
+This keeps the resource list compact when skills contain many files. Clients that need supporting files read the manifest first, then request specific files by URI.
+
+### Resources Mode
+
+With `supporting_files="resources"`, every file in every skill appears as an individual resource in `list_resources()`. Clients get full enumeration upfront without needing to read manifests.
+
+```python
+from pathlib import Path
+
+from fastmcp.server.providers.skills import SkillsDirectoryProvider
+
+# All files visible as individual resources
+provider = SkillsDirectoryProvider(
+ roots=Path.home() / ".claude" / "skills",
+ supporting_files="resources",
+)
+```
+
+Use this mode when clients need to discover all available files without additional round trips, or when integrating with tools that expect flat resource lists.
+
+## Reload Mode
+
+Enable reload mode to re-scan the skills directory on every request. Changes to skills take effect immediately without restarting the server.
+
+```python
+from pathlib import Path
+
+from fastmcp.server.providers.skills import SkillsDirectoryProvider
+
+provider = SkillsDirectoryProvider(
+ roots=Path.home() / ".claude" / "skills",
+ reload=True,
+)
+```
+
+With `reload=True`, the provider re-discovers skills on each `list_resources()` or `read_resource()` call. New skills appear, removed skills disappear, and modified content reflects current file state.
+
+
+Reload mode adds overhead to every request. Use it during development when you're actively editing skills, but disable it in production.
+
+
+## Client Utilities
+
+FastMCP provides utilities for downloading skills from any MCP server that exposes them. These are standalone functions in `fastmcp.utilities.skills`.
+
+### Discovering Skills
+
+Use `list_skills()` to see what skills are available on a server.
+
+```python
+from fastmcp import Client
+from fastmcp.utilities.skills import list_skills
+
+async with Client("http://skills-server/mcp") as client:
+ skills = await list_skills(client)
+ for skill in skills:
+ print(f"{skill.name}: {skill.description}")
+```
+
+### Downloading Skills
+
+Use `download_skill()` to download a single skill, or `sync_skills()` to download all available skills.
+
+```python
+from pathlib import Path
+
+from fastmcp import Client
+from fastmcp.utilities.skills import download_skill, sync_skills
+
+async with Client("http://skills-server/mcp") as client:
+ # Download one skill
+ path = await download_skill(client, "pdf-processing", Path.home() / ".claude" / "skills")
+
+ # Or download all skills
+ paths = await sync_skills(client, Path.home() / ".claude" / "skills")
+```
+
+Both functions accept an `overwrite` parameter. When `False` (default), existing skills are skipped. When `True`, existing files are replaced.
+
+### Inspecting Manifests
+
+Use `get_skill_manifest()` to see what files a skill contains before downloading.
+
+```python
+from fastmcp import Client
+from fastmcp.utilities.skills import get_skill_manifest
+
+async with Client("http://skills-server/mcp") as client:
+ manifest = await get_skill_manifest(client, "pdf-processing")
+ for file in manifest.files:
+ print(f"{file.path} ({file.size} bytes, {file.hash})")
+```
diff --git a/docs/v3/servers/resources.mdx b/docs/v3/servers/resources.mdx
new file mode 100644
index 000000000..c756c5ff9
--- /dev/null
+++ b/docs/v3/servers/resources.mdx
@@ -0,0 +1,747 @@
+---
+title: Resources & Templates
+sidebarTitle: Resources
+description: Expose data sources and dynamic content generators to your MCP client.
+icon: folder-open
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+Resources represent data or files that an MCP client can read, and resource templates extend this concept by allowing clients to request dynamically generated resources based on parameters passed in the URI.
+
+FastMCP simplifies defining both static and dynamic resources, primarily using the `@mcp.resource` decorator.
+
+## What Are Resources?
+
+Resources provide read-only access to data for the LLM or client application. When a client requests a resource URI:
+
+1. FastMCP finds the corresponding resource definition.
+2. If it's dynamic (defined by a function), the function is executed.
+3. The content (text, JSON, binary data) is returned to the client.
+
+This allows LLMs to access files, database content, configuration, or dynamically generated information relevant to the conversation.
+
+## Resources
+
+### The `@resource` Decorator
+
+The most common way to define a resource is by decorating a Python function. The decorator requires the resource's unique URI.
+
+```python
+import json
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="DataServer")
+
+# Basic dynamic resource returning a string
+@mcp.resource("resource://greeting")
+def get_greeting() -> str:
+ """Provides a simple greeting message."""
+ return "Hello from FastMCP Resources!"
+
+# Resource returning JSON data
+@mcp.resource("data://config")
+def get_config() -> str:
+ """Provides application configuration as JSON."""
+ return json.dumps({
+ "theme": "dark",
+ "version": "1.2.0",
+ "features": ["tools", "resources"],
+ })
+```
+
+**Key Concepts:**
+
+* **URI:** The first argument to `@resource` is the unique URI (e.g., `"resource://greeting"`) clients use to request this data.
+* **Lazy Loading:** The decorated function (`get_greeting`, `get_config`) is only executed when a client specifically requests that resource URI via `resources/read`.
+* **Inferred Metadata:** By default:
+ * Resource Name: Taken from the function name (`get_greeting`).
+ * Resource Description: Taken from the function's docstring.
+
+#### Decorator Arguments
+
+You can customize the resource's properties using arguments in the `@mcp.resource` decorator:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="DataServer")
+
+# Example specifying metadata
+@mcp.resource(
+ uri="data://app-status", # Explicit URI (required)
+ name="ApplicationStatus", # Custom name
+ description="Provides the current status of the application.", # Custom description
+ mime_type="application/json", # Explicit MIME type
+ tags={"monitoring", "status"}, # Categorization tags
+ meta={"version": "2.1", "team": "infrastructure"} # Custom metadata
+)
+def get_application_status() -> str:
+ """Internal function description (ignored if description is provided above)."""
+ return json.dumps({"status": "ok", "uptime": 12345, "version": mcp.settings.version})
+```
+
+
+
+ The unique identifier for the resource
+
+
+
+ A human-readable name. If not provided, defaults to function name
+
+
+
+ Explanation of the resource. If not provided, defaults to docstring
+
+
+
+ Specifies the content type. FastMCP often infers a default like `text/plain` or `application/json`, but explicit is better for non-text types
+
+
+
+ A set of strings used to categorize the resource. These can be used by the server and, in some cases, by clients to filter or group available resources.
+
+
+
+ Deprecated in v3.0.0. Use `mcp.enable()` / `mcp.disable()` at the server level instead.
+ A boolean to enable or disable the resource. See [Component Visibility](#component-visibility) for the recommended approach.
+
+
+
+
+
+ Optional list of icon representations for this resource or template. See [Icons](/servers/icons) for detailed examples
+
+
+
+ An optional `Annotations` object or dictionary to add additional metadata about the resource.
+
+
+ If true, the resource is read-only and does not modify its environment.
+
+
+ If true, reading the resource repeatedly will have no additional effect on its environment.
+
+
+
+
+
+
+
+ Optional meta information about the resource. This data is passed through to the MCP client as the `meta` field of the client-side resource object and can be used for custom metadata, versioning, or other application-specific purposes.
+
+
+
+
+
+ Optional version identifier for this resource. See [Versioning](/servers/versioning) for details.
+
+
+
+#### Using with Methods
+
+For decorating instance or class methods, use the standalone `@resource` decorator and register the bound method. See [Tools: Using with Methods](/servers/tools#using-with-methods) for the pattern.
+
+### Return Values
+
+Resource functions must return one of three types:
+
+- **`str`**: Sent as `TextResourceContents` (with `mime_type="text/plain"` by default).
+- **`bytes`**: Base64 encoded and sent as `BlobResourceContents`. You should specify an appropriate `mime_type` (e.g., `"image/png"`, `"application/octet-stream"`).
+- **`ResourceResult`**: Full control over contents, MIME types, and metadata. See [ResourceResult](#resourceresult) below.
+
+
+To return structured data like dicts or lists, serialize them to JSON strings using `json.dumps()`. This explicit approach ensures your type checker catches errors during development rather than at runtime when a client reads the resource.
+
+
+#### ResourceResult
+
+
+
+`ResourceResult` gives you explicit control over resource responses: multiple content items, per-item MIME types, and metadata at both the item and result level.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.resources import ResourceResult, ResourceContent
+
+mcp = FastMCP()
+
+@mcp.resource("data://users")
+def get_users() -> ResourceResult:
+ return ResourceResult(
+ contents=[
+ ResourceContent(content='[{"id": 1}]', mime_type="application/json"),
+ ResourceContent(content="# Users\n...", mime_type="text/markdown"),
+ ],
+ meta={"total": 1}
+ )
+```
+
+`ResourceContent` accepts three fields:
+
+**`content`** - The actual resource content. Can be `str` (text content) or `bytes` (binary content). This is the data that will be returned to the client.
+
+**`mime_type`** - Optional MIME type for the content. Defaults to `"text/plain"` for string content and `"application/octet-stream"` for binary content.
+
+**`meta`** - Optional metadata dictionary that will be included in the MCP response's `meta` field. Use this for runtime metadata like Content Security Policy headers, caching hints, or other client-specific data.
+
+For simple cases, you can pass `str` or `bytes` directly to `ResourceResult`:
+
+```python
+return ResourceResult("plain text") # auto-converts to ResourceContent
+return ResourceResult(b"\x00\x01\x02") # binary content
+```
+
+
+
+ Content to return. Strings and bytes are wrapped in a single `ResourceContent`. Use a list of `ResourceContent` for multiple items or custom MIME types.
+
+
+ Result-level metadata, included in the MCP response's `_meta` field.
+
+
+
+
+
+ The content data. Strings and bytes pass through directly. Other types (dict, list, BaseModel) are automatically JSON-serialized.
+
+
+ MIME type. Defaults to `text/plain` for strings, `application/octet-stream` for bytes, `application/json` for serialized objects.
+
+
+ Item-level metadata for this specific content.
+
+
+
+### Component Visibility
+
+
+
+You can control which resources are enabled for clients using server-level enabled control. Disabled resources don't appear in `list_resources` and can't be read.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("MyServer")
+
+@mcp.resource("data://public", tags={"public"})
+def get_public(): return "public"
+
+@mcp.resource("data://secret", tags={"internal"})
+def get_secret(): return "secret"
+
+# Disable specific resources by key
+mcp.disable(keys={"resource:data://secret"})
+
+# Disable resources by tag
+mcp.disable(tags={"internal"})
+
+# Or use allowlist mode - only enable resources with specific tags
+mcp.enable(tags={"public"}, only=True)
+```
+
+See [Visibility](/servers/visibility) for the complete visibility control API including key formats, tag-based filtering, and provider-level control.
+
+
+### Accessing MCP Context
+
+
+
+Resources and resource templates can access additional MCP information and features through the `Context` object. To access it, add a parameter to your resource function with a type annotation of `Context`:
+
+```python {6, 14}
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP(name="DataServer")
+
+@mcp.resource("resource://system-status")
+async def get_system_status(ctx: Context) -> str:
+ """Provides system status information."""
+ return json.dumps({
+ "status": "operational",
+ "request_id": ctx.request_id
+ })
+
+@mcp.resource("resource://{name}/details")
+async def get_details(name: str, ctx: Context) -> str:
+ """Get details for a specific name."""
+ return json.dumps({
+ "name": name,
+ "accessed_at": ctx.request_id
+ })
+```
+
+For full documentation on the Context object and all its capabilities, see the [Context documentation](/servers/context).
+
+
+### Async Resources
+
+FastMCP supports both `async def` and regular `def` resource functions. Synchronous functions automatically run in a threadpool to avoid blocking the event loop.
+
+For I/O-bound operations, async functions are more efficient:
+
+```python
+import aiofiles
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="DataServer")
+
+@mcp.resource("file:///app/data/important_log.txt", mime_type="text/plain")
+async def read_important_log() -> str:
+ """Reads content from a specific log file asynchronously."""
+ try:
+ async with aiofiles.open("/app/data/important_log.txt", mode="r") as f:
+ content = await f.read()
+ return content
+ except FileNotFoundError:
+ return "Log file not found."
+```
+
+
+### Resource Classes
+
+While `@mcp.resource` is ideal for dynamic content, you can directly register pre-defined resources (like static files or simple text) using `mcp.add_resource()` and concrete `Resource` subclasses.
+
+```python
+from pathlib import Path
+from fastmcp import FastMCP
+from fastmcp.resources import FileResource, TextResource, DirectoryResource
+
+mcp = FastMCP(name="DataServer")
+
+# 1. Exposing a static file directly
+readme_path = Path("./README.md").resolve()
+if readme_path.exists():
+ # Use a file:// URI scheme
+ readme_resource = FileResource(
+ uri=f"file://{readme_path.as_posix()}",
+ path=readme_path, # Path to the actual file
+ name="README File",
+ description="The project's README.",
+ mime_type="text/markdown",
+ tags={"documentation"}
+ )
+ mcp.add_resource(readme_resource)
+
+# 2. Exposing simple, predefined text
+notice_resource = TextResource(
+ uri="resource://notice",
+ name="Important Notice",
+ text="System maintenance scheduled for Sunday.",
+ tags={"notification"}
+)
+mcp.add_resource(notice_resource)
+
+# 3. Exposing a directory listing
+data_dir_path = Path("./app_data").resolve()
+if data_dir_path.is_dir():
+ data_listing_resource = DirectoryResource(
+ uri="resource://data-files",
+ path=data_dir_path, # Path to the directory
+ name="Data Directory Listing",
+ description="Lists files available in the data directory.",
+ recursive=False # Set to True to list subdirectories
+ )
+ mcp.add_resource(data_listing_resource) # Returns JSON list of files
+```
+
+**Common Resource Classes:**
+
+- `TextResource`: For simple string content.
+- `BinaryResource`: For raw `bytes` content.
+- `FileResource`: Reads content from a local file path. Handles text/binary modes, encoding, and lazy reading.
+- `HttpResource`: Fetches content from an HTTP(S) URL (requires `httpx`).
+- `DirectoryResource`: Lists files in a local directory (returns JSON).
+- (`FunctionResource`: Internal class used by `@mcp.resource`).
+
+Use these when the content is static or sourced directly from a file/URL, bypassing the need for a dedicated Python function.
+
+### Notifications
+
+
+
+FastMCP automatically sends `notifications/resources/list_changed` notifications to connected clients when resources or templates are added, enabled, or disabled. This allows clients to stay up-to-date with the current resource set without manually polling for changes.
+
+```python
+@mcp.resource("data://example")
+def example_resource() -> str:
+ return "Hello!"
+
+# These operations trigger notifications:
+mcp.add_resource(example_resource) # Sends resources/list_changed notification
+mcp.disable(keys={"resource:data://example"}) # Sends resources/list_changed notification
+mcp.enable(keys={"resource:data://example"}) # Sends resources/list_changed notification
+```
+
+Notifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications.
+
+Clients can handle these notifications using a [message handler](/clients/notifications) to automatically refresh their resource lists or update their interfaces.
+
+### Annotations
+
+
+
+FastMCP allows you to add specialized metadata to your resources through annotations. These annotations communicate how resources behave to client applications without consuming token context in LLM prompts.
+
+Annotations serve several purposes in client applications:
+- Indicating whether resources are read-only or may have side effects
+- Describing the safety profile of resources (idempotent vs. non-idempotent)
+- Helping clients optimize caching and access patterns
+
+You can add annotations to a resource using the `annotations` parameter in the `@mcp.resource` decorator:
+
+```python
+@mcp.resource(
+ "data://config",
+ annotations={
+ "readOnlyHint": True,
+ "idempotentHint": True
+ }
+)
+def get_config() -> str:
+ """Get application configuration."""
+ return json.dumps({"version": "1.0", "debug": False})
+```
+
+FastMCP supports these standard annotations:
+
+| Annotation | Type | Default | Purpose |
+| :--------- | :--- | :------ | :------ |
+| `readOnlyHint` | boolean | true | Indicates if the resource only provides data without side effects |
+| `idempotentHint` | boolean | true | Indicates if repeated reads have the same effect as a single read |
+
+Remember that annotations help make better user experiences but should be treated as advisory hints. They help client applications present appropriate UI elements and optimize access patterns, but won't enforce behavior on their own. Always focus on making your annotations accurately represent what your resource actually does.
+
+## Resource Templates
+
+Resource Templates allow clients to request resources whose content depends on parameters embedded in the URI. Define a template using the **same `@mcp.resource` decorator**, but include `{parameter_name}` placeholders in the URI string and add corresponding arguments to your function signature.
+
+Resource templates share most configuration options with regular resources (name, description, mime_type, tags, annotations), but add the ability to define URI parameters that map to function parameters.
+
+Resource templates generate a new resource for each unique set of parameters, which means that resources can be dynamically created on-demand. For example, if the resource template `"user://profile/{name}"` is registered, MCP clients could request `"user://profile/ford"` or `"user://profile/marvin"` to retrieve either of those two user profiles as resources, without having to register each resource individually.
+
+
+Functions with `*args` are not supported as resource templates. However, unlike tools and prompts, resource templates do support `**kwargs` because the URI template defines specific parameter names that will be collected and passed as keyword arguments.
+
+
+Here is a complete example that shows how to define two resource templates:
+
+```python
+import json
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="DataServer")
+
+# Template URI includes {city} placeholder
+@mcp.resource("weather://{city}/current")
+def get_weather(city: str) -> str:
+ """Provides weather information for a specific city."""
+ return json.dumps({
+ "city": city.capitalize(),
+ "temperature": 22,
+ "condition": "Sunny",
+ "unit": "celsius"
+ })
+
+# Template with multiple parameters and annotations
+@mcp.resource(
+ "repos://{owner}/{repo}/info",
+ annotations={
+ "readOnlyHint": True,
+ "idempotentHint": True
+ }
+)
+def get_repo_info(owner: str, repo: str) -> str:
+ """Retrieves information about a GitHub repository."""
+ return json.dumps({
+ "owner": owner,
+ "name": repo,
+ "full_name": f"{owner}/{repo}",
+ "stars": 120,
+ "forks": 48
+ })
+```
+
+With these two templates defined, clients can request a variety of resources:
+- `weather://london/current` → Returns weather for London
+- `weather://paris/current` → Returns weather for Paris
+- `repos://PrefectHQ/fastmcp/info` → Returns info about the PrefectHQ/fastmcp repository
+- `repos://prefecthq/prefect/info` → Returns info about the prefecthq/prefect repository
+
+### RFC 6570 URI Templates
+
+
+FastMCP implements [RFC 6570 URI Templates](https://datatracker.ietf.org/doc/html/rfc6570) for resource templates, providing a standardized way to define parameterized URIs. This includes support for simple expansion, wildcard path parameters, and form-style query parameters.
+
+#### Wildcard Parameters
+
+
+
+Resource templates support wildcard parameters that can match multiple path segments. Standard parameters (`{param}`) match a single URI segment before decoding and do not cross literal "/" boundaries in the request URI. Wildcard parameters (`{param*}`) can capture multiple segments including slashes. Wildcards capture all subsequent path segments *up until* the defined part of the URI template (whether literal or another parameter). This allows you to have multiple wildcard parameters in a single URI template.
+
+```python {15, 23}
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="DataServer")
+
+
+# Standard parameter only matches one segment
+@mcp.resource("files://{filename}")
+def get_file(filename: str) -> str:
+ """Retrieves a file by name."""
+ # Will only match files://
+ return f"File content for: {filename}"
+
+
+# Wildcard parameter can match multiple segments
+@mcp.resource("path://{filepath*}")
+def get_path_content(filepath: str) -> str:
+ """Retrieves content at a specific path."""
+ # Can match path://docs/server/resources.mdx
+ return f"Content at path: {filepath}"
+
+
+# Mixing standard and wildcard parameters
+@mcp.resource("repo://{owner}/{path*}/template.py")
+def get_template_file(owner: str, path: str) -> dict:
+ """Retrieves a file from a specific repository and path, but
+ only if the resource ends with `template.py`"""
+ # Can match repo://PrefectHQ/fastmcp/src/resources/template.py
+ return {
+ "owner": owner,
+ "path": path + "/template.py",
+ "content": f"File at {path}/template.py in {owner}'s repository"
+ }
+```
+
+Wildcard parameters are useful when:
+
+- Working with file paths or hierarchical data
+- Creating APIs that need to capture variable-length path segments
+- Building URL-like patterns similar to REST APIs
+
+Note that like regular parameters, each wildcard parameter must still be a named parameter in your function signature, and all required function parameters must appear in the URI template.
+
+#### Filesystem Path Safety
+
+Template parameters are decoded before your function receives them. A standard `{filename}` parameter matches one URI segment before decoding, so a request like `files://a%2Fb` passes `filename="a/b"` to the handler. Treat template values as untrusted decoded URI data whenever they determine filesystem paths.
+
+Validate the final resolved path against an allowed root before reading:
+
+```python
+from pathlib import Path
+
+from fastmcp import FastMCP
+from fastmcp.exceptions import ResourceError
+
+mcp = FastMCP(name="DocsServer")
+DOCS_ROOT = Path("docs").resolve()
+
+
+@mcp.resource("docs://{filename}")
+def read_doc(filename: str) -> str:
+ requested_path = (DOCS_ROOT / filename).resolve()
+
+ if not requested_path.is_relative_to(DOCS_ROOT) or not requested_path.is_file():
+ raise ResourceError("Document not found")
+
+ return requested_path.read_text(encoding="utf-8")
+```
+
+Use wildcard parameters (`{path*}`) for resources whose URI shape intentionally includes slashes, and apply the same containment check before accessing the filesystem.
+
+#### Query Parameters
+
+
+
+FastMCP supports RFC 6570 form-style query parameters using the `{?param1,param2}` syntax. Query parameters provide a clean way to pass optional configuration to resources without cluttering the path.
+
+Query parameters must be optional function parameters (have default values), while path parameters map to required function parameters. This enforces a clear separation: required data goes in the path, optional configuration in query params.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="DataServer")
+
+# Basic query parameters
+@mcp.resource("data://{id}{?format}")
+def get_data(id: str, format: str = "json") -> str:
+ """Retrieve data in specified format."""
+ if format == "xml":
+ return f" "
+ return f'{{"id": "{id}"}}'
+
+# Multiple query parameters with type coercion
+@mcp.resource("api://{endpoint}{?version,limit,offset}")
+def call_api(endpoint: str, version: int = 1, limit: int = 10, offset: int = 0) -> dict:
+ """Call API endpoint with pagination."""
+ return {
+ "endpoint": endpoint,
+ "version": version,
+ "limit": limit,
+ "offset": offset,
+ "results": fetch_results(endpoint, version, limit, offset)
+ }
+
+# Query parameters with wildcards
+@mcp.resource("files://{path*}{?encoding,lines}")
+def read_file(path: str, encoding: str = "utf-8", lines: int = 100) -> str:
+ """Read file with optional encoding and line limit."""
+ return read_file_content(path, encoding, lines)
+```
+
+**Example requests:**
+- `data://123` → Uses default format `"json"`
+- `data://123?format=xml` → Uses format `"xml"`
+- `api://users?version=2&limit=50` → `version=2, limit=50, offset=0`
+- `files://src/main.py?encoding=ascii&lines=50` → Custom encoding and line limit
+
+FastMCP automatically coerces query parameter string values to the correct types based on your function's type hints (`int`, `float`, `bool`, `str`).
+
+**Query parameters vs. hidden defaults:**
+
+Query parameters expose optional configuration to clients. To hide optional parameters from clients entirely (always use defaults), simply omit them from the URI template:
+
+```python
+# Clients CAN override max_results via query string
+@mcp.resource("search://{query}{?max_results}")
+def search_configurable(query: str, max_results: int = 10) -> dict:
+ return {"query": query, "limit": max_results}
+
+# Clients CANNOT override max_results (not in URI template)
+@mcp.resource("search://{query}")
+def search_fixed(query: str, max_results: int = 10) -> dict:
+ return {"query": query, "limit": max_results}
+```
+
+### Template Parameter Rules
+
+
+
+FastMCP enforces these validation rules when creating resource templates:
+
+1. **Required function parameters** (no default values) must appear in the URI path template
+2. **Query parameters** (specified with `{?param}` syntax) must be optional function parameters with default values
+3. **All URI template parameters** (path and query) must exist as function parameters
+
+Optional function parameters (those with default values) can be:
+- Included as query parameters (`{?param}`) - clients can override via query string
+- Omitted from URI template - always uses default value, not exposed to clients
+- Used in alternative path templates - enables multiple ways to access the same resource
+
+**Multiple templates for one function:**
+
+Create multiple resource templates that expose the same function through different URI patterns by manually applying decorators:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="DataServer")
+
+# Define a user lookup function that can be accessed by different identifiers
+def lookup_user(name: str | None = None, email: str | None = None) -> dict:
+ """Look up a user by either name or email."""
+ if email:
+ return find_user_by_email(email) # pseudocode
+ elif name:
+ return find_user_by_name(name) # pseudocode
+ else:
+ return {"error": "No lookup parameters provided"}
+
+# Manually apply multiple decorators to the same function
+mcp.resource("users://email/{email}")(lookup_user)
+mcp.resource("users://name/{name}")(lookup_user)
+```
+
+Now an LLM or client can retrieve user information in two different ways:
+- `users://email/alice@example.com` → Looks up user by email (with name=None)
+- `users://name/Bob` → Looks up user by name (with email=None)
+
+This approach allows a single function to be registered with multiple URI patterns while keeping the implementation clean and straightforward.
+
+Templates provide a powerful way to expose parameterized data access points following REST-like principles.
+
+## Error Handling
+
+
+
+If your resource function encounters an error, you can raise a standard Python exception (`ValueError`, `TypeError`, `FileNotFoundError`, custom exceptions, etc.) or a FastMCP `ResourceError`.
+
+By default, all exceptions (including their details) are logged and converted into an MCP error response to be sent back to the client LLM. This helps the LLM understand failures and react appropriately.
+
+If you want to mask internal error details for security reasons, you can:
+
+1. Use the `mask_error_details=True` parameter when creating your `FastMCP` instance:
+```python
+mcp = FastMCP(name="SecureServer", mask_error_details=True)
+```
+
+2. Or use `ResourceError` to explicitly control what error information is sent to clients:
+```python
+from fastmcp import FastMCP
+from fastmcp.exceptions import ResourceError
+
+mcp = FastMCP(name="DataServer")
+
+@mcp.resource("resource://safe-error")
+def fail_with_details() -> str:
+ """This resource provides detailed error information."""
+ # ResourceError contents are always sent back to clients,
+ # regardless of mask_error_details setting
+ raise ResourceError("Unable to retrieve data: file not found")
+
+@mcp.resource("resource://masked-error")
+def fail_with_masked_details() -> str:
+ """This resource masks internal error details when mask_error_details=True."""
+ # This message would be masked if mask_error_details=True
+ raise ValueError("Sensitive internal file path: /etc/secrets.conf")
+
+@mcp.resource("data://{id}")
+def get_data_by_id(id: str) -> dict:
+ """Template resources also support the same error handling pattern."""
+ if id == "secure":
+ raise ValueError("Cannot access secure data")
+ elif id == "missing":
+ raise ResourceError("Data ID 'missing' not found in database")
+ return {"id": id, "value": "data"}
+```
+
+When `mask_error_details=True`, only error messages from `ResourceError` will include details, other exceptions will be converted to a generic message.
+
+## Server Behavior
+
+### Duplicate Resources
+
+
+
+You can configure how the FastMCP server handles attempts to register multiple resources or templates with the same URI. Use the `on_duplicate_resources` setting during `FastMCP` initialization.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP(
+ name="ResourceServer",
+ on_duplicate_resources="error" # Raise error on duplicates
+)
+
+@mcp.resource("data://config")
+def get_config_v1(): return {"version": 1}
+
+# This registration attempt will raise a ValueError because
+# "data://config" is already registered and the behavior is "error".
+# @mcp.resource("data://config")
+# def get_config_v2(): return {"version": 2}
+```
+
+The duplicate behavior options are:
+
+- `"warn"` (default): Logs a warning, and the new resource/template replaces the old one.
+- `"error"`: Raises a `ValueError`, preventing the duplicate registration.
+- `"replace"`: Silently replaces the existing resource/template with the new one.
+- `"ignore"`: Keeps the original resource/template and ignores the new registration attempt.
+
+## Versioning
+
+
+
+Resources and resource templates support versioning, allowing you to maintain multiple implementations under the same URI while clients automatically receive the highest version. See [Versioning](/servers/versioning) for complete documentation on version comparison, retrieval, and migration patterns.
diff --git a/docs/v3/servers/sampling.mdx b/docs/v3/servers/sampling.mdx
new file mode 100644
index 000000000..8ea479eb0
--- /dev/null
+++ b/docs/v3/servers/sampling.mdx
@@ -0,0 +1,573 @@
+---
+title: Sampling
+sidebarTitle: Sampling
+description: Request LLM text generation from the client or a configured provider through the MCP context.
+icon: robot
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+LLM sampling allows your MCP tools to request text generation from an LLM during execution. This enables tools to leverage AI capabilities for analysis, generation, reasoning, and more—without the client needing to orchestrate multiple calls.
+
+By default, sampling requests are routed to the client's LLM. You can also configure a fallback handler to use a specific provider (like OpenAI) when the client doesn't support sampling, or to always use your own LLM regardless of client capabilities.
+
+## Overview
+
+The simplest use of sampling is passing a prompt string to `ctx.sample()`. The method sends the prompt to the LLM, waits for the complete response, and returns a `SamplingResult`. You can access the generated text through the `.text` attribute.
+
+```python
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP()
+
+@mcp.tool
+async def summarize(content: str, ctx: Context) -> str:
+ """Generate a summary of the provided content."""
+ result = await ctx.sample(f"Please summarize this:\n\n{content}")
+ return result.text or ""
+```
+
+The `SamplingResult` also provides `.result` (identical to `.text` for plain text responses) and `.history` containing the full message exchange—useful if you need to continue the conversation or debug the interaction.
+
+### System Prompts
+
+System prompts let you establish the LLM's role and behavioral guidelines before it processes your request. This is useful for controlling tone, enforcing constraints, or providing context that shouldn't clutter the user-facing prompt.
+
+````python
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP()
+
+@mcp.tool
+async def generate_code(concept: str, ctx: Context) -> str:
+ """Generate a Python code example for a concept."""
+ result = await ctx.sample(
+ messages=f"Write a Python example demonstrating '{concept}'.",
+ system_prompt=(
+ "You are an expert Python programmer. "
+ "Provide concise, working code without explanations."
+ ),
+ temperature=0.7,
+ max_tokens=300
+ )
+ return f"```python\n{result.text}\n```"
+````
+
+The `temperature` parameter controls randomness—higher values (up to 1.0) produce more varied outputs, while lower values make responses more deterministic. The `max_tokens` parameter limits response length.
+
+### Model Preferences
+
+Model preferences let you hint at which LLM the client should use for a request. You can pass a single model name or a list of preferences in priority order. These are hints rather than requirements—the actual model used depends on what the client has available.
+
+```python
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP()
+
+@mcp.tool
+async def technical_analysis(data: str, ctx: Context) -> str:
+ """Analyze data using a reasoning-focused model."""
+ result = await ctx.sample(
+ messages=f"Analyze this data:\n\n{data}",
+ model_preferences=["claude-opus-4-5", "gpt-5-2"],
+ temperature=0.2,
+ )
+ return result.text or ""
+```
+
+Use model preferences when different tasks benefit from different model characteristics. Creative writing might prefer faster models with higher temperature, while complex analysis might benefit from larger reasoning-focused models.
+
+### Multi-Turn Conversations
+
+For requests that need conversational context, construct a list of `SamplingMessage` objects representing the conversation history. Each message has a `role` ("user" or "assistant") and `content` (a `TextContent` object).
+
+```python
+from mcp.types import SamplingMessage, TextContent
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP()
+
+@mcp.tool
+async def contextual_analysis(query: str, data: str, ctx: Context) -> str:
+ """Analyze data with conversational context."""
+ messages = [
+ SamplingMessage(
+ role="user",
+ content=TextContent(type="text", text=f"Here's my data: {data}"),
+ ),
+ SamplingMessage(
+ role="assistant",
+ content=TextContent(type="text", text="I see the data. What would you like to know?"),
+ ),
+ SamplingMessage(
+ role="user",
+ content=TextContent(type="text", text=query),
+ ),
+ ]
+ result = await ctx.sample(messages=messages)
+ return result.text or ""
+```
+
+The LLM receives the full conversation thread and responds with awareness of the preceding context.
+
+### Fallback Handlers
+
+Client support for sampling is optional—some clients may not implement it. To ensure your tools work regardless of client capabilities, configure a `sampling_handler` that sends requests directly to an LLM provider.
+
+FastMCP provides built-in handlers for [OpenAI and Anthropic APIs](/clients/sampling#built-in-handlers). These handlers support the full sampling API including tools, automatically converting your Python functions to each provider's format.
+
+
+Install handlers with `pip install fastmcp[openai]` or `pip install fastmcp[anthropic]`.
+
+
+```python
+from fastmcp import FastMCP
+from fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler
+
+server = FastMCP(
+ name="My Server",
+ sampling_handler=OpenAISamplingHandler(default_model="gpt-4o-mini"),
+ sampling_handler_behavior="fallback",
+)
+```
+
+The `sampling_handler_behavior` parameter controls when the handler is used:
+
+- **`"fallback"`** (default): Use the handler only when the client doesn't support sampling. This lets capable clients use their own LLM while ensuring your tools still work with clients that lack sampling support.
+- **`"always"`**: Always use the handler, bypassing the client entirely. Use this when you need guaranteed control over which LLM processes requests—for cost control, compliance requirements, or when specific model characteristics are essential.
+
+## Structured Output
+
+
+
+When you need validated, typed data instead of free-form text, use the `result_type` parameter. FastMCP ensures the LLM returns data matching your type, handling validation and retries automatically.
+
+The `result_type` parameter accepts Pydantic models, dataclasses, and basic types like `int`, `list[str]`, or `dict[str, int]`. When you specify a result type, FastMCP automatically creates a `final_response` tool that the LLM calls to provide its response. If validation fails, the error is sent back to the LLM for retry.
+
+```python
+from pydantic import BaseModel
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP()
+
+class SentimentResult(BaseModel):
+ sentiment: str
+ confidence: float
+ reasoning: str
+
+@mcp.tool
+async def analyze_sentiment(text: str, ctx: Context) -> SentimentResult:
+ """Analyze text sentiment with structured output."""
+ result = await ctx.sample(
+ messages=f"Analyze the sentiment of: {text}",
+ result_type=SentimentResult,
+ )
+ return result.result # A validated SentimentResult object
+```
+
+When you call this tool, the LLM returns a structured response that FastMCP validates against your Pydantic model. You access the validated object through `result.result`, while `result.text` contains the JSON representation.
+
+### Structured Output with Tools
+
+Combine structured output with tools for agentic workflows that return validated data. The LLM uses your tools to gather information, then returns a response matching your type.
+
+```python
+from pydantic import BaseModel
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP()
+
+def search(query: str) -> str:
+ """Search the web for information."""
+ return f"Results for: {query}"
+
+def fetch_url(url: str) -> str:
+ """Fetch content from a URL."""
+ return f"Content from: {url}"
+
+class ResearchResult(BaseModel):
+ summary: str
+ sources: list[str]
+ confidence: float
+
+@mcp.tool
+async def research(topic: str, ctx: Context) -> ResearchResult:
+ """Research a topic and return structured findings."""
+ result = await ctx.sample(
+ messages=f"Research: {topic}",
+ tools=[search, fetch_url],
+ result_type=ResearchResult,
+ )
+ return result.result
+```
+
+
+Structured output with automatic validation only applies to `sample()`. With `sample_step()`, you must manage structured output yourself.
+
+
+## Tool Use
+
+
+
+Sampling with tools enables agentic workflows where the LLM can call functions to gather information before responding. This implements [SEP-1577](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1577), allowing the LLM to autonomously orchestrate multi-step operations.
+
+Pass Python functions to the `tools` parameter, and FastMCP handles the execution loop automatically—calling tools, returning results to the LLM, and continuing until the LLM provides a final response.
+
+### Defining Tools
+
+Define regular Python functions with type hints and docstrings. FastMCP extracts the function's name, docstring, and parameter types to create tool schemas that the LLM can understand.
+
+```python
+from fastmcp import FastMCP, Context
+
+def search(query: str) -> str:
+ """Search the web for information."""
+ return f"Results for: {query}"
+
+def get_time() -> str:
+ """Get the current time."""
+ from datetime import datetime
+ return datetime.now().strftime("%H:%M:%S")
+
+mcp = FastMCP()
+
+@mcp.tool
+async def research(question: str, ctx: Context) -> str:
+ """Answer questions using available tools."""
+ result = await ctx.sample(
+ messages=question,
+ tools=[search, get_time],
+ )
+ return result.text or ""
+```
+
+The LLM sees each function's signature and docstring, using this information to decide when and how to call them. Tool errors are caught and sent back to the LLM, allowing it to recover gracefully. An internal safety limit prevents infinite loops.
+
+### Custom Tool Definitions
+
+For custom names or descriptions, use `SamplingTool.from_function()`:
+
+```python
+from fastmcp.server.sampling import SamplingTool
+
+tool = SamplingTool.from_function(
+ my_func,
+ name="custom_name",
+ description="Custom description"
+)
+
+result = await ctx.sample(messages="...", tools=[tool])
+```
+
+### Error Handling
+
+By default, when a sampling tool raises an exception, the error message (including details) is sent back to the LLM so it can attempt recovery. To prevent sensitive information from leaking to the LLM, use the `mask_error_details` parameter:
+
+```python
+result = await ctx.sample(
+ messages=question,
+ tools=[search],
+ mask_error_details=True, # Generic error messages only
+)
+```
+
+When `mask_error_details=True`, tool errors become generic messages like `"Error executing tool 'search'"` instead of exposing stack traces or internal details.
+
+To intentionally provide specific error messages to the LLM regardless of masking, raise `ToolError`:
+
+```python
+from fastmcp.exceptions import ToolError
+
+def search(query: str) -> str:
+ """Search for information."""
+ if not query.strip():
+ raise ToolError("Search query cannot be empty")
+ return f"Results for: {query}"
+```
+
+`ToolError` messages always pass through to the LLM, making it the escape hatch for errors you want the LLM to see and handle.
+
+### Concurrent Tool Execution
+
+By default, tools execute sequentially — one at a time, in order. When your tools are independent (no shared state between them), you can execute them in parallel with `tool_concurrency`:
+
+```python
+result = await ctx.sample(
+ messages="Research these three topics",
+ tools=[search, fetch_url],
+ tool_concurrency=0, # Unlimited parallel execution
+)
+```
+
+The `tool_concurrency` parameter controls how many tools run at once:
+
+- **`None`** (default): Sequential execution
+- **`0`**: Unlimited parallel execution
+- **`N > 0`**: Execute at most N tools concurrently
+
+For tools that must not run concurrently (file writes, shared state mutations, etc.), mark them as `sequential` when creating the `SamplingTool`:
+
+```python
+from fastmcp.server.sampling import SamplingTool
+
+db_writer = SamplingTool.from_function(
+ write_to_db,
+ sequential=True, # Forces all tools in the batch to run sequentially
+)
+
+result = await ctx.sample(
+ messages="Process this data",
+ tools=[search, db_writer],
+ tool_concurrency=0, # Would be parallel, but db_writer forces sequential
+)
+```
+
+
+When any tool in a batch has `sequential=True`, the entire batch executes sequentially regardless of `tool_concurrency`. This is a conservative guarantee — if one tool needs ordering, all tools in that batch respect it.
+
+
+### Client Requirements
+
+
+Sampling with tools requires the client to advertise the `sampling.tools` capability. FastMCP clients do this automatically. For external clients that don't support tool-enabled sampling, configure a fallback handler with `sampling_handler_behavior="always"`.
+
+
+## Advanced Control
+
+
+
+While `sample()` handles the tool execution loop automatically, some scenarios require fine-grained control over each step. The `sample_step()` method makes a single LLM call and returns a `SampleStep` containing the response and updated history.
+
+Unlike `sample()`, `sample_step()` is stateless—it doesn't remember previous calls. You control the conversation by passing the full message history each time. The returned `step.history` includes all messages up through the current response, making it easy to continue the loop.
+
+Use `sample_step()` when you need to:
+
+- Inspect tool calls before they execute
+- Implement custom termination conditions
+- Add logging, metrics, or checkpointing between steps
+- Build custom agentic loops with domain-specific logic
+
+### Basic Loop
+
+By default, `sample_step()` executes any tool calls and includes the results in the history. Call it in a loop, passing the updated history each time, until a stop condition is met.
+
+```python
+from mcp.types import SamplingMessage
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP()
+
+def search(query: str) -> str:
+ return f"Results for: {query}"
+
+def get_time() -> str:
+ return "12:00 PM"
+
+@mcp.tool
+async def controlled_agent(question: str, ctx: Context) -> str:
+ """Agent with manual loop control."""
+ messages: list[str | SamplingMessage] = [question]
+
+ while True:
+ step = await ctx.sample_step(
+ messages=messages,
+ tools=[search, get_time],
+ )
+
+ if step.is_tool_use:
+ # Tools already executed (execute_tools=True by default)
+ for call in step.tool_calls:
+ print(f"Called tool: {call.name}")
+
+ if not step.is_tool_use:
+ return step.text or ""
+
+ messages = step.history
+```
+
+### SampleStep Properties
+
+Each `SampleStep` provides information about what the LLM returned:
+
+| Property | Description |
+|----------|-------------|
+| `step.is_tool_use` | True if the LLM requested tool calls |
+| `step.tool_calls` | List of tool calls requested (if any) |
+| `step.text` | The text content (if any) |
+| `step.history` | All messages exchanged so far |
+
+The contents of `step.history` depend on `execute_tools`:
+- **`execute_tools=True`** (default): Includes tool results, ready for the next iteration
+- **`execute_tools=False`**: Includes the assistant's tool request, but you add results yourself
+
+### Manual Tool Execution
+
+Set `execute_tools=False` to handle tool execution yourself. When disabled, `step.history` contains the user message and the assistant's response with tool calls—but no tool results. You execute the tools and append the results as a user message.
+
+```python
+from mcp.types import SamplingMessage, ToolResultContent, TextContent
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP()
+
+@mcp.tool
+async def research(question: str, ctx: Context) -> str:
+ """Research with manual tool handling."""
+
+ def search(query: str) -> str:
+ return f"Results for: {query}"
+
+ def get_time() -> str:
+ return "12:00 PM"
+
+ tools = {"search": search, "get_time": get_time}
+ messages: list[SamplingMessage] = [question]
+
+ while True:
+ step = await ctx.sample_step(
+ messages=messages,
+ tools=list(tools.values()),
+ execute_tools=False,
+ )
+
+ if not step.is_tool_use:
+ return step.text or ""
+
+ # Execute tools and collect results
+ tool_results = []
+ for call in step.tool_calls:
+ fn = tools[call.name]
+ result = fn(**call.input)
+ tool_results.append(
+ ToolResultContent(
+ type="tool_result",
+ toolUseId=call.id,
+ content=[TextContent(type="text", text=result)],
+ )
+ )
+
+ messages = list(step.history)
+ messages.append(SamplingMessage(role="user", content=tool_results))
+```
+
+To report an error to the LLM, set `isError=True` on the tool result:
+
+```python
+tool_result = ToolResultContent(
+ type="tool_result",
+ toolUseId=call.id,
+ content=[TextContent(type="text", text="Permission denied")],
+ isError=True,
+)
+```
+
+## Method Reference
+
+
+
+ Request text generation from the LLM, running to completion automatically.
+
+
+
+ The prompt to send. Can be a simple string or a list of messages for multi-turn conversations.
+
+
+
+ Instructions that establish the LLM's role and behavior.
+
+
+
+ Controls randomness (0.0 = deterministic, 1.0 = creative).
+
+
+
+ Maximum tokens to generate.
+
+
+
+ Hints for which model the client should use.
+
+
+
+ Functions the LLM can call during sampling.
+
+
+
+ A type for validated structured output. Supports Pydantic models, dataclasses, and basic types like `int`, `list[str]`, or `dict[str, int]`.
+
+
+
+ If True, mask detailed error messages from tool execution. When None (default), uses the global `settings.mask_error_details` value. Tools can raise `ToolError` to bypass masking and provide specific error messages to the LLM.
+
+
+
+ Controls parallel execution of tools. `None` (default) for sequential, `0` for unlimited parallel, or a positive integer for bounded concurrency. If any tool has `sequential=True`, all tools execute sequentially regardless.
+
+
+
+
+
+
+ - `.text`: The raw text response (or JSON for structured output)
+ - `.result`: The typed result—same as `.text` for plain text, or a validated Pydantic object for structured output
+ - `.history`: All messages exchanged during sampling
+
+
+
+
+
+
+
+ Make a single LLM sampling call. Use this for fine-grained control over the sampling loop.
+
+
+
+ The prompt or conversation history.
+
+
+
+ Instructions that establish the LLM's role and behavior.
+
+
+
+ Controls randomness (0.0 = deterministic, 1.0 = creative).
+
+
+
+ Maximum tokens to generate.
+
+
+
+ Functions the LLM can call during sampling.
+
+
+
+ Controls tool usage: `"auto"`, `"required"`, or `"none"`.
+
+
+
+ If True, execute tool calls and append results to history. If False, return immediately with tool calls available for manual execution.
+
+
+
+ If True, mask detailed error messages from tool execution.
+
+
+
+ Controls parallel execution of tools. `None` (default) for sequential, `0` for unlimited parallel, or a positive integer for bounded concurrency.
+
+
+
+
+
+ - `.response`: The raw LLM response
+ - `.history`: Messages including input, assistant response, and tool results
+ - `.is_tool_use`: True if the LLM requested tool execution
+ - `.tool_calls`: List of tool calls (if any)
+ - `.text`: The text content (if any)
+
+
+
+
diff --git a/docs/v3/servers/server.mdx b/docs/v3/servers/server.mdx
new file mode 100644
index 000000000..65befc502
--- /dev/null
+++ b/docs/v3/servers/server.mdx
@@ -0,0 +1,285 @@
+---
+title: The FastMCP Server
+sidebarTitle: Overview
+description: The core FastMCP server class for building MCP applications
+icon: server
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+The `FastMCP` class is the central piece of every FastMCP application. It acts as the container for your tools, resources, and prompts, managing communication with MCP clients and orchestrating the entire server lifecycle.
+
+## Creating a Server
+
+At its simplest, a FastMCP server just needs a name. Everything else has sensible defaults.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("MyServer")
+```
+
+Instructions help clients (and the LLMs behind them) understand what your server does and how to use it effectively.
+
+```python
+mcp = FastMCP(
+ "DataAnalysis",
+ instructions="Provides tools for analyzing numerical datasets. Start with get_summary() for an overview.",
+)
+```
+
+## Components
+
+FastMCP servers expose three types of components to clients, each serving a distinct role in the MCP protocol.
+
+**Tools** are functions that clients invoke to perform actions or access external systems.
+
+```python
+@mcp.tool
+def multiply(a: float, b: float) -> float:
+ """Multiplies two numbers together."""
+ return a * b
+```
+
+**Resources** expose data that clients can read — passive data sources rather than invocable functions.
+
+```python
+@mcp.resource("data://config")
+def get_config() -> dict:
+ return {"theme": "dark", "version": "1.0"}
+```
+
+**Prompts** are reusable message templates that guide LLM interactions.
+
+```python
+@mcp.prompt
+def analyze_data(data_points: list[float]) -> str:
+ formatted_data = ", ".join(str(point) for point in data_points)
+ return f"Please analyze these data points: {formatted_data}"
+```
+
+Each component type has detailed documentation: [Tools](/servers/tools), [Resources](/servers/resources) (including [Resource Templates](/servers/resources#resource-templates)), and [Prompts](/servers/prompts).
+
+## Running the Server
+
+Start your server by calling `mcp.run()`. The `if __name__` guard ensures compatibility with MCP clients that launch your server as a subprocess.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("MyServer")
+
+@mcp.tool
+def greet(name: str) -> str:
+ """Greet a user by name."""
+ return f"Hello, {name}!"
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+FastMCP supports several transports:
+- **STDIO** (default): For local integrations and CLI tools
+- **HTTP**: For web services using the Streamable HTTP protocol
+- **SSE**: Legacy web transport (deprecated)
+
+```python
+# Run with HTTP transport
+mcp.run(transport="http", host="127.0.0.1", port=9000)
+```
+
+The server can also be run using the FastMCP CLI. For detailed information on transports and deployment, see [Running Your Server](/deployment/running-server).
+
+
+## Configuration Reference
+
+The `FastMCP` constructor accepts parameters organized into four categories: identity, composition, behavior, and handlers.
+
+### Identity
+
+These parameters control how your server presents itself to clients.
+
+
+
+ A human-readable name for your server, shown in client applications and logs
+
+
+
+ Description of how to interact with this server. Clients surface these instructions to help LLMs understand the server's purpose and available functionality
+
+
+
+ Version string for your server. Defaults to the FastMCP library version if not provided
+
+
+
+
+
+ URL to a website with more information about your server. Displayed in client applications
+
+
+
+
+
+ List of icon representations for your server. See [Icons](/servers/icons) for details
+
+
+
+
+
+ Arbitrary experimental capabilities to advertise in the MCP `initialize` response. Use this to declare cross-server interop conventions or draft extensions that follow the MCP spec's `experimental` field. Keys are capability names; values are free-form dicts. FastMCP's built-in derived capabilities (`tools`, `resources`, etc.) are unaffected — this only populates `capabilities.experimental`
+
+
+
+### Composition
+
+These parameters control what your server is built from — its components, middleware, providers, and lifecycle.
+
+
+
+ Tools to register on the server. An alternative to the `@mcp.tool` decorator when you need to add tools programmatically
+
+
+
+ Authentication provider for securing HTTP-based transports. See [Authentication](/servers/auth/authentication) for configuration
+
+
+
+ [Middleware](/servers/middleware) that intercepts and transforms every MCP message flowing through the server — requests, responses, and notifications in both directions. Use for cross-cutting concerns like logging, error handling, and rate limiting
+
+
+
+ [Providers](/servers/providers/overview) that supply tools, resources, and prompts dynamically. Providers are queried at request time, so they can serve components from databases, APIs, or other external sources
+
+
+
+
+
+ Server-level [transforms](/servers/transforms/transforms) to apply to all components. Transforms modify how tools, resources, and prompts are presented to clients — for example, [search transforms](/servers/transforms/tool-search) replace large catalogs with on-demand discovery
+
+
+
+ Server-level setup and teardown logic that runs when the server starts and stops. See [Lifespans](/servers/lifespan) for composable lifespans
+
+
+
+### Behavior
+
+These parameters tune how the server processes requests and communicates with clients.
+
+
+
+ How to handle duplicate component registrations
+
+
+
+
+
+ When `False` (default), FastMCP uses Pydantic's flexible validation that coerces compatible inputs (e.g., `"10"` → `10` for int parameters). When `True`, validates inputs against the exact JSON Schema before calling your function, rejecting type mismatches. See [Input Validation Modes](/servers/tools#input-validation-modes) for details
+
+
+
+ When `True`, replaces internal error details in tool/resource responses with a generic message to avoid leaking implementation details to clients. Defaults to the `FASTMCP_MASK_ERROR_DETAILS` environment variable
+
+
+
+
+
+ Maximum items per page for list operations (`tools/list`, `resources/list`, etc.). When `None`, all results are returned in a single response. See [Pagination](/servers/pagination) for details
+
+
+
+ Enable background task support. When `True`, tools and resources can return `CreateTaskResult` to run work asynchronously while the client polls for results
+
+
+
+
+
+ Default minimum log level for messages sent to MCP clients via `context.log()`. When set, messages below this level are suppressed. Individual clients can override this per-session using the MCP `logging/setLevel` request. One of `"debug"`, `"info"`, `"notice"`, `"warning"`, `"error"`, `"critical"`, `"alert"`, or `"emergency"`
+
+
+
+ Automatically dereference `$ref` pointers in JSON schemas generated from complex Pydantic models. Most clients require flat schemas without `$ref`, so this should usually stay enabled
+
+
+
+### Handlers and Storage
+
+These parameters provide custom handlers for MCP capabilities and persistent storage for session state.
+
+
+
+ Custom handler for MCP sampling requests (server-initiated LLM calls). See [Sampling](/servers/sampling) for details
+
+
+
+ When `"fallback"`, the sampling handler is used only when no tool-specific handler exists. When `"always"`, this handler is used for all sampling requests
+
+
+
+ Persistent key-value store for session state that survives across requests. Defaults to an in-memory store. Provide a custom implementation for persistence across server restarts
+
+
+
+
+## Tag-Based Filtering
+
+
+
+Tags let you categorize components and selectively expose them. This is useful for creating different views of your server for different environments or user types.
+
+```python
+@mcp.tool(tags={"public", "utility"})
+def public_tool() -> str:
+ return "This tool is public"
+
+@mcp.tool(tags={"internal", "admin"})
+def admin_tool() -> str:
+ return "This tool is for admins only"
+```
+
+The filtering logic works as follows:
+- **Enable with `only=True`**: Switches to allowlist mode — only components with at least one matching tag are exposed
+- **Disable**: Components with any matching tag are hidden
+- **Precedence**: Later calls override earlier ones, so call `disable` after `enable` to exclude from an allowlist
+
+
+To ensure a component is never exposed, you can set `enabled=False` on the component itself. See the component-specific documentation for details.
+
+
+```python
+# Only expose components tagged with "public"
+mcp = FastMCP()
+mcp.enable(tags={"public"}, only=True)
+
+# Hide components tagged as "internal" or "deprecated"
+mcp = FastMCP()
+mcp.disable(tags={"internal", "deprecated"})
+
+# Combine both: show admin tools but hide deprecated ones
+mcp = FastMCP()
+mcp.enable(tags={"admin"}, only=True).disable(tags={"deprecated"})
+```
+
+This filtering applies to all component types (tools, resources, resource templates, and prompts) and affects both listing and access.
+
+## Custom Routes
+
+When running with HTTP transport, you can add custom web routes alongside your MCP endpoint using the `@custom_route` decorator.
+
+```python
+from fastmcp import FastMCP
+from starlette.requests import Request
+from starlette.responses import PlainTextResponse
+
+mcp = FastMCP("MyServer")
+
+@mcp.custom_route("/health", methods=["GET"])
+async def health_check(request: Request) -> PlainTextResponse:
+ return PlainTextResponse("OK")
+
+if __name__ == "__main__":
+ mcp.run(transport="http") # Health check at http://localhost:8000/health
+```
+
+Custom routes are useful for health checks, status endpoints, and simple webhooks. For more complex web applications, consider [mounting your MCP server into a FastAPI or Starlette app](/deployment/http#integration-with-web-frameworks).
diff --git a/docs/v3/servers/storage-backends.mdx b/docs/v3/servers/storage-backends.mdx
new file mode 100644
index 000000000..d13ce176d
--- /dev/null
+++ b/docs/v3/servers/storage-backends.mdx
@@ -0,0 +1,296 @@
+---
+title: Storage Backends
+sidebarTitle: Storage Backends
+description: Configure persistent and distributed storage for caching and OAuth state management
+icon: database
+tag: NEW
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+FastMCP uses pluggable storage backends for caching responses and managing OAuth state. By default, all storage is in-memory, which is perfect for development but doesn't persist across restarts. FastMCP includes support for multiple storage backends, and you can easily extend it with custom implementations.
+
+
+The storage layer is powered by **[py-key-value-aio](https://github.com/strawgate/py-key-value)**, an async key-value library maintained by a core FastMCP maintainer. This library provides a unified interface for multiple backends, making it easy to swap implementations based on your deployment needs.
+
+
+## Available Backends
+
+### In-Memory Storage
+
+**Best for:** Development, testing, single-process deployments
+
+In-memory storage is the default for all FastMCP storage needs. It's fast, requires no setup, and is perfect for getting started.
+
+```python
+from key_value.aio.stores.memory import MemoryStore
+
+# Used by default - no configuration needed
+# But you can also be explicit:
+cache_store = MemoryStore()
+```
+
+**Characteristics:**
+- ✅ No setup required
+- ✅ Very fast
+- ❌ Data lost on restart
+- ❌ Not suitable for multi-process deployments
+
+### File Storage
+
+**Best for:** Single-server production deployments, persistent caching
+
+File storage persists data to the filesystem as one JSON file per key, allowing it to survive server restarts. This is the default backend for OAuth storage on Mac and Windows.
+
+```python
+from pathlib import Path
+from key_value.aio.stores.filetree import (
+ FileTreeStore,
+ FileTreeV1KeySanitizationStrategy,
+ FileTreeV1CollectionSanitizationStrategy,
+)
+from fastmcp.server.middleware.caching import ResponseCachingMiddleware
+
+storage_dir = Path("/var/cache/fastmcp")
+store = FileTreeStore(
+ data_directory=storage_dir,
+ key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(storage_dir),
+ collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(storage_dir),
+)
+
+# Persistent response cache
+middleware = ResponseCachingMiddleware(cache_storage=store)
+```
+
+
+**Sanitization strategies are required** when using `FileTreeStore`. Without them, keys containing special characters (such as URL-based OAuth client IDs like `https://claude.ai/oauth/claude-code-client-metadata`) will be used as-is in filesystem paths, causing `FileNotFoundError` crashes. The V1 strategies shown above are safe defaults — alphanumeric names pass through as-is for readability, while special characters are hashed to prevent path errors and traversal attacks. Changing sanitization strategies after data has been written is a breaking change, so choose your strategy upfront.
+
+
+**Characteristics:**
+- ✅ Data persists across restarts
+- ✅ No external dependencies
+- ✅ Human-readable files on disk
+- ❌ Not suitable for distributed deployments
+- ❌ Filesystem access required
+
+### Redis
+
+**Best for:** Distributed production deployments, shared caching across multiple servers
+
+
+Redis support requires an optional dependency: `pip install 'py-key-value-aio[redis]'`
+
+
+Redis provides distributed caching and state management, ideal for production deployments with multiple server instances.
+
+```python
+from key_value.aio.stores.redis import RedisStore
+from fastmcp.server.middleware.caching import ResponseCachingMiddleware
+
+# Distributed response cache
+middleware = ResponseCachingMiddleware(
+ cache_storage=RedisStore(host="redis.example.com", port=6379)
+)
+```
+
+With authentication:
+
+```python
+from key_value.aio.stores.redis import RedisStore
+
+cache_store = RedisStore(
+ host="redis.example.com",
+ port=6379,
+ password="your-redis-password"
+)
+```
+
+For OAuth token storage:
+
+```python
+import os
+from fastmcp.server.auth.providers.github import GitHubProvider
+from key_value.aio.stores.redis import RedisStore
+
+auth = GitHubProvider(
+ client_id=os.environ["GITHUB_CLIENT_ID"],
+ client_secret=os.environ["GITHUB_CLIENT_SECRET"],
+ base_url="https://your-server.com",
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ client_storage=RedisStore(host="redis.example.com", port=6379)
+)
+```
+
+**Characteristics:**
+- ✅ Distributed and highly available
+- ✅ Fast in-memory performance
+- ✅ Works across multiple server instances
+- ✅ Built-in TTL support
+- ❌ Requires Redis infrastructure
+- ❌ Network latency vs local storage
+
+### Other Backends from py-key-value-aio
+
+The py-key-value-aio library includes additional implementations for various storage systems:
+
+- **DynamoDB** - AWS distributed database
+- **MongoDB** - NoSQL document store
+- **Elasticsearch** - Distributed search and analytics
+- **Memcached** - Distributed memory caching
+- **RocksDB** - Embedded high-performance key-value store
+- **Valkey** - Redis-compatible server
+
+For configuration details on these backends, consult the [py-key-value-aio documentation](https://github.com/strawgate/py-key-value).
+
+
+Before using these backends in production, review the [py-key-value documentation](https://github.com/strawgate/py-key-value) to understand the maturity level and limitations of your chosen backend. Some backends may be in preview or have specific constraints that make them unsuitable for production use.
+
+
+## Use Cases in FastMCP
+
+### Server-Side OAuth Token Storage
+
+The [OAuth Proxy](/servers/auth/oauth-proxy) and OAuth auth providers use storage for persisting OAuth client registrations and upstream tokens. **By default, storage is automatically encrypted using `FernetEncryptionWrapper`.** When providing custom storage, wrap it in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest.
+
+**Development (default behavior):**
+
+By default, FastMCP automatically manages keys and storage based on your platform:
+- **Mac/Windows**: Keys are auto-managed via system keyring, storage defaults to disk. Suitable **only** for development and local testing.
+- **Linux**: Keys are ephemeral, storage defaults to memory.
+
+No configuration needed:
+
+```python
+from fastmcp.server.auth.providers.github import GitHubProvider
+
+auth = GitHubProvider(
+ client_id="your-id",
+ client_secret="your-secret",
+ base_url="https://your-server.com"
+)
+```
+
+**Production:**
+
+For production deployments, configure explicit keys and persistent network-accessible storage with encryption:
+
+```python
+import os
+from fastmcp.server.auth.providers.github import GitHubProvider
+from key_value.aio.stores.redis import RedisStore
+from key_value.aio.wrappers.encryption import FernetEncryptionWrapper
+from cryptography.fernet import Fernet
+
+auth = GitHubProvider(
+ client_id=os.environ["GITHUB_CLIENT_ID"],
+ client_secret=os.environ["GITHUB_CLIENT_SECRET"],
+ base_url="https://your-server.com",
+ # Explicit JWT signing key (required for production)
+ jwt_signing_key=os.environ["JWT_SIGNING_KEY"],
+ # Encrypted persistent storage (required for production)
+ client_storage=FernetEncryptionWrapper(
+ key_value=RedisStore(host="redis.example.com", port=6379),
+ fernet=Fernet(os.environ["STORAGE_ENCRYPTION_KEY"])
+ )
+)
+```
+
+Both parameters are required for production. **Wrap your storage in `FernetEncryptionWrapper` to encrypt sensitive OAuth tokens at rest** - without it, tokens are stored in plaintext. See [OAuth Token Security](/deployment/http#oauth-token-security) and [Key and Storage Management](/servers/auth/oauth-proxy#key-and-storage-management) for complete setup details.
+
+### Response Caching Middleware
+
+The [Response Caching Middleware](/servers/middleware#caching-middleware) caches tool calls, resource reads, and prompt requests. Storage configuration is passed via the `cache_storage` parameter:
+
+```python
+from pathlib import Path
+from fastmcp import FastMCP
+from fastmcp.server.middleware.caching import ResponseCachingMiddleware
+from key_value.aio.stores.filetree import (
+ FileTreeStore,
+ FileTreeV1KeySanitizationStrategy,
+ FileTreeV1CollectionSanitizationStrategy,
+)
+
+mcp = FastMCP("My Server")
+
+cache_dir = Path("cache")
+cache_store = FileTreeStore(
+ data_directory=cache_dir,
+ key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(cache_dir),
+ collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(cache_dir),
+)
+
+# Cache to disk instead of memory
+mcp.add_middleware(ResponseCachingMiddleware(cache_storage=cache_store))
+```
+
+For multi-server deployments sharing a Redis instance:
+
+```python
+from fastmcp.server.middleware.caching import ResponseCachingMiddleware
+from key_value.aio.stores.redis import RedisStore
+from key_value.aio.wrappers.prefix_collections import PrefixCollectionsWrapper
+
+base_store = RedisStore(host="redis.example.com")
+namespaced_store = PrefixCollectionsWrapper(
+ key_value=base_store,
+ prefix="my-server"
+)
+
+middleware = ResponseCachingMiddleware(cache_storage=namespaced_store)
+```
+
+### Client-Side OAuth Token Storage
+
+The [FastMCP Client](/clients/client) uses storage for persisting OAuth tokens locally. By default, tokens are stored in memory:
+
+```python
+from pathlib import Path
+from fastmcp.client.auth import OAuth
+from key_value.aio.stores.filetree import (
+ FileTreeStore,
+ FileTreeV1KeySanitizationStrategy,
+ FileTreeV1CollectionSanitizationStrategy,
+)
+
+# Store tokens on disk for persistence across restarts
+token_dir = Path("~/.local/share/fastmcp/tokens").expanduser()
+token_storage = FileTreeStore(
+ data_directory=token_dir,
+ key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(token_dir),
+ collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(token_dir),
+)
+
+oauth_provider = OAuth(
+ mcp_url="https://your-mcp-server.com/mcp/sse",
+ token_storage=token_storage
+)
+```
+
+This allows clients to reconnect without re-authenticating after restarts.
+
+## Choosing a Backend
+
+| Backend | Development | Single Server | Multi-Server | Cloud Native |
+|---------|-------------|---------------|--------------|--------------|
+| Memory | ✅ Best | ⚠️ Limited | ❌ | ❌ |
+| File | ✅ Good | ✅ Recommended | ❌ | ⚠️ |
+| Redis | ⚠️ Overkill | ✅ Good | ✅ Best | ✅ Best |
+| DynamoDB | ❌ | ⚠️ | ✅ | ✅ Best (AWS) |
+| MongoDB | ❌ | ⚠️ | ✅ | ✅ Good |
+
+**Decision tree:**
+
+1. **Just starting?** Use **Memory** (default) - no configuration needed
+2. **Single server, needs persistence?** Use **File**
+3. **Multiple servers or cloud deployment?** Use **Redis** or **DynamoDB**
+4. **Existing infrastructure?** Look for a matching py-key-value-aio backend
+
+## More Resources
+
+- [py-key-value-aio GitHub](https://github.com/strawgate/py-key-value) - Full library documentation
+- [Response Caching Middleware](/servers/middleware#caching-middleware) - Using storage for caching
+- [OAuth Token Security](/deployment/http#oauth-token-security) - Production OAuth configuration
+- [HTTP Deployment](/deployment/http) - Complete deployment guide
diff --git a/docs/v3/servers/tasks.mdx b/docs/v3/servers/tasks.mdx
new file mode 100644
index 000000000..d4aae9f54
--- /dev/null
+++ b/docs/v3/servers/tasks.mdx
@@ -0,0 +1,263 @@
+---
+title: Background Tasks
+sidebarTitle: Background Tasks
+description: Run long-running operations asynchronously with progress tracking
+icon: clock
+tag: "NEW"
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+
+Background tasks require the `tasks` optional extra. See [installation instructions](#enabling-background-tasks) below.
+
+
+FastMCP implements the MCP background task protocol ([SEP-1686](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks)), giving your servers a production-ready distributed task scheduler with a single decorator change.
+
+
+**What is Docket?** FastMCP's task system is powered by [Docket](https://github.com/chrisguidry/docket), originally built by [Prefect](https://prefect.io) to power [Prefect Cloud](https://www.prefect.io/prefect/cloud)'s managed task scheduling and execution service, where it processes millions of concurrent tasks every day. Docket is now open-sourced for the community.
+
+
+
+## What Are MCP Background Tasks?
+
+In MCP, all component interactions are blocking by default. When a client calls a tool, reads a resource, or fetches a prompt, it sends a request and waits for the response. For operations that take seconds or minutes, this creates a poor user experience.
+
+The MCP background task protocol solves this by letting clients:
+1. **Start** an operation and receive a task ID immediately
+2. **Track** progress as the operation runs
+3. **Retrieve** the result when ready
+
+FastMCP handles all of this for you. Add `task=True` to your decorator, and your function gains full background execution with progress reporting, distributed processing, and horizontal scaling.
+
+### MCP Background Tasks vs Python Concurrency
+
+You can always use Python's concurrency primitives (asyncio, threads, multiprocessing) or external task queues in your FastMCP servers. FastMCP is just Python—run code however you like.
+
+MCP background tasks are different: they're **protocol-native**. This means MCP clients that support the task protocol can start operations, receive progress updates, and retrieve results through the standard MCP interface. The coordination happens at the protocol level, not inside your application code.
+
+## Enabling Background Tasks
+
+ Background tasks require the `tasks` extra:
+
+```bash
+pip install "fastmcp[tasks]"
+```
+
+Add `task=True` to any tool, resource, resource template, or prompt decorator. This marks the component as capable of background execution.
+
+```python {6}
+import asyncio
+from fastmcp import FastMCP
+
+mcp = FastMCP("MyServer")
+
+@mcp.tool(task=True)
+async def slow_computation(duration: int) -> str:
+ """A long-running operation."""
+ for i in range(duration):
+ await asyncio.sleep(1)
+ return f"Completed in {duration} seconds"
+```
+
+When a client requests background execution, the call returns immediately with a task ID. The work executes in a background worker, and the client can poll for status or wait for the result.
+
+
+Background tasks require async functions. Attempting to use `task=True` with a sync function raises a `ValueError` at registration time.
+
+
+## Execution Modes
+
+For fine-grained control over task execution behavior, use `TaskConfig` instead of the boolean shorthand. The MCP task protocol defines three execution modes:
+
+| Mode | Client calls without task | Client calls with task |
+|------|--------------------------|------------------------|
+| `"forbidden"` | Executes synchronously | Error: task not supported |
+| `"optional"` | Executes synchronously | Executes as background task |
+| `"required"` | Error: task required | Executes as background task |
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.tasks import TaskConfig
+
+mcp = FastMCP("MyServer")
+
+# Supports both sync and background execution (default when task=True)
+@mcp.tool(task=TaskConfig(mode="optional"))
+async def flexible_task() -> str:
+ return "Works either way"
+
+# Requires background execution - errors if client doesn't request task
+@mcp.tool(task=TaskConfig(mode="required"))
+async def must_be_background() -> str:
+ return "Only runs as a background task"
+
+# No task support (default when task=False or omitted)
+@mcp.tool(task=TaskConfig(mode="forbidden"))
+async def sync_only() -> str:
+ return "Never runs as background task"
+```
+
+The boolean shortcuts map to these modes:
+- `task=True` → `TaskConfig(mode="optional")`
+- `task=False` → `TaskConfig(mode="forbidden")`
+
+### Poll Interval
+
+
+
+When clients poll for task status, the server tells them how frequently to check back. By default, FastMCP suggests a 5-second interval, but you can customize this per component:
+
+```python
+from datetime import timedelta
+from fastmcp import FastMCP
+from fastmcp.server.tasks import TaskConfig
+
+mcp = FastMCP("MyServer")
+
+# Poll every 2 seconds for a fast-completing task
+@mcp.tool(task=TaskConfig(mode="optional", poll_interval=timedelta(seconds=2)))
+async def quick_task() -> str:
+ return "Done quickly"
+
+# Poll every 30 seconds for a long-running task
+@mcp.tool(task=TaskConfig(mode="optional", poll_interval=timedelta(seconds=30)))
+async def slow_task() -> str:
+ return "Eventually done"
+```
+
+Shorter intervals give clients faster feedback but increase server load. Longer intervals reduce load but delay status updates.
+
+### Server-Wide Default
+
+To enable background task support for all components by default, pass `tasks=True` to the constructor. Individual decorators can still override this with `task=False`.
+
+```python
+mcp = FastMCP("MyServer", tasks=True)
+```
+
+
+If your server defines any synchronous tools, resources, or prompts, you will need to explicitly set `task=False` on their decorators to avoid an error.
+
+
+### Graceful Degradation
+
+When a client requests background execution but the component has `mode="forbidden"`, FastMCP executes synchronously and returns the result inline. This follows the SEP-1686 specification for graceful degradation—clients can always request background execution without worrying about server capabilities.
+
+Conversely, when a component has `mode="required"` but the client doesn't request background execution, FastMCP returns an error indicating that task execution is required.
+
+### Configuration
+
+| Environment Variable | Default | Description |
+|---------------------|---------|-------------|
+| `FASTMCP_DOCKET_URL` | `memory://` | Backend URL (`memory://` or `redis://host:port/db`) |
+
+## Backends
+
+FastMCP supports two backends for task execution, each with different tradeoffs.
+
+### In-Memory Backend (Default)
+
+The in-memory backend (`memory://`) requires zero configuration and works out of the box.
+
+**Advantages:**
+- No external dependencies
+- Simple single-process deployment
+
+**Disadvantages:**
+- **Ephemeral**: If the server restarts, all pending tasks are lost
+- **Higher latency**: ~250ms task pickup time vs single-digit milliseconds with Redis
+- **No horizontal scaling**: Single process only—you cannot add additional workers
+
+### Redis Backend
+
+For production deployments, use Redis (or Valkey) as your backend by setting `FASTMCP_DOCKET_URL=redis://localhost:6379`.
+
+**Advantages:**
+- **Persistent**: Tasks survive server restarts
+- **Fast**: Single-digit millisecond task pickup latency
+- **Scalable**: Add workers to distribute load across processes or machines
+
+## Workers
+
+Every FastMCP server with task-enabled components automatically starts an **embedded worker**. You do not need to start a separate worker process for tasks to execute.
+
+To scale horizontally, add more workers using the CLI:
+
+```bash
+fastmcp tasks worker server.py
+```
+
+Each additional worker pulls tasks from the same queue, distributing load across processes. Configure worker concurrency via environment:
+
+```bash
+export FASTMCP_DOCKET_CONCURRENCY=20
+fastmcp tasks worker server.py
+```
+
+
+Additional workers only work with Redis/Valkey backends. The in-memory backend is single-process only.
+
+
+
+Task-enabled components must be defined at server startup to be registered with all workers. Components added dynamically after the server starts will not be available for background execution.
+
+
+## Progress Reporting
+
+The `Progress` dependency lets you report progress back to clients. Inject it as a parameter with a default value, and FastMCP will provide the active progress reporter.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.dependencies import Progress
+
+mcp = FastMCP("MyServer")
+
+@mcp.tool(task=True)
+async def process_files(files: list[str], progress: Progress = Progress()) -> str:
+ await progress.set_total(len(files))
+
+ for file in files:
+ await progress.set_message(f"Processing {file}")
+ # ... do work ...
+ await progress.increment()
+
+ return f"Processed {len(files)} files"
+```
+
+The progress API:
+- `await progress.set_total(n)` — Set the total number of steps
+- `await progress.increment(amount=1)` — Increment progress
+- `await progress.set_message(text)` — Update the status message
+
+Progress works in both immediate and background execution modes—you can use the same code regardless of how the client invokes your function.
+
+## Docket Dependencies
+
+FastMCP exposes Docket's full dependency injection system within your task-enabled functions. Beyond `Progress`, you can access the Docket instance, worker information, and use advanced features like retries and timeouts.
+
+```python
+from docket import Docket, Worker
+from fastmcp import FastMCP
+from fastmcp.dependencies import Progress, CurrentDocket, CurrentWorker
+
+mcp = FastMCP("MyServer")
+
+@mcp.tool(task=True)
+async def my_task(
+ progress: Progress = Progress(),
+ docket: Docket = CurrentDocket(),
+ worker: Worker = CurrentWorker(),
+) -> str:
+ # Schedule additional background work
+ await docket.add(another_task, arg1, arg2)
+
+ # Access worker metadata
+ worker_name = worker.name
+
+ return "Done"
+```
+
+With `CurrentDocket()`, you can schedule additional background tasks, chain work together, and coordinate complex workflows. See the [Docket documentation](https://chrisguidry.github.io/docket/) for the complete API, including retry policies, timeouts, and custom dependencies.
diff --git a/docs/v3/servers/telemetry.mdx b/docs/v3/servers/telemetry.mdx
new file mode 100644
index 000000000..aed7308db
--- /dev/null
+++ b/docs/v3/servers/telemetry.mdx
@@ -0,0 +1,345 @@
+---
+title: OpenTelemetry
+sidebarTitle: Telemetry
+description: Native OpenTelemetry instrumentation for distributed tracing.
+icon: chart-line
+tag: NEW
+---
+
+FastMCP includes native OpenTelemetry instrumentation for observability. Traces are automatically generated for tool, prompt, resource, and resource template operations, providing visibility into server behavior, request handling, and provider delegation chains.
+
+## How It Works
+
+FastMCP uses the OpenTelemetry API for instrumentation. This means:
+
+- **Zero configuration required** - Instrumentation is always active
+- **No overhead when unused** - Without an SDK, all operations are no-ops
+- **Bring your own SDK** - You control collection, export, and sampling
+- **Works with any OTEL backend** - Jaeger, Zipkin, Datadog, New Relic, etc.
+
+## Enabling Telemetry
+
+The easiest way to export traces is using `opentelemetry-instrument`, which configures the SDK automatically:
+
+```bash
+pip install opentelemetry-distro opentelemetry-exporter-otlp
+opentelemetry-bootstrap -a install
+```
+
+Then run your server with tracing enabled:
+
+```bash
+opentelemetry-instrument \
+ --service_name my-fastmcp-server \
+ --exporter_otlp_endpoint http://localhost:4317 \
+ fastmcp run server.py
+```
+
+Or configure via environment variables:
+
+```bash
+export OTEL_SERVICE_NAME=my-fastmcp-server
+export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
+
+opentelemetry-instrument fastmcp run server.py
+```
+
+This works with any OTLP-compatible backend (Jaeger, Zipkin, Grafana Tempo, Datadog, etc.) and requires no changes to your FastMCP code.
+
+
+ Learn more about the OpenTelemetry Python SDK, auto-instrumentation, and available exporters.
+
+
+## Tracing
+
+FastMCP creates spans for all MCP operations, providing end-to-end visibility into request handling.
+
+### Server Spans
+
+The server creates spans for each operation using [MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/):
+
+| Span Name | Description |
+|-----------|-------------|
+| `tools/call {name}` | Tool execution (e.g., `tools/call get_weather`) |
+| `resources/read` | Resource read (URI in `mcp.resource.uri` attribute, not span name) |
+| `prompts/get {name}` | Prompt render (e.g., `prompts/get greeting`) |
+
+For mounted servers, an additional `delegate {name}` span shows the delegation to the child server.
+
+### Client Spans
+
+The FastMCP client creates spans for outgoing requests with the same naming pattern (`tools/call {name}`, `resources/read`, `prompts/get {name}`).
+
+### Span Hierarchy
+
+Spans form a hierarchy showing the request flow. For mounted servers:
+
+```
+tools/call weather_forecast (CLIENT)
+ └── tools/call weather_forecast (SERVER, provider=FastMCPProvider)
+ └── delegate get_weather (INTERNAL)
+ └── tools/call get_weather (SERVER, provider=LocalProvider)
+```
+
+For proxy providers connecting to remote servers:
+
+```
+tools/call remote_search (CLIENT)
+ └── tools/call remote_search (SERVER, provider=ProxyProvider)
+ └── [remote server spans via trace context propagation]
+```
+
+## Programmatic Configuration
+
+For more control, configure the SDK in your Python code before importing FastMCP:
+
+```python
+from opentelemetry import trace
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace.export import BatchSpanProcessor
+from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
+
+# Configure the SDK with OTLP exporter
+provider = TracerProvider()
+processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://localhost:4317"))
+provider.add_span_processor(processor)
+trace.set_tracer_provider(provider)
+
+# Now import and use FastMCP - traces will be exported automatically
+from fastmcp import FastMCP
+
+mcp = FastMCP("my-server")
+
+@mcp.tool()
+def greet(name: str) -> str:
+ return f"Hello, {name}!"
+```
+
+
+The SDK must be configured **before** importing FastMCP to ensure the tracer provider is set when FastMCP initializes.
+
+
+### Local Development
+
+For quick local trace visualization, [otel-desktop-viewer](https://github.com/CtrlSpice/otel-desktop-viewer) is a lightweight single-binary tool:
+
+```bash
+# macOS
+brew install nico-barbas/brew/otel-desktop-viewer
+
+# Or download from GitHub releases
+```
+
+Run it alongside your server:
+
+```bash
+# Terminal 1: Start the viewer (UI at http://localhost:8000, OTLP on :4317)
+otel-desktop-viewer
+
+# Terminal 2: Run your server with tracing
+opentelemetry-instrument fastmcp run server.py
+```
+
+For more features, use [Jaeger](https://www.jaegertracing.io/):
+
+```bash
+docker run -d --name jaeger \
+ -p 16686:16686 \
+ -p 4317:4317 \
+ jaegertracing/all-in-one:latest
+```
+
+Then view traces at http://localhost:16686
+
+## Custom Spans
+
+You can add your own spans using the FastMCP tracer:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.telemetry import get_tracer
+
+mcp = FastMCP("custom-spans")
+
+@mcp.tool()
+async def complex_operation(input: str) -> str:
+ tracer = get_tracer()
+
+ with tracer.start_as_current_span("parse_input") as span:
+ span.set_attribute("input.length", len(input))
+ parsed = parse(input)
+
+ with tracer.start_as_current_span("process_data") as span:
+ span.set_attribute("data.count", len(parsed))
+ result = process(parsed)
+
+ return result
+```
+
+### Where custom spans help most
+
+Custom spans are most useful around work that is expensive or hard to debug:
+
+- External calls such as databases, vector stores, HTTP APIs, or queue operations
+- Multi-step tool logic where one stage dominates latency
+- Prompt or resource generation that fans out to other systems
+- Sampling calls made from inside a tool via `ctx.sample(...)`
+
+Avoid wrapping every small helper function or simple in-memory transformation. That usually adds noise without making traces easier to interpret.
+
+### Recommended naming and attributes
+
+- Use `{tool_name}.{operation}` or `{resource_name}.{operation}` for child spans such as `search.fetch`, `search.rank`, or `docs.render`
+- Add attributes that explain workload shape, such as counts, sizes, cache hits, or IDs
+- Do not record secrets, prompts with sensitive user data, or raw tokens as span attributes
+- Let exceptions propagate unless you have a specific recovery path; FastMCP's server spans already mark failures and record exceptions
+
+### Instrumenting tools, prompts, and resources
+
+```python
+from fastmcp import FastMCP
+from fastmcp.telemetry import get_tracer
+
+mcp = FastMCP("my-server")
+
+@mcp.tool
+async def search(query: str) -> str:
+ tracer = get_tracer()
+
+ with tracer.start_as_current_span("search.fetch") as span:
+ span.set_attribute("search.query_length", len(query))
+ results = await fetch_results(query)
+ span.set_attribute("search.result_count", len(results))
+
+ with tracer.start_as_current_span("search.rank"):
+ ranked = rank_results(results)
+
+ return format_results(ranked)
+
+@mcp.prompt
+async def summarize_prompt(topic: str) -> str:
+ tracer = get_tracer()
+ with tracer.start_as_current_span("summarize_prompt.render") as span:
+ span.set_attribute("prompt.topic_length", len(topic))
+ return f"Summarize the latest updates about {topic}."
+
+@mcp.resource("docs://{slug}")
+async def docs_resource(slug: str) -> str:
+ tracer = get_tracer()
+ with tracer.start_as_current_span("docs_resource.load") as span:
+ span.set_attribute("docs.slug", slug)
+ return await load_doc(slug)
+```
+
+### Sampling calls inside tools
+
+If your tool uses `ctx.sample(...)`, keep the LLM work nested under the tool span so traces show both application logic and model latency together.
+
+For providers with their own OTEL integrations, prefer enabling that instrumentation rather than manually creating a span around every model call. For example, if you use Google GenAI, `logfire.instrument_google_genai()` will emit child spans with token and request metadata under the active FastMCP tool span.
+
+### Exporter choices
+
+- For local debugging, `ConsoleSpanExporter` or `otel-desktop-viewer` gives quick feedback with minimal setup
+- For shared environments, use OTLP exporters to backends like Logfire, Jaeger, Tempo, Datadog, or New Relic
+- If traces are too noisy, tune sampling in your OpenTelemetry SDK instead of removing FastMCP instrumentation
+
+## Error Handling
+
+When errors occur, spans are automatically marked with error status and the exception is recorded:
+
+```python
+@mcp.tool()
+def risky_operation() -> str:
+ raise ValueError("Something went wrong")
+
+# The span will have:
+# - status = ERROR with exception message as description
+# - error.type = "tool_error" (or exception class name for non-tool errors)
+# - exception event with stack trace
+```
+
+## Attributes Reference
+
+
+**Migrating from v3.1 or earlier:** The `rpc.system`, `rpc.service`, and `rpc.method` span attributes were removed in favor of the [MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/) listed below. If you have dashboards or alerts keyed on those `rpc.*` attributes, update them to use `mcp.method.name` and the `fastmcp.*` attributes instead.
+
+
+### MCP Semantic Conventions
+
+FastMCP implements the [OpenTelemetry MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/):
+
+| Attribute | Description |
+|-----------|-------------|
+| `mcp.method.name` | The MCP method being called (`tools/call`, `resources/read`, `prompts/get`) |
+| `mcp.session.id` | Session identifier for the MCP connection |
+| `mcp.resource.uri` | The resource URI (for resource operations) |
+| `gen_ai.tool.name` | Tool name (on `tools/call` spans) |
+| `gen_ai.prompt.name` | Prompt name (on `prompts/get` spans) |
+| `error.type` | Error classification (`tool_error` for ToolError, otherwise exception class name) |
+
+### Auth Attributes
+
+Standard [identity attributes](https://opentelemetry.io/docs/specs/semconv/attributes-registry/enduser/):
+
+| Attribute | Description |
+|-----------|-------------|
+| `enduser.id` | Client ID from access token (when authenticated) |
+| `enduser.scope` | Space-separated OAuth scopes (when authenticated) |
+
+### FastMCP Custom Attributes
+
+All custom attributes use the `fastmcp.` prefix for features unique to FastMCP:
+
+| Attribute | Description |
+|-----------|-------------|
+| `fastmcp.server.name` | Server name |
+| `fastmcp.component.type` | `tool`, `resource`, `prompt`, or `resource_template` |
+| `fastmcp.component.key` | Full component identifier (e.g., `tool:greet`) |
+| `fastmcp.provider.type` | Provider class (`LocalProvider`, `FastMCPProvider`, `ProxyProvider`) |
+
+Provider-specific attributes for delegation context:
+
+| Attribute | Description |
+|-----------|-------------|
+| `fastmcp.delegate.original_name` | Original tool/prompt name before namespacing |
+| `fastmcp.delegate.original_uri` | Original resource URI before namespacing |
+| `fastmcp.proxy.backend_name` | Remote server tool/prompt name |
+| `fastmcp.proxy.backend_uri` | Remote server resource URI |
+
+## Testing with Telemetry
+
+For testing, use the in-memory exporter:
+
+```python
+import pytest
+from collections.abc import Generator
+from opentelemetry import trace
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace.export import SimpleSpanProcessor
+from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
+
+from fastmcp import FastMCP
+
+@pytest.fixture
+def trace_exporter() -> Generator[InMemorySpanExporter, None, None]:
+ exporter = InMemorySpanExporter()
+ provider = TracerProvider()
+ provider.add_span_processor(SimpleSpanProcessor(exporter))
+ original_provider = trace.get_tracer_provider()
+ trace.set_tracer_provider(provider)
+ yield exporter
+ exporter.clear()
+ trace.set_tracer_provider(original_provider)
+
+async def test_tool_creates_span(trace_exporter: InMemorySpanExporter) -> None:
+ mcp = FastMCP("test")
+
+ @mcp.tool()
+ def hello() -> str:
+ return "world"
+
+ await mcp.call_tool("hello", {})
+
+ spans = trace_exporter.get_finished_spans()
+ assert any(s.name == "tools/call hello" for s in spans)
+```
diff --git a/docs/v3/servers/testing.mdx b/docs/v3/servers/testing.mdx
new file mode 100644
index 000000000..7bd8600c5
--- /dev/null
+++ b/docs/v3/servers/testing.mdx
@@ -0,0 +1,104 @@
+---
+title: Testing your FastMCP Server
+sidebarTitle: Testing
+description: How to test your FastMCP server.
+icon: vial
+---
+
+The best way to ensure a reliable and maintainable FastMCP Server is to test it! The FastMCP Client combined with Pytest provides a simple and powerful way to test your FastMCP servers.
+
+## Prerequisites
+
+Testing FastMCP servers requires `pytest-asyncio` to handle async test functions and fixtures. Install it as a development dependency:
+
+```bash
+pip install pytest-asyncio
+```
+
+We recommend configuring pytest to automatically handle async tests by setting the asyncio mode to `auto` in your `pyproject.toml`:
+
+```toml
+[tool.pytest.ini_options]
+asyncio_mode = "auto"
+```
+
+This eliminates the need to decorate every async test with `@pytest.mark.asyncio`.
+
+## Testing with Pytest Fixtures
+
+Using Pytest Fixtures, you can wrap your FastMCP Server in a Client instance that makes interacting with your server fast and easy. This is especially useful when building your own MCP Servers and enables a tight development loop by allowing you to avoid using a separate tool like MCP Inspector during development:
+
+```python
+import pytest
+from fastmcp.client import Client
+from fastmcp.client.transports import FastMCPTransport
+
+from my_project.main import mcp
+
+@pytest.fixture
+async def main_mcp_client():
+ async with Client(transport=mcp) as mcp_client:
+ yield mcp_client
+
+async def test_list_tools(main_mcp_client: Client[FastMCPTransport]):
+ list_tools = await main_mcp_client.list_tools()
+
+ assert len(list_tools) == 5
+```
+
+We recommend the [inline-snapshot library](https://github.com/15r10nk/inline-snapshot) for asserting complex data structures coming from your MCP Server. This library allows you to write tests that are easy to read and understand, and are also easy to update when the data structure changes.
+
+```python
+from inline_snapshot import snapshot
+
+async def test_list_tools(main_mcp_client: Client[FastMCPTransport]):
+ list_tools = await main_mcp_client.list_tools()
+
+ assert list_tools == snapshot()
+```
+
+Simply run `pytest --inline-snapshot=fix,create` to fill in the `snapshot()` with actual data.
+
+
+For values that change you can leverage the [dirty-equals](https://github.com/samuelcolvin/dirty-equals) library to perform flexible equality assertions on dynamic or non-deterministic values.
+
+
+Using the pytest `parametrize` decorator, you can easily test your tools with a wide variety of inputs.
+
+```python
+import pytest
+from my_project.main import mcp
+
+from fastmcp.client import Client
+from fastmcp.client.transports import FastMCPTransport
+@pytest.fixture
+async def main_mcp_client():
+ async with Client(mcp) as client:
+ yield client
+
+
+@pytest.mark.parametrize(
+ "first_number, second_number, expected",
+ [
+ (1, 2, 3),
+ (2, 3, 5),
+ (3, 4, 7),
+ ],
+)
+async def test_add(
+ first_number: int,
+ second_number: int,
+ expected: int,
+ main_mcp_client: Client[FastMCPTransport],
+):
+ result = await main_mcp_client.call_tool(
+ name="add", arguments={"x": first_number, "y": second_number}
+ )
+ assert result.data is not None
+ assert isinstance(result.data, int)
+ assert result.data == expected
+```
+
+
+The [FastMCP Repository contains thousands of tests](https://github.com/PrefectHQ/fastmcp/tree/main/tests) for the FastMCP Client and Server. Everything from connecting to remote MCP servers, to testing tools, resources, and prompts is covered, take a look for inspiration!
+
\ No newline at end of file
diff --git a/docs/v3/servers/tool-fingerprinting.mdx b/docs/v3/servers/tool-fingerprinting.mdx
new file mode 100644
index 000000000..b8c06c04e
--- /dev/null
+++ b/docs/v3/servers/tool-fingerprinting.mdx
@@ -0,0 +1,156 @@
+---
+title: Tool Fingerprinting
+sidebarTitle: Tool Fingerprinting
+description: Build stable fingerprints for tool identity and schema change detection
+icon: fingerprint
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx";
+
+
+
+Downstream systems like routers, gateways, and audit loggers often need to detect whether a tool's schema changed between deployments. Rather than each system inventing its own JSON normalization and hashing logic, you can build stable fingerprints from FastMCP's existing API surface.
+
+FastMCP does not define a single "contract hash" because the inclusion policy is necessarily application-specific: some systems care only about the input schema, others include the description, metadata, tags, or version. Instead, this recipe shows how to assemble a fingerprint payload from the parts you care about, then hash it deterministically.
+
+## The Recipe
+
+The two key building blocks are:
+
+- **`tool.key`** — FastMCP's canonical component identity, encoding type, name, and version (e.g. `tool:greet@1.0` or `tool:greet@`)
+- **`tool.to_mcp_tool()`** — the protocol-facing tool object that MCP clients see, including the input schema
+
+Combine them into a payload, serialize deterministically, and hash:
+
+```python
+import hashlib
+import json
+
+from fastmcp import FastMCP
+
+mcp = FastMCP("demo")
+
+
+@mcp.tool()
+def greet(name: str) -> str:
+ """Say hello."""
+ return f"Hello {name}"
+
+
+async def fingerprint_tool(server: FastMCP, tool_name: str) -> str:
+ tool = await server.get_tool(tool_name)
+ if tool is None:
+ raise ValueError(f"Tool {tool_name!r} not found")
+
+ mcp_tool = tool.to_mcp_tool()
+ dumped = mcp_tool.model_dump(mode="json", by_alias=True, exclude_none=True)
+
+ payload = {
+ "key": tool.key,
+ "inputSchema": dumped["inputSchema"],
+ }
+
+ canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
+```
+
+The fingerprint is stable across process restarts as long as the tool's name, version, and input schema remain the same.
+
+## Why `tool.key`?
+
+`tool.key` is FastMCP's canonical component identity. It encodes the component type, identifier, and version into a single string:
+
+```
+tool:greet@1.0 # versioned tool
+tool:greet@ # unversioned tool
+```
+
+Using `key` rather than just the tool name ensures that two versions of the same tool produce distinct fingerprints, and that a tool and a resource with the same name cannot collide.
+
+## Why `to_mcp_tool()`?
+
+`to_mcp_tool()` returns the protocol-facing representation — the shape that MCP clients actually receive. This matters because routers and gateways typically operate on the protocol layer, not FastMCP internals. The `model_dump(mode="json", by_alias=True, exclude_none=True)` call produces a clean, serializable dictionary using the MCP protocol field names.
+
+## Customizing the Payload
+
+You own the inclusion policy. Add or remove fields depending on what constitutes a "contract" in your system:
+
+```python
+async def custom_fingerprint(server: FastMCP, tool_name: str) -> str:
+ tool = await server.get_tool(tool_name)
+ if tool is None:
+ raise ValueError(f"Tool {tool_name!r} not found")
+
+ mcp_tool = tool.to_mcp_tool()
+ dumped = mcp_tool.model_dump(mode="json", by_alias=True, exclude_none=True)
+
+ # Include description to detect documentation drift
+ payload = {
+ "key": tool.key,
+ "inputSchema": dumped["inputSchema"],
+ "description": dumped.get("description"),
+ }
+
+ canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
+ return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
+```
+
+Common variations:
+
+| Field | When to include |
+| -------------- | -------------------------------------------------------------------------- |
+| `inputSchema` | Always — this is the core contract |
+| `description` | When documentation drift matters (e.g. LLM routing decisions depend on it) |
+| `outputSchema` | When downstream consumers validate response shapes |
+| `annotations` | When behavioral hints (read-only, destructive) affect routing |
+| `_meta` | When custom metadata drives policy decisions |
+
+## Detecting Schema Drift in CI
+
+Store fingerprints as artifacts and compare between deployments:
+
+```python
+import json
+import hashlib
+from pathlib import Path
+
+from fastmcp import FastMCP
+
+
+async def generate_manifest(server: FastMCP) -> dict[str, str]:
+ """Generate a fingerprint manifest for all tools."""
+ manifest = {}
+
+ for tool in await server.list_tools():
+ mcp_tool = tool.to_mcp_tool()
+ dumped = mcp_tool.model_dump(mode="json", by_alias=True, exclude_none=True)
+
+ payload = {
+ "key": tool.key,
+ "inputSchema": dumped["inputSchema"],
+ }
+
+ canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"))
+ manifest[tool.key] = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
+
+ return manifest
+
+
+async def check_drift(server: FastMCP, baseline_path: Path) -> list[str]:
+ """Compare current fingerprints against a stored baseline."""
+ current = await generate_manifest(server)
+ baseline = json.loads(baseline_path.read_text())
+
+ changed = []
+ for key, fingerprint in current.items():
+ if baseline.get(key) != fingerprint:
+ changed.append(key)
+
+ for key in baseline:
+ if key not in current:
+ changed.append(key)
+
+ return changed
+```
+
+Run `generate_manifest` in CI after each build and compare against the previous run. Any differences indicate a schema change that downstream consumers should be aware of.
diff --git a/docs/v3/servers/tools.mdx b/docs/v3/servers/tools.mdx
new file mode 100644
index 000000000..862066bc7
--- /dev/null
+++ b/docs/v3/servers/tools.mdx
@@ -0,0 +1,1143 @@
+---
+title: Tools
+sidebarTitle: Tools
+description: Expose functions as executable capabilities for your MCP client.
+icon: wrench
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+Tools are the core building blocks that allow your LLM to interact with external systems, execute code, and access data that isn't in its training data. In FastMCP, tools are Python functions exposed to LLMs through the MCP protocol.
+
+Tools in FastMCP transform regular Python functions into capabilities that LLMs can invoke during conversations. When an LLM decides to use a tool:
+
+1. It sends a request with parameters based on the tool's schema.
+2. FastMCP validates these parameters against your function's signature.
+3. Your function executes with the validated inputs.
+4. The result is returned to the LLM, which can use it in its response.
+
+This allows LLMs to perform tasks like querying databases, calling APIs, making calculations, or accessing files—extending their capabilities beyond what's in their training data.
+
+
+## The `@tool` Decorator
+
+Creating a tool is as simple as decorating a Python function with `@mcp.tool`:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="CalculatorServer")
+
+@mcp.tool
+def add(a: int, b: int) -> int:
+ """Adds two integer numbers together."""
+ return a + b
+```
+
+When this tool is registered, FastMCP automatically:
+- Uses the function name (`add`) as the tool name.
+- Parses the function's docstring for the tool description and, if present, per-parameter descriptions (see [Docstring Descriptions](#docstring-descriptions)).
+- Generates an input schema based on the function's parameters and type annotations.
+- Handles parameter validation and error reporting.
+
+The way you define your Python function dictates how the tool appears and behaves for the LLM client.
+
+
+Functions with `*args` or `**kwargs` are not supported as tools. This restriction exists because FastMCP needs to generate a complete parameter schema for the MCP protocol, which isn't possible with variable argument lists.
+
+
+### Decorator Arguments
+
+While FastMCP infers the name and description from your function, you can override these and add additional metadata using arguments to the `@mcp.tool` decorator:
+
+```python
+@mcp.tool(
+ name="find_products", # Custom tool name for the LLM
+ description="Search the product catalog with optional category filtering.", # Custom description
+ tags={"catalog", "search"}, # Optional tags for organization/filtering
+ meta={"version": "1.2", "author": "product-team"} # Custom metadata
+)
+def search_products_implementation(query: str, category: str | None = None) -> list[dict]:
+ """Internal function description (ignored if description is provided above)."""
+ # Implementation...
+ print(f"Searching for '{query}' in category '{category}'")
+ return [{"id": 2, "name": "Another Product"}]
+```
+
+
+
+ Sets the explicit tool name exposed via MCP. If not provided, uses the function name
+
+
+
+ Provides the description exposed via MCP. If set, the function's docstring is ignored for the tool description, though docstring-derived parameter descriptions still apply (see [Docstring Descriptions](#docstring-descriptions)).
+
+
+
+ A set of strings used to categorize the tool. These can be used by the server and, in some cases, by clients to filter or group available tools.
+
+
+
+ Deprecated in v3.0.0. Use `mcp.enable()` / `mcp.disable()` at the server level instead.
+ A boolean to enable or disable the tool. See [Component Visibility](#component-visibility) for the recommended approach.
+
+
+
+
+
+ Optional list of icon representations for this tool. See [Icons](/servers/icons) for detailed examples
+
+
+
+ An optional `ToolAnnotations` object or dictionary to add additional metadata about the tool.
+
+
+ A human-readable title for the tool.
+
+
+ If true, the tool does not modify its environment.
+
+
+ If true, the tool may perform destructive updates to its environment.
+
+
+ If true, calling the tool repeatedly with the same arguments will have no additional effect on the its environment.
+
+
+ If true, this tool may interact with an "open world" of external entities. If false, the tool's domain of interaction is closed.
+
+
+
+
+
+
+
+ Optional meta information about the tool. This data is passed through to the MCP client as the `meta` field of the client-side tool object and can be used for custom metadata, versioning, or other application-specific purposes.
+
+
+
+
+
+ Execution timeout in seconds. If the tool takes longer than this to complete, an MCP error is returned to the client. See [Timeouts](#timeouts) for details.
+
+
+
+
+
+ Optional version identifier for this tool. See [Versioning](/servers/versioning) for details.
+
+
+
+
+
+ Optional JSON schema for the tool's output. When provided, the tool must return structured output matching this schema. If not provided, FastMCP automatically generates a schema from the function's return type annotation. See [Output Schemas](#output-schemas) for details.
+
+
+
+ Applies to sync tool functions only. When `True` (default), sync functions are dispatched to a thread pool so they don't block the event loop. Set to `False` to run the function inline on the event loop thread — useful for libraries with thread affinity like Windows COM (`pywin32`, `uiautomation`, `comtypes`), `tkinter`, or certain GPU/driver bindings. Ignored for async functions, which always run on the event loop. See [Thread affinity](#thread-affinity) for details.
+
+
+
+### Using with Methods
+
+The `@mcp.tool` decorator registers tools immediately, which doesn't work with instance or class methods (you'd see `self` or `cls` as required parameters). For methods, use the standalone `@tool` decorator to attach metadata, then register the bound method:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.tools import tool
+
+class Calculator:
+ def __init__(self, multiplier: int):
+ self.multiplier = multiplier
+
+ @tool()
+ def multiply(self, x: int) -> int:
+ """Multiply x by the instance multiplier."""
+ return x * self.multiplier
+
+calc = Calculator(multiplier=3)
+mcp = FastMCP()
+mcp.add_tool(calc.multiply) # Registers with correct schema (only 'x', not 'self')
+```
+
+### Async Support
+
+FastMCP supports both asynchronous (`async def`) and synchronous (`def`) functions as tools. Synchronous tools automatically run in a threadpool to avoid blocking the event loop, so multiple tool calls can execute concurrently even if individual tools perform blocking operations.
+
+```python
+from fastmcp import FastMCP
+import time
+
+mcp = FastMCP()
+
+@mcp.tool
+def slow_tool(x: int) -> int:
+ """This sync function won't block other concurrent requests."""
+ time.sleep(2) # Runs in threadpool, not on the event loop
+ return x * 2
+```
+
+For I/O-bound operations like network requests or database queries, async tools are still preferred since they're more efficient than threadpool dispatch. Use sync tools when working with synchronous libraries or for simple operations where the threading overhead doesn't matter.
+
+### Thread affinity
+
+This section applies to sync tools only. Async tools already run on the event loop and are not affected.
+
+Some libraries bind state to the thread they're first used from and break when called from a different thread. The most common case is Windows COM — libraries like `uiautomation`, `comtypes`, and parts of `pywin32` require `CoInitialize` to have been called on the current thread, and worker-pool threads don't initialize COM by default. Similar constraints apply to `tkinter`, some GPU bindings (CUDA contexts), and certain hardware drivers.
+
+For these cases, pass `run_in_thread=False` so FastMCP invokes the sync function inline on the event loop thread instead of dispatching it to a worker:
+
+```python
+import uiautomation as auto
+
+@mcp.tool(run_in_thread=False)
+def list_windows() -> list[str]:
+ """List desktop windows via Windows UI Automation (COM)."""
+ desktop = auto.GetRootControl()
+ return [w.Name for w in desktop.GetChildren()[:5]]
+```
+
+The tradeoff is that the event loop is blocked for the duration of the call — other in-flight requests wait until the tool returns. Keep `run_in_thread=False` reserved for tools that genuinely need thread affinity, and prefer short-running calls in that path.
+
+Inline sync calls have no cancellation checkpoints, so `timeout` cannot interrupt them. Combining `timeout` with `run_in_thread=False` on a sync function is rejected at registration — drop one or the other.
+
+## Arguments
+
+By default, FastMCP converts Python functions into MCP tools by inspecting the function's signature and type annotations. This allows you to use standard Python type annotations for your tools. In general, the framework strives to "just work": idiomatic Python behaviors like parameter defaults and type annotations are automatically translated into MCP schemas. However, there are a number of ways to customize the behavior of your tools.
+
+
+FastMCP automatically dereferences `$ref` entries in tool schemas to ensure compatibility with MCP clients that don't fully support JSON Schema references (e.g., VS Code Copilot, Claude Desktop). This means complex Pydantic models with shared types are inlined in the schema rather than using `$defs` references.
+
+Dereferencing happens at serve-time via middleware, so your schemas are stored with `$ref` intact and only inlined when sent to clients. If you know your clients handle `$ref` correctly and prefer smaller schemas, you can opt out:
+
+```python
+mcp = FastMCP("my-server", dereference_schemas=False)
+```
+
+
+### Type Annotations
+
+MCP tools have typed arguments, and FastMCP uses type annotations to determine those types. Therefore, you should use standard Python type annotations for tool arguments:
+
+```python
+@mcp.tool
+def analyze_text(
+ text: str,
+ max_tokens: int = 100,
+ language: str | None = None
+) -> dict:
+ """Analyze the provided text."""
+ # Implementation...
+```
+
+FastMCP supports a wide range of type annotations, including all Pydantic types:
+
+| Type Annotation | Example | Description |
+| :---------------------- | :---------------------------- | :---------------------------------- |
+| Basic types | `int`, `float`, `str`, `bool` | Simple scalar values |
+| Binary data | `bytes` | Binary content (raw strings, not auto-decoded base64) |
+| Date and Time | `datetime`, `date`, `timedelta` | Date and time objects (ISO format strings) |
+| Collection types | `list[str]`, `dict[str, int]`, `set[int]` | Collections of items |
+| Optional types | `float \| None`, `Optional[float]`| Parameters that may be null/omitted |
+| Union types | `str \| int`, `Union[str, int]`| Parameters accepting multiple types |
+| Constrained types | `Literal["A", "B"]`, `Enum` | Parameters with specific allowed values |
+| Paths | `Path` | File system paths (auto-converted from strings) |
+| UUIDs | `UUID` | Universally unique identifiers (auto-converted from strings) |
+| Pydantic models | `UserData` | Complex structured data with validation |
+
+FastMCP supports all types that Pydantic supports as fields, including all Pydantic custom types. A few FastMCP-specific behaviors to note:
+
+**Binary Data**: `bytes` parameters accept raw strings without automatic base64 decoding. For base64 data, use `str` and decode manually with `base64.b64decode()`.
+
+**Enums**: Clients send enum values (`"red"`), not names (`"RED"`). Your function receives the Enum member (`Color.RED`).
+
+**Paths and UUIDs**: String inputs are automatically converted to `Path` and `UUID` objects.
+
+**Pydantic Models**: Must be provided as JSON objects (dicts), not stringified JSON. Even with flexible validation, `{"user": {"name": "Alice"}}` works, but `{"user": '{"name": "Alice"}'}` does not.
+
+### Optional Arguments
+
+FastMCP follows Python's standard function parameter conventions. Parameters without default values are required, while those with default values are optional.
+
+```python
+@mcp.tool
+def search_products(
+ query: str, # Required - no default value
+ max_results: int = 10, # Optional - has default value
+ sort_by: str = "relevance", # Optional - has default value
+ category: str | None = None # Optional - can be None
+) -> list[dict]:
+ """Search the product catalog."""
+ # Implementation...
+```
+
+In this example, the LLM must provide a `query` parameter, while `max_results`, `sort_by`, and `category` will use their default values if not explicitly provided.
+
+### Validation Modes
+
+
+
+By default, FastMCP uses Pydantic's flexible validation that coerces compatible inputs to match your type annotations. This improves compatibility with LLM clients that may send string representations of values (like `"10"` for an integer parameter).
+
+If you need stricter validation that rejects any type mismatches, you can enable strict input validation. Strict mode uses the MCP SDK's built-in JSON Schema validation to validate inputs against the exact schema before passing them to your function:
+
+```python
+# Enable strict validation for this server
+mcp = FastMCP("StrictServer", strict_input_validation=True)
+
+@mcp.tool
+def add_numbers(a: int, b: int) -> int:
+ """Add two numbers."""
+ return a + b
+
+# With strict_input_validation=True, sending {"a": "10", "b": "20"} will fail
+# With strict_input_validation=False (default), it will be coerced to integers
+```
+
+**Validation Behavior Comparison:**
+
+| Input Type | strict_input_validation=False (default) | strict_input_validation=True |
+| :--------- | :-------------------------------------- | :--------------------------- |
+| String integers (`"10"` for `int`) | ✅ Coerced to integer | ❌ Validation error |
+| String floats (`"3.14"` for `float`) | ✅ Coerced to float | ❌ Validation error |
+| String booleans (`"true"` for `bool`) | ✅ Coerced to boolean | ❌ Validation error |
+| Lists with string elements (`["1", "2"]` for `list[int]`) | ✅ Elements coerced | ❌ Validation error |
+| Pydantic model fields with type mismatches | ✅ Fields coerced | ❌ Validation error |
+| Invalid values (`"abc"` for `int`) | ❌ Validation error | ❌ Validation error |
+
+
+**Note on Pydantic Models:** Even with `strict_input_validation=False`, Pydantic model parameters must be provided as JSON objects (dicts), not as stringified JSON. For example, `{"user": {"name": "Alice"}}` works, but `{"user": '{"name": "Alice"}'}` does not.
+
+
+The default flexible validation mode is recommended for most use cases as it handles common LLM client behaviors gracefully while still providing strong type safety through Pydantic's validation.
+
+### Parameter Metadata
+
+You can provide additional metadata about parameters in several ways:
+
+#### Docstring Descriptions
+
+
+
+FastMCP parses your function's docstring to extract both the tool description and per-parameter descriptions. Google, NumPy, and Sphinx docstring styles are all supported — the parser tries each and uses whichever finds parameter descriptions:
+
+```python
+@mcp.tool
+def process_image(
+ image_url: str,
+ resize: bool = False,
+ width: int = 800,
+) -> dict:
+ """Process an image with optional resizing.
+
+ Args:
+ image_url: URL of the image to process.
+ resize: Whether to resize the image.
+ width: Target width in pixels.
+ """
+ # Implementation...
+```
+
+The free-form text above the `Args` section — whether a single line or multiple paragraphs — becomes the tool description, and each parameter's docstring entry becomes the description for that parameter in the generated schema. Sections like `Returns`, `Raises`, and `Example` are excluded from the description but otherwise ignored.
+
+If a parameter already has an explicit description — via `Annotated[x, "..."]` or `Field(description=...)` — that description takes precedence over the docstring. This makes it safe to adopt docstring-based descriptions incrementally: existing annotations keep working, and docstrings fill in the gaps.
+
+#### Simple String Descriptions
+
+
+
+For basic parameter descriptions, you can use a convenient shorthand with `Annotated`:
+
+```python
+from typing import Annotated
+
+@mcp.tool
+def process_image(
+ image_url: Annotated[str, "URL of the image to process"],
+ resize: Annotated[bool, "Whether to resize the image"] = False,
+ width: Annotated[int, "Target width in pixels"] = 800,
+ format: Annotated[str, "Output image format"] = "jpeg"
+) -> dict:
+ """Process an image with optional resizing."""
+ # Implementation...
+```
+
+This shorthand syntax is equivalent to using `Field(description=...)` but more concise for simple descriptions.
+
+
+This shorthand syntax is only applied to `Annotated` types with a single string description.
+
+
+#### Advanced Metadata with Field
+
+For validation constraints and advanced metadata, use Pydantic's `Field` class with `Annotated`:
+
+```python
+from typing import Annotated
+from pydantic import Field
+
+@mcp.tool
+def process_image(
+ image_url: Annotated[str, Field(description="URL of the image to process")],
+ resize: Annotated[bool, Field(description="Whether to resize the image")] = False,
+ width: Annotated[int, Field(description="Target width in pixels", ge=1, le=2000)] = 800,
+ format: Annotated[
+ Literal["jpeg", "png", "webp"],
+ Field(description="Output image format")
+ ] = "jpeg"
+) -> dict:
+ """Process an image with optional resizing."""
+ # Implementation...
+```
+
+
+You can also use the Field as a default value, though the Annotated approach is preferred:
+
+```python
+@mcp.tool
+def search_database(
+ query: str = Field(description="Search query string"),
+ limit: int = Field(10, description="Maximum number of results", ge=1, le=100)
+) -> list:
+ """Search the database with the provided query."""
+ # Implementation...
+```
+
+Field provides several validation and documentation features:
+- `description`: Human-readable explanation of the parameter (shown to LLMs)
+- `ge`/`gt`/`le`/`lt`: Greater/less than (or equal) constraints
+- `min_length`/`max_length`: String or collection length constraints
+- `pattern`: Regex pattern for string validation
+- `default`: Default value if parameter is omitted
+
+### Hiding Parameters from the LLM
+
+
+
+To inject values at runtime without exposing them to the LLM (such as `user_id`, credentials, or database connections), use dependency injection with `Depends()`. Parameters using `Depends()` are automatically excluded from the tool schema:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.dependencies import Depends
+
+mcp = FastMCP()
+
+def get_user_id() -> str:
+ return "user_123" # Injected at runtime
+
+@mcp.tool
+def get_user_details(user_id: str = Depends(get_user_id)) -> str:
+ # user_id is injected by the server, not provided by the LLM
+ return f"Details for {user_id}"
+```
+
+See [Custom Dependencies](/servers/context#custom-dependencies) for more details on dependency injection.
+
+## Return Values
+
+
+FastMCP tools can return data in two complementary formats: **traditional content blocks** (like text and images) and **structured outputs** (machine-readable JSON). When you add return type annotations, FastMCP automatically generates **output schemas** to validate the structured data and enables clients to deserialize results back to Python objects.
+
+Understanding how these three concepts work together:
+
+- **Return Values**: What your Python function returns (determines both content blocks and structured data)
+- **Structured Outputs**: JSON data sent alongside traditional content for machine processing
+- **Output Schemas**: JSON Schema declarations that describe and validate the structured output format
+
+The following sections explain each concept in detail.
+
+### Content Blocks
+
+FastMCP automatically converts tool return values into appropriate MCP content blocks:
+
+- **`str`**: Sent as `TextContent`
+- **`bytes`**: Base64 encoded and sent as `BlobResourceContents` (within an `EmbeddedResource`)
+- **`fastmcp.utilities.types.Image`**: Sent as `ImageContent`
+- **`fastmcp.utilities.types.Audio`**: Sent as `AudioContent`
+- **`fastmcp.utilities.types.File`**: Sent as base64-encoded `EmbeddedResource`
+- **MCP SDK content blocks**: Sent as-is
+- **A list of any of the above**: Converts each item according to the above rules
+- **`None`**: Results in an empty response
+
+#### Media Helper Classes
+
+FastMCP provides helper classes for returning images, audio, and files. When you return one of these classes, either directly or as part of a list, FastMCP automatically converts it to the appropriate MCP content block. For example, if you return a `fastmcp.utilities.types.Image` object, FastMCP will convert it to an MCP `ImageContent` block with the correct MIME type and base64 encoding.
+
+```python
+from fastmcp.utilities.types import Image, Audio, File
+
+@mcp.tool
+def get_chart() -> Image:
+ """Generate a chart image."""
+ return Image(path="chart.png")
+
+@mcp.tool
+def get_multiple_charts() -> list[Image]:
+ """Return multiple charts."""
+ return [Image(path="chart1.png"), Image(path="chart2.png")]
+```
+
+
+Helper classes are only automatically converted to MCP content blocks when returned **directly** or as part of a **list**. For more complex containers like dicts, you can manually convert them to MCP types:
+
+```python
+# ✅ Automatic conversion
+return Image(path="chart.png")
+return [Image(path="chart1.png"), "text content"]
+
+# ❌ Will not be automatically converted
+return {"image": Image(path="chart.png")}
+
+# ✅ Manual conversion for nested use
+return {"image": Image(path="chart.png").to_image_content()}
+```
+
+
+Each helper class accepts either `path=` or `data=` (mutually exclusive):
+- **`path`**: File path (string or Path object) - MIME type detected from extension
+- **`data`**: Raw bytes - requires `format=` parameter for MIME type
+- **`format`**: Optional format override (e.g., "png", "wav", "pdf")
+- **`name`**: Optional name for `File` when using `data=`
+- **`annotations`**: Optional MCP annotations for the content
+
+### Structured Output
+
+
+
+The 6/18/2025 MCP spec update [introduced](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content) structured content, which is a new way to return data from tools. Structured content is a JSON object that is sent alongside traditional content. FastMCP automatically creates structured outputs alongside traditional content when your tool returns data that has a JSON object representation. This provides machine-readable JSON data that clients can deserialize back to Python objects.
+
+**Automatic Structured Content Rules:**
+- **Object-like results** (`dict`, Pydantic models, dataclasses) → Always become structured content (even without output schema)
+- **Non-object results** (`int`, `str`, `list`) → Only become structured content if there's an output schema to validate/serialize them
+- **All results** → Always become traditional content blocks for backward compatibility
+
+
+This automatic behavior enables clients to receive machine-readable data alongside human-readable content without requiring explicit output schemas for object-like returns.
+
+
+#### Dictionaries and Objects
+
+When your tool returns a dictionary, dataclass, or Pydantic model, FastMCP automatically creates structured content from it. The structured content contains the actual object data, making it easy for clients to deserialize back to native objects.
+
+
+```python Tool Definition
+@mcp.tool
+def get_user_data(user_id: str) -> dict:
+ """Get user data."""
+ return {"name": "Alice", "age": 30, "active": True}
+```
+
+```json MCP Result
+{
+ "content": [
+ {
+ "type": "text",
+ "text": "{\n \"name\": \"Alice\",\n \"age\": 30,\n \"active\": true\n}"
+ }
+ ],
+ "structuredContent": {
+ "name": "Alice",
+ "age": 30,
+ "active": true
+ }
+}
+```
+
+
+#### Primitives and Collections
+
+When your tool returns a primitive type (int, str, bool) or a collection (list, set), FastMCP needs a return type annotation to generate structured content. The annotation tells FastMCP how to validate and serialize the result.
+
+Without a type annotation, the tool only produces `content`:
+
+
+```python Tool Definition
+@mcp.tool
+def calculate_sum(a: int, b: int):
+ """Calculate sum without return annotation."""
+ return a + b # Returns 8
+```
+
+```json MCP Result
+{
+ "content": [
+ {
+ "type": "text",
+ "text": "8"
+ }
+ ]
+}
+```
+
+
+When you add a return annotation, such as `-> int`, FastMCP generates `structuredContent` by wrapping the primitive value in a `{"result": ...}` object, since JSON schemas require object-type roots for structured output:
+
+
+```python Tool Definition
+@mcp.tool
+def calculate_sum(a: int, b: int) -> int:
+ """Calculate sum with return annotation."""
+ return a + b # Returns 8
+```
+
+```json MCP Result
+{
+ "content": [
+ {
+ "type": "text",
+ "text": "8"
+ }
+ ],
+ "structuredContent": {
+ "result": 8
+ }
+}
+```
+
+
+#### Typed Models
+
+Return type annotations work with any type that can be converted to a JSON schema. Dataclasses and Pydantic models are particularly useful because FastMCP extracts their field definitions to create detailed schemas.
+
+
+```python Tool Definition
+from dataclasses import dataclass
+from fastmcp import FastMCP
+
+mcp = FastMCP()
+
+@dataclass
+class Person:
+ name: str
+ age: int
+ email: str
+
+@mcp.tool
+def get_user_profile(user_id: str) -> Person:
+ """Get a user's profile information."""
+ return Person(
+ name="Alice",
+ age=30,
+ email="alice@example.com",
+ )
+```
+
+```json Generated Output Schema
+{
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "age": {"title": "Age", "type": "integer"},
+ "email": {"title": "Email", "type": "string"}
+ },
+ "required": ["name", "age", "email"],
+ "title": "Person",
+ "type": "object"
+}
+```
+
+```json MCP Result
+{
+ "content": [
+ {
+ "type": "text",
+ "text": "{\"name\": \"Alice\", \"age\": 30, \"email\": \"alice@example.com\"}"
+ }
+ ],
+ "structuredContent": {
+ "name": "Alice",
+ "age": 30,
+ "email": "alice@example.com"
+ }
+}
+```
+
+
+The `Person` dataclass becomes an output schema (second tab) that describes the expected format. When executed, clients receive the result (third tab) with both `content` and `structuredContent` fields.
+
+### Output Schemas
+
+
+
+The 6/18/2025 MCP spec update [introduced](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema) output schemas, which are a new way to describe the expected output format of a tool. When an output schema is provided, the tool *must* return structured output that matches the schema.
+
+When you add return type annotations to your functions, FastMCP automatically generates JSON schemas that describe the expected output format. These schemas help MCP clients understand and validate the structured data they receive.
+
+#### Primitive Type Wrapping
+
+For primitive return types (like `int`, `str`, `bool`), FastMCP automatically wraps the result under a `"result"` key to create valid structured output:
+
+
+```python Primitive Return Type
+@mcp.tool
+def calculate_sum(a: int, b: int) -> int:
+ """Add two numbers together."""
+ return a + b
+```
+
+```json Generated Schema (Wrapped)
+{
+ "type": "object",
+ "properties": {
+ "result": {"type": "integer"}
+ },
+ "x-fastmcp-wrap-result": true
+}
+```
+
+```json Structured Output
+{
+ "result": 8
+}
+```
+
+
+#### Manual Schema Control
+
+You can override the automatically generated schema by providing a custom `output_schema`:
+
+```python
+@mcp.tool(output_schema={
+ "type": "object",
+ "properties": {
+ "data": {"type": "string"},
+ "metadata": {"type": "object"}
+ }
+})
+def custom_schema_tool() -> dict:
+ """Tool with custom output schema."""
+ return {"data": "Hello", "metadata": {"version": "1.0"}}
+```
+
+Schema generation works for most common types including basic types, collections, union types, Pydantic models, TypedDict structures, and dataclasses.
+
+
+**Important Constraints**:
+- Output schemas must be object types (`"type": "object"`)
+- If you provide an output schema, your tool **must** return structured output that matches it
+- However, you can provide structured output without an output schema (using `ToolResult`)
+
+
+### ToolResult and Metadata
+
+For complete control over tool responses, return a `ToolResult` object. This gives you explicit control over all aspects of the tool's output: traditional content, structured data, and metadata.
+
+```python
+from fastmcp.tools.tool import ToolResult
+from mcp.types import TextContent
+
+@mcp.tool
+def advanced_tool() -> ToolResult:
+ """Tool with full control over output."""
+ return ToolResult(
+ content=[TextContent(type="text", text="Human-readable summary")],
+ structured_content={"data": "value", "count": 42},
+ meta={"execution_time_ms": 145}
+ )
+```
+
+`ToolResult` accepts three fields:
+
+**`content`** - The traditional MCP content blocks that clients display to users. Can be a string (automatically converted to `TextContent`), a list of MCP content blocks, or any serializable value (converted to JSON string). At least one of `content` or `structured_content` must be provided.
+
+```python
+# Simple string
+ToolResult(content="Hello, world!")
+
+# List of content blocks
+ToolResult(content=[
+ TextContent(type="text", text="Result: 42"),
+ ImageContent(type="image", data="base64...", mimeType="image/png")
+])
+```
+
+**`structured_content`** - A dictionary containing structured data that matches your tool's output schema. This enables clients to programmatically process the results. If you provide `structured_content`, it must be a dictionary or `None`. If only `structured_content` is provided, it will also be used as `content` (converted to JSON string).
+
+```python
+ToolResult(
+ content="Found 3 users",
+ structured_content={"users": [{"name": "Alice"}, {"name": "Bob"}]}
+)
+```
+
+**`meta`**
+
+Runtime metadata about the tool execution. Use this for performance metrics, debugging information, or any client-specific data that doesn't belong in the content or structured output.
+
+```python
+ToolResult(
+ content="Analysis complete",
+ structured_content={"result": "positive"},
+ meta={
+ "execution_time_ms": 145,
+ "model_version": "2.1",
+ "confidence": 0.95
+ }
+)
+```
+
+
+The `meta` field in `ToolResult` is for runtime metadata about tool execution (e.g., execution time, performance metrics). This is separate from the `meta` parameter in `@mcp.tool(meta={...})`, which provides static metadata about the tool definition itself.
+
+
+When returning `ToolResult`, you have full control - FastMCP won't automatically wrap or transform your data. `ToolResult` can be returned with or without an output schema.
+
+### Custom Serialization
+
+When you need custom serialization (like YAML, Markdown tables, or specialized formats), return `ToolResult` with your serialized content. This makes the serialization explicit and visible in your tool's code:
+
+```python
+import yaml
+from fastmcp import FastMCP
+from fastmcp.tools.tool import ToolResult
+
+mcp = FastMCP("MyServer")
+
+@mcp.tool
+def get_config() -> ToolResult:
+ """Returns configuration as YAML."""
+ data = {"api_key": "abc123", "debug": True, "rate_limit": 100}
+ return ToolResult(
+ content=yaml.dump(data, sort_keys=False),
+ structured_content=data
+ )
+```
+
+
+For reusable serialization across multiple tools, create a wrapper decorator that returns `ToolResult`. This lets you compose serializers with other behaviors (logging, validation, caching) and keeps the serialization visible at the tool definition. See [examples/custom_tool_serializer_decorator.py](https://github.com/PrefectHQ/fastmcp/blob/main/examples/custom_tool_serializer_decorator.py) for a complete implementation.
+
+
+## Error Handling
+
+
+
+If your tool encounters an error, you can raise a standard Python exception (`ValueError`, `TypeError`, `FileNotFoundError`, custom exceptions, etc.) or a FastMCP `ToolError`.
+
+By default, all exceptions (including their details) are logged and converted into an MCP error response to be sent back to the client LLM. This helps the LLM understand failures and react appropriately.
+
+If you want to mask internal error details for security reasons, you can:
+
+1. Use the `mask_error_details=True` parameter when creating your `FastMCP` instance:
+```python
+mcp = FastMCP(name="SecureServer", mask_error_details=True)
+```
+
+2. Or use `ToolError` to explicitly control what error information is sent to clients:
+```python
+from fastmcp import FastMCP
+from fastmcp.exceptions import ToolError
+
+@mcp.tool
+def divide(a: float, b: float) -> float:
+ """Divide a by b."""
+
+ if b == 0:
+ # Error messages from ToolError are always sent to clients,
+ # regardless of mask_error_details setting
+ raise ToolError("Division by zero is not allowed.")
+
+ # If mask_error_details=True, this message would be masked
+ if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
+ raise TypeError("Both arguments must be numbers.")
+
+ return a / b
+```
+
+When `mask_error_details=True`, only error messages from `ToolError` will include details, other exceptions will be converted to a generic message.
+
+## Timeouts
+
+
+
+Tools can specify a `timeout` parameter to limit how long execution can take. When the timeout is exceeded, the client receives an MCP error and the tool stops processing. This protects your server from unexpectedly slow operations that could block resources or leave clients waiting indefinitely.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP()
+
+@mcp.tool(timeout=30.0)
+async def fetch_data(url: str) -> dict:
+ """Fetch data with a 30-second timeout."""
+ # If this takes longer than 30 seconds,
+ # the client receives an MCP error
+ ...
+```
+
+Timeouts are specified in seconds as a float. When a tool exceeds its timeout, FastMCP returns an MCP error with code `-32000` and a message indicating which tool timed out and how long it ran. Both sync and async tools support timeouts—sync functions run in thread pools, so the timeout applies to the entire operation regardless of execution model.
+
+
+Tools must explicitly opt-in to timeouts. There is no server-level default timeout setting.
+
+
+### Timeouts vs Background Tasks
+
+Timeouts apply to **foreground execution**—when a tool runs directly in response to a client request. They protect your server from tools that unexpectedly hang due to network issues, resource contention, or other transient problems.
+
+
+The `timeout` parameter does **not** apply to background tasks. When a tool runs as a background task (`task=True`), execution happens in a Docket worker where the FastMCP timeout is not enforced.
+
+For task timeouts, use Docket's `Timeout` dependency directly in your function signature:
+
+```python
+from datetime import timedelta
+from docket import Timeout
+
+@mcp.tool(task=True)
+async def long_running_task(
+ data: str,
+ timeout: Timeout = Timeout(timedelta(minutes=10))
+) -> str:
+ """Task with a 10-minute timeout enforced by Docket."""
+ ...
+```
+
+See the [Docket documentation](https://chrisguidry.github.io/docket/dependencies/#task-timeouts) for more on task timeouts and retries.
+
+
+When a tool times out, FastMCP logs a warning suggesting task mode. For operations you know will be long-running, use `task=True` instead—background tasks offload work to distributed workers and let clients poll for progress.
+
+## Component Visibility
+
+
+
+You can control which tools are enabled for clients using server-level enabled control. Disabled tools don't appear in `list_tools` and can't be called.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("MyServer")
+
+@mcp.tool(tags={"admin"})
+def admin_action() -> str:
+ """Admin-only action."""
+ return "Done"
+
+@mcp.tool(tags={"public"})
+def public_action() -> str:
+ """Public action."""
+ return "Done"
+
+# Disable specific tools by key
+mcp.disable(keys={"tool:admin_action"})
+
+# Disable tools by tag
+mcp.disable(tags={"admin"})
+
+# Or use allowlist mode - only enable tools with specific tags
+mcp.enable(tags={"public"}, only=True)
+```
+
+See [Visibility](/servers/visibility) for the complete visibility control API including key formats, tag-based filtering, and provider-level control.
+
+## MCP Annotations
+
+
+
+FastMCP allows you to add specialized metadata to your tools through annotations. These annotations communicate how tools behave to client applications without consuming token context in LLM prompts.
+
+Annotations serve several purposes in client applications:
+- Adding user-friendly titles for display purposes
+- Indicating whether tools modify data or systems
+- Describing the safety profile of tools (destructive vs. non-destructive)
+- Signaling if tools interact with external systems
+
+You can add annotations to a tool using the `annotations` parameter in the `@mcp.tool` decorator. FastMCP accepts either a plain dict or `ToolAnnotations`; the examples below use `ToolAnnotations` for consistency and stronger editor/type support.
+
+```python
+from mcp.types import ToolAnnotations
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ title="Calculate Sum",
+ readOnlyHint=True,
+ openWorldHint=False,
+ )
+)
+def calculate_sum(a: float, b: float) -> float:
+ """Add two numbers together."""
+ return a + b
+```
+
+FastMCP supports these standard annotations:
+
+| Annotation | Type | Default | Purpose |
+| :--------- | :--- | :------ | :------ |
+| `title` | string | - | Display name for user interfaces |
+| `readOnlyHint` | boolean | false | Indicates if the tool only reads without making changes |
+| `destructiveHint` | boolean | true | For non-readonly tools, signals if changes are destructive |
+| `idempotentHint` | boolean | false | Indicates if repeated identical calls have the same effect as a single call |
+| `openWorldHint` | boolean | true | Specifies if the tool interacts with external systems |
+
+Remember that annotations help make better user experiences but should be treated as advisory hints. They help client applications present appropriate UI elements and safety controls, but won't enforce security boundaries on their own. Always focus on making your annotations accurately represent what your tool actually does.
+
+### Using Annotation Hints
+
+MCP clients like Claude and ChatGPT use annotation hints to determine when to skip confirmation prompts and how to present tools to users. The most commonly used hint is `readOnlyHint`, which signals that a tool only reads data without making changes.
+
+**Read-only tools** improve user experience by:
+- Skipping confirmation prompts for safe operations
+- Allowing broader access without security concerns
+- Enabling more aggressive batching and caching
+
+Mark a tool as read-only when it retrieves data, performs calculations, or checks status without modifying state:
+
+```python
+from fastmcp import FastMCP
+from mcp.types import ToolAnnotations
+
+mcp = FastMCP("Data Server")
+
+@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
+def get_user(user_id: str) -> dict:
+ """Retrieve user information by ID."""
+ return {"id": user_id, "name": "Alice"}
+
+@mcp.tool(
+ annotations=ToolAnnotations(
+ readOnlyHint=True,
+ idempotentHint=True, # Same result for repeated calls
+ openWorldHint=False # Only internal data
+ )
+)
+def search_products(query: str) -> list[dict]:
+ """Search the product catalog."""
+ return [{"id": 1, "name": "Widget", "price": 29.99}]
+
+# Write operations - no readOnlyHint
+@mcp.tool()
+def update_user(user_id: str, name: str) -> dict:
+ """Update user information."""
+ return {"id": user_id, "name": name, "updated": True}
+
+@mcp.tool(annotations=ToolAnnotations(destructiveHint=True))
+def delete_user(user_id: str) -> dict:
+ """Permanently delete a user account."""
+ return {"deleted": user_id}
+```
+
+For tools that write to databases, send notifications, create/update/delete resources, or trigger workflows, omit `readOnlyHint` or set it to `False`. Use `destructiveHint=True` for operations that cannot be undone.
+
+Client-specific behavior:
+- **ChatGPT**: Skips confirmation prompts for read-only tools in Chat mode (see [ChatGPT integration](/integrations/chatgpt))
+- **Claude**: Uses hints to understand tool safety profiles and make better execution decisions
+
+## Notifications
+
+
+
+FastMCP automatically sends `notifications/tools/list_changed` notifications to connected clients when tools are added, removed, enabled, or disabled. This allows clients to stay up-to-date with the current tool set without manually polling for changes.
+
+```python
+@mcp.tool
+def example_tool() -> str:
+ return "Hello!"
+
+# These operations trigger notifications:
+mcp.add_tool(example_tool) # Sends tools/list_changed notification
+mcp.disable(keys={"tool:example_tool"}) # Sends tools/list_changed notification
+mcp.enable(keys={"tool:example_tool"}) # Sends tools/list_changed notification
+mcp.local_provider.remove_tool("example_tool") # Sends tools/list_changed notification
+```
+
+Notifications are only sent when these operations occur within an active MCP request context (e.g., when called from within a tool or other MCP operation). Operations performed during server initialization do not trigger notifications.
+
+Clients can handle these notifications using a [message handler](/clients/notifications) to automatically refresh their tool lists or update their interfaces.
+
+## Accessing the MCP Context
+
+Tools can access MCP features like logging, reading resources, or reporting progress through the `Context` object. To use it, add a parameter to your tool function with the type hint `Context`.
+
+```python
+from fastmcp import FastMCP, Context
+
+mcp = FastMCP(name="ContextDemo")
+
+@mcp.tool
+async def process_data(data_uri: str, ctx: Context) -> dict:
+ """Process data from a resource with progress reporting."""
+ await ctx.info(f"Processing data from {data_uri}")
+
+ # Read a resource
+ resource = await ctx.read_resource(data_uri)
+ data = resource[0].content if resource else ""
+
+ # Report progress
+ await ctx.report_progress(progress=50, total=100)
+
+ # Example request to the client's LLM for help
+ summary = await ctx.sample(f"Summarize this in 10 words: {data[:200]}")
+
+ await ctx.report_progress(progress=100, total=100)
+ return {
+ "length": len(data),
+ "summary": summary.text
+ }
+```
+
+The Context object provides access to:
+
+- **Logging**: `ctx.debug()`, `ctx.info()`, `ctx.warning()`, `ctx.error()`
+- **Progress Reporting**: `ctx.report_progress(progress, total)`
+- **Resource Access**: `ctx.read_resource(uri)`
+- **LLM Sampling**: `ctx.sample(...)`
+- **Request Information**: `ctx.request_id`, `ctx.client_id`
+
+For full documentation on the Context object and all its capabilities, see the [Context documentation](/servers/context).
+
+## Server Behavior
+
+### Duplicate Tools
+
+
+
+You can control how the FastMCP server behaves if you try to register multiple tools with the same name. This is configured using the `on_duplicate_tools` argument when creating the `FastMCP` instance.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP(
+ name="StrictServer",
+ # Configure behavior for duplicate tool names
+ on_duplicate_tools="error"
+)
+
+@mcp.tool
+def my_tool(): return "Version 1"
+
+# This will now raise a ValueError because 'my_tool' already exists
+# and on_duplicate_tools is set to "error".
+# @mcp.tool
+# def my_tool(): return "Version 2"
+```
+
+The duplicate behavior options are:
+
+- `"warn"` (default): Logs a warning and the new tool replaces the old one.
+- `"error"`: Raises a `ValueError`, preventing the duplicate registration.
+- `"replace"`: Silently replaces the existing tool with the new one.
+- `"ignore"`: Keeps the original tool and ignores the new registration attempt.
+
+### Removing Tools
+
+
+
+You can dynamically remove tools from a server through its [local provider](/servers/providers/local):
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="DynamicToolServer")
+
+@mcp.tool
+def calculate_sum(a: int, b: int) -> int:
+ """Add two numbers together."""
+ return a + b
+
+mcp.local_provider.remove_tool("calculate_sum")
+```
+
+## Versioning
+
+
+
+Tools support versioning, allowing you to maintain multiple implementations under the same name while clients automatically receive the highest version. See [Versioning](/servers/versioning) for complete documentation on version comparison, retrieval, and migration patterns.
diff --git a/docs/v3/servers/transforms/code-mode.mdx b/docs/v3/servers/transforms/code-mode.mdx
new file mode 100644
index 000000000..c7ef55bf0
--- /dev/null
+++ b/docs/v3/servers/transforms/code-mode.mdx
@@ -0,0 +1,361 @@
+---
+title: Code Mode
+sidebarTitle: Code Mode
+description: Let LLMs write Python to orchestrate tools in a sandbox
+icon: flask
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+
+CodeMode is experimental. The core interface is stable, but the specific discovery tools and their parameters may evolve as we learn more about what works best in practice.
+
+
+Standard MCP tool usage has two scaling problems. First, every tool in the catalog is loaded into the LLM's context upfront — with hundreds of tools, that's tens of thousands of tokens spent before the LLM even reads the user's request. Second, every tool call is a round-trip: the LLM calls a tool, the result passes back through the context window, the LLM reasons about it, calls another tool, and so on. Intermediate results that only exist to feed the next step still burn tokens flowing through the model.
+
+CodeMode solves both problems. Instead of seeing your entire tool catalog, the LLM gets meta-tools for discovering what's available and for writing and executing code that calls the tools it needs. It discovers on demand, writes a script that chains tool calls in a sandbox, and gets back only the final answer.
+
+The approach was introduced by Cloudflare in [Code Mode](https://blog.cloudflare.com/code-mode/) and explored further by Anthropic in [Code Execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp).
+
+## Getting Started
+
+
+CodeMode requires the `code-mode` extra for sandbox support. Install it with `pip install "fastmcp[code-mode]"`.
+
+
+You take a normal server with normally registered tools and add a `CodeMode` transform. The transform wraps your existing tools in the code mode machinery — your tool functions don't change at all:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.experimental.transforms.code_mode import CodeMode
+
+mcp = FastMCP("Server", transforms=[CodeMode()])
+
+@mcp.tool
+def add(x: int, y: int) -> int:
+ """Add two numbers."""
+ return x + y
+
+@mcp.tool
+def multiply(x: int, y: int) -> int:
+ """Multiply two numbers."""
+ return x * y
+```
+
+Clients connecting to this server no longer see `add` and `multiply` directly. Instead, they see the meta-tools that CodeMode provides — tools for discovering what's available and executing code against it. The original tools are still there, but they're accessed through the CodeMode layer.
+
+## Discovery
+
+Before the LLM can write code that calls your tools, it needs to know what tools exist and how to call them. This is the **discovery** process — the LLM uses meta-tools to learn about your tool catalog, then writes code against what it finds.
+
+The fundamental tradeoff is **tokens vs. round-trips**. Each discovery step is an LLM round-trip: the model calls a tool, waits for the response, reasons about it, then decides what to do next. More steps mean less wasted context (each step is targeted) but more latency and API calls. Fewer steps mean the LLM gets information upfront but pays for detail it might not need.
+
+By default, CodeMode gives the LLM three tools — `search`, `get_schema`, and `execute` — creating a three-stage discovery flow:
+
+
+
+First, the LLM uses the `search` meta-tool to find tools by keyword.
+
+For example, it might do `search(query="math numbers")` and receive the following response:
+
+```
+- add: Add two numbers.
+- multiply: Multiply two numbers.
+```
+
+This lets the LLM know which tools are available and what they do, significantly reducing the surface area it needs to consider.
+
+
+
+Next, the LLM calls `get_schema` to get parameter details for the tools it found in the previous step.
+
+For example, it might do `get_schema(tools=["add", "multiply"])` and receive the following response:
+
+```
+### add
+
+Add two numbers.
+
+**Parameters**
+- `x` (integer, required)
+- `y` (integer, required)
+
+### multiply
+
+Multiply two numbers.
+
+**Parameters**
+- `x` (integer, required)
+- `y` (integer, required)
+```
+
+Now the LLM knows the parameters for the tools it found, and can write code that chains the tool calls. If it needed more detail, it could have called `get_schema` with `detail="full"` to get the complete JSON schema.
+
+
+
+Finally, the LLM writes and executes code that chains the tool calls in a Python sandbox. Inside the sandbox, `call_tool(name, params)` is the only function available. The LLM uses this to compose tools into a workflow and return a final result.
+
+For example, it might write the following code and call the `execute` tool with it:
+
+```python
+a = await call_tool("add", {"x": 3, "y": 4})
+b = await call_tool("multiply", {"x": a, "y": 2})
+return b
+```
+
+The result is returned to the LLM.
+
+
+
+This three-stage flow works well for most servers — each step pulls in only the information needed for the next one, keeping context usage minimal. But CodeMode's discovery surface is fully configurable. The sections below explain each built-in discovery tool and how to combine them into different patterns.
+
+## Discovery Tools
+
+CodeMode ships with four built-in discovery tools: `Search`, `GetSchemas`, `GetTags`, and `ListTools`. By default, only `Search` and `GetSchemas` are enabled. Each tool supports a `default_detail` parameter that sets the default verbosity level, and the LLM can override the detail level on any individual call.
+
+### Detail Levels
+
+`Search` and `GetSchemas` share the same three detail levels, so the same `detail` value produces the same output format regardless of which tool the LLM calls:
+
+| Level | Output | Token cost |
+|---|---|---|
+| `"brief"` | Tool names and one-line descriptions | Cheapest — good for scanning |
+| `"detailed"` | Compact markdown with parameter names, types, and required markers | Medium — often enough to write code |
+| `"full"` | Complete JSON schema | Most expensive — everything |
+
+`Search` defaults to `"brief"` and `GetSchemas` defaults to `"detailed"`.
+
+### Search
+
+`Search` finds tools by natural-language query using BM25 ranking. At its default `"brief"` detail, results include just tool names and descriptions — enough to decide which tools are worth inspecting further. The LLM can request `"detailed"` to get parameter schemas inline, or `"full"` for the complete JSON.
+
+Search results include an annotation like `"2 of 10 tools:"` when the result set is smaller than the full catalog, so the LLM knows there are more tools to discover with different queries.
+
+You can cap result count with `default_limit`. The LLM can also override the limit per call. This is useful for large catalogs where you want to keep search results focused:
+
+```python
+Search(default_limit=5) # return at most 5 results per search
+```
+
+If your tools use [tags](/servers/tools#tags), Search also accepts a `tags` parameter so the LLM can narrow results to specific categories before searching.
+
+### GetSchemas
+
+`GetSchemas` returns parameter details for specific tools by name. At its default `"detailed"` level, it renders compact markdown with parameter names, types, and required markers. At `"full"`, it returns the complete JSON schema — useful when tools have deeply nested parameters that the compact format doesn't capture.
+
+### GetTags
+
+`GetTags` lets the LLM browse tools by category using [tag](/servers/tools#tags) metadata. At brief detail, the LLM sees tag names with counts. At full detail, it sees tools listed under each tag:
+
+```
+- math (3 tools)
+- text (2 tools)
+- untagged (1 tool)
+```
+
+`GetTags` isn't included in the defaults — add it when browsing by category would help the LLM orient itself in a large catalog. The LLM can browse tags first, then pass specific tags into Search to narrow results.
+
+### ListTools
+
+`ListTools` dumps the entire catalog at whatever detail level the LLM requests. It supports the same three detail levels as `Search` and `GetSchemas`, defaulting to `"brief"`.
+
+`ListTools` isn't included in the defaults — for large catalogs, search-based discovery is more token-efficient. But for smaller catalogs (under ~20 tools), letting the LLM see everything upfront can be faster than multiple search round-trips:
+
+```python
+from fastmcp.experimental.transforms.code_mode import CodeMode, ListTools, GetSchemas
+
+code_mode = CodeMode(
+ discovery_tools=[ListTools(), GetSchemas()],
+)
+```
+
+## Discovery Patterns
+
+The right discovery configuration depends on your server — how many tools you have and how complex their parameters are. It may be tempting to minimize round-trips by collapsing everything into fewer steps, but for the complex servers that benefit most from CodeMode, our experience is that staged discovery leads to better results. Flooding the LLM with detailed schemas for tools it doesn't end up using can hurt more than the extra round-trip costs. Each pattern below is a complete, copyable configuration.
+
+### Three-Stage
+
+The default. The LLM searches for candidates, inspects schemas for the ones it wants, then writes code. Best for **large or complex tool sets** where you want to minimize context usage — the LLM only pays for schemas it actually needs.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.experimental.transforms.code_mode import CodeMode
+
+mcp = FastMCP("Server", transforms=[CodeMode()])
+```
+
+If your tools use [tags](/servers/tools#tags), add `GetTags` so the LLM can browse by category before searching — giving it four stages of progressive disclosure:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.experimental.transforms.code_mode import CodeMode
+from fastmcp.experimental.transforms.code_mode import GetTags, Search, GetSchemas
+
+code_mode = CodeMode(
+ discovery_tools=[GetTags(), Search(), GetSchemas()],
+)
+
+mcp = FastMCP("Server", transforms=[code_mode])
+```
+
+### Two-Stage
+
+Search returns parameter schemas inline, so the LLM can go straight from search to execute. Best for **smaller catalogs** where the extra tokens per search result are a reasonable price for one fewer round-trip.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.experimental.transforms.code_mode import CodeMode
+from fastmcp.experimental.transforms.code_mode import Search, GetSchemas
+
+code_mode = CodeMode(
+ discovery_tools=[Search(default_detail="detailed"), GetSchemas()],
+)
+
+mcp = FastMCP("Server", transforms=[code_mode])
+```
+
+`GetSchemas` is still available as a fallback — the LLM can call it with `detail="full"` if it encounters a tool with complex nested parameters where the compact markdown isn't enough.
+
+### Single-Stage
+
+Skip discovery entirely and bake tool instructions into the execute tool's description. Best for **very simple servers** where the LLM already knows what tools are available — maybe there are only a few, or they're described in the system prompt.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.experimental.transforms.code_mode import CodeMode
+
+code_mode = CodeMode(
+ discovery_tools=[],
+ execute_description=(
+ "Available tools:\n"
+ "- add(x: int, y: int) -> int: Add two numbers\n"
+ "- multiply(x: int, y: int) -> int: Multiply two numbers\n\n"
+ "Write Python using `await call_tool(name, params)` and `return` the result."
+ ),
+)
+
+mcp = FastMCP("Server", transforms=[code_mode])
+```
+
+## Custom Discovery Tools
+
+Discovery tools are composable — you can mix the built-ins with your own. Each discovery tool is a callable that receives catalog access and returns a `Tool`. The catalog accessor is a function (not the catalog itself) because the catalog is request-scoped — different users may see different tools based on auth.
+
+Here's a minimal example:
+
+```python
+from fastmcp.experimental.transforms.code_mode import CodeMode
+from fastmcp.experimental.transforms.code_mode import GetToolCatalog, GetSchemas
+from fastmcp.server.context import Context
+from fastmcp.tools.tool import Tool
+
+def list_all_tools(get_catalog: GetToolCatalog) -> Tool:
+ async def list_tools(ctx: Context) -> str:
+ """List all available tool names."""
+ tools = await get_catalog(ctx)
+ return ", ".join(t.name for t in tools)
+
+ return Tool.from_function(fn=list_tools, name="list_tools")
+
+code_mode = CodeMode(discovery_tools=[list_all_tools, GetSchemas()])
+```
+
+The LLM sees the docstring of each discovery tool's inner function as its description — that's how it learns what each tool does and when to use it. Write docstrings that explain what the tool returns and when the LLM should call it.
+
+Discovery tools and the execute tool can also have custom names:
+
+```python
+from fastmcp.experimental.transforms.code_mode import Search, GetSchemas
+
+code_mode = CodeMode(
+ discovery_tools=[
+ Search(name="find_tools"),
+ GetSchemas(name="describe"),
+ ],
+ execute_tool_name="run_workflow",
+)
+
+mcp = FastMCP("Server", transforms=[code_mode])
+```
+
+## Sandbox Configuration
+
+### Resource Limits
+
+The default `MontySandboxProvider` enforces execution limits — timeouts, memory caps, recursion depth, and more.
+
+Constructed with no arguments, it applies a conservative baseline so the out-of-box configuration is not unbounded: `max_duration_secs=30` and `max_memory=100_000_000` (100 MB). Pass an explicit `limits` dict to override it, or `limits=None` to run with no limits at all:
+
+```python
+from fastmcp.experimental.transforms.code_mode import MontySandboxProvider
+
+MontySandboxProvider() # baseline: 30s, 100 MB
+MontySandboxProvider(limits={...}) # your own limits
+MontySandboxProvider(limits=None) # explicitly uncapped
+```
+
+```python
+from fastmcp.experimental.transforms.code_mode import CodeMode
+from fastmcp.experimental.transforms.code_mode import MontySandboxProvider
+
+sandbox = MontySandboxProvider(
+ limits={"max_duration_secs": 10, "max_memory": 50_000_000},
+)
+
+mcp = FastMCP("Server", transforms=[CodeMode(sandbox_provider=sandbox)])
+```
+
+All keys are optional — omit any to leave that dimension uncapped:
+
+| Key | Type | Description |
+|---|---|---|
+| `max_duration_secs` | `float` | Maximum wall-clock execution time |
+| `max_memory` | `int` | Memory ceiling in bytes |
+| `max_allocations` | `int` | Cap on total object allocations |
+| `max_recursion_depth` | `int` | Maximum recursion depth |
+| `gc_interval` | `int` | Garbage collection frequency |
+
+### Tool Call Limits
+
+A single `execute` block can issue many `call_tool()` invocations — a loop in LLM-generated code can fan out into a large number of backend operations from one request. `CodeMode` caps this at `max_tool_calls` (default `50`); exceeding it raises a `ToolError`. Pass `None` for no cap:
+
+```python
+from fastmcp.experimental.transforms.code_mode import CodeMode
+
+CodeMode() # default: 50 call_tool() calls per execute()
+CodeMode(max_tool_calls=200) # raise the cap
+CodeMode(max_tool_calls=None) # no cap
+```
+
+### Custom Sandbox Providers
+
+You can replace the default sandbox with any object implementing the `SandboxProvider` protocol:
+
+```python
+from collections.abc import Callable
+from typing import Any
+
+from fastmcp.experimental.transforms.code_mode import CodeMode
+from fastmcp.experimental.transforms.code_mode import SandboxProvider
+
+class RemoteSandboxProvider:
+ async def run(
+ self,
+ code: str,
+ *,
+ inputs: dict[str, Any] | None = None,
+ external_functions: dict[str, Callable[..., Any]] | None = None,
+ ) -> Any:
+ # Send code to your remote sandbox runtime
+ ...
+
+mcp = FastMCP(
+ "Server",
+ transforms=[CodeMode(sandbox_provider=RemoteSandboxProvider())],
+)
+```
+
+The `external_functions` dict contains async callables injected into the sandbox scope — `execute` uses this to provide `call_tool`.
diff --git a/docs/v3/servers/transforms/namespace.mdx b/docs/v3/servers/transforms/namespace.mdx
new file mode 100644
index 000000000..fdb0d1c7f
--- /dev/null
+++ b/docs/v3/servers/transforms/namespace.mdx
@@ -0,0 +1,63 @@
+---
+title: Namespace Transform
+sidebarTitle: Namespace
+description: Prefix component names to prevent conflicts
+icon: tag
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+The `Namespace` transform prefixes all component names, preventing conflicts when composing multiple servers.
+
+Tools and prompts receive an underscore-separated prefix. Resources and templates receive a path-segment prefix in their URIs.
+
+| Component | Original | With `Namespace("api")` |
+|-----------|----------|-------------------------|
+| Tool | `my_tool` | `api_my_tool` |
+| Prompt | `my_prompt` | `api_my_prompt` |
+| Resource | `data://info` | `data://api/info` |
+| Template | `data://{id}` | `data://api/{id}` |
+
+The most common use is through the `mount()` method's `namespace` parameter.
+
+```python
+from fastmcp import FastMCP
+
+weather = FastMCP("Weather")
+calendar = FastMCP("Calendar")
+
+@weather.tool
+def get_data() -> str:
+ return "Weather data"
+
+@calendar.tool
+def get_data() -> str:
+ return "Calendar data"
+
+# Without namespacing, these would conflict
+main = FastMCP("Main")
+main.mount(weather, namespace="weather")
+main.mount(calendar, namespace="calendar")
+
+# Clients see: weather_get_data, calendar_get_data
+```
+
+You can also apply namespacing directly using the `Namespace` transform.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.transforms import Namespace
+
+mcp = FastMCP("Server")
+
+@mcp.tool
+def greet(name: str) -> str:
+ return f"Hello, {name}!"
+
+# Namespace all components
+mcp.add_transform(Namespace("api"))
+
+# Tool is now: api_greet
+```
diff --git a/docs/v3/servers/transforms/namespacing.mdx b/docs/v3/servers/transforms/namespacing.mdx
new file mode 100644
index 000000000..009a1ee39
--- /dev/null
+++ b/docs/v3/servers/transforms/namespacing.mdx
@@ -0,0 +1,9 @@
+---
+title: Namespacing
+sidebarTitle: Namespacing
+description: Namespace and transform components with transforms
+icon: wand-magic-sparkles
+redirect: /servers/transforms/transforms
+---
+
+This page has moved to [Transforms](/servers/transforms/transforms).
diff --git a/docs/v3/servers/transforms/prompts-as-tools.mdx b/docs/v3/servers/transforms/prompts-as-tools.mdx
new file mode 100644
index 000000000..6a9ab1b47
--- /dev/null
+++ b/docs/v3/servers/transforms/prompts-as-tools.mdx
@@ -0,0 +1,130 @@
+---
+title: Prompts as Tools
+sidebarTitle: Prompts as Tools
+description: Expose prompts to tool-only clients
+icon: message-lines
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Some MCP clients only support tools. They cannot list or get prompts directly because they lack prompt protocol support. The `PromptsAsTools` transform bridges this gap by generating tools that provide access to your server's prompts.
+
+When you add `PromptsAsTools` to a server, it creates two tools that clients can call instead of using the prompt protocol:
+
+- **`list_prompts`** returns JSON describing all available prompts and their arguments
+- **`get_prompt`** renders a specific prompt with provided arguments
+
+This means any client that can call tools can now access prompts, even if the client has no native prompt support.
+
+## Basic Usage
+
+Pass your FastMCP server to `PromptsAsTools` when adding the transform. The generated tools route through the server at runtime, which means all server middleware — auth, visibility, rate limiting — applies to prompt operations automatically, exactly as it would for direct `prompts/get` calls.
+
+
+`PromptsAsTools` (and `ResourcesAsTools`) should be applied to a FastMCP server instance, not a raw Provider. The generated tools call back into the server's middleware chain at runtime, so they need a server to route through. If you want to expose only a subset of prompts, create a dedicated FastMCP server for those prompts and apply the transform there.
+
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.transforms import PromptsAsTools
+
+mcp = FastMCP("My Server")
+
+@mcp.prompt
+def analyze_code(code: str, language: str = "python") -> str:
+ """Analyze code for potential issues."""
+ return f"Analyze this {language} code:\n{code}"
+
+@mcp.prompt
+def explain_concept(concept: str) -> str:
+ """Explain a programming concept."""
+ return f"Explain: {concept}"
+
+# Add the transform - creates list_prompts and get_prompt tools
+mcp.add_transform(PromptsAsTools(mcp))
+```
+
+Clients now see three items: whatever tools you defined directly, plus `list_prompts` and `get_prompt`.
+
+## Listing Prompts
+
+The `list_prompts` tool returns JSON with metadata for each prompt, including its arguments.
+
+```python
+result = await client.call_tool("list_prompts", {})
+prompts = json.loads(result.data)
+# [
+# {
+# "name": "analyze_code",
+# "description": "Analyze code for potential issues.",
+# "arguments": [
+# {"name": "code", "description": null, "required": true},
+# {"name": "language", "description": null, "required": false}
+# ]
+# },
+# {
+# "name": "explain_concept",
+# "description": "Explain a programming concept.",
+# "arguments": [
+# {"name": "concept", "description": null, "required": true}
+# ]
+# }
+#]
+```
+
+Each argument includes:
+- `name`: The argument name
+- `description`: Optional description from type hints or docstrings
+- `required`: Whether the argument must be provided
+
+## Getting Prompts
+
+The `get_prompt` tool accepts a prompt name and optional arguments dict. It returns the rendered prompt as JSON with a messages array.
+
+```python
+# Prompt with required and optional arguments
+result = await client.call_tool(
+ "get_prompt",
+ {
+ "name": "analyze_code",
+ "arguments": {
+ "code": "x = 1\nprint(x)",
+ "language": "python"
+ }
+ }
+)
+
+response = json.loads(result.data)
+# {
+# "messages": [
+# {
+# "role": "user",
+# "content": "Analyze this python code:\nx = 1\nprint(x)"
+# }
+# ]
+# }
+```
+
+If a prompt has no arguments, you can omit the `arguments` field or pass an empty dict:
+
+```python
+result = await client.call_tool(
+ "get_prompt",
+ {"name": "simple_prompt"}
+)
+```
+
+## Message Format
+
+Rendered prompts return a messages array following the standard MCP format. Each message includes:
+- `role`: The message role ("user" or "assistant")
+- `content`: The message text content
+
+Multi-message prompts are supported - the array will contain all messages in order.
+
+## Binary Content
+
+Unlike resources, prompts always return text content. There is no binary encoding needed.
diff --git a/docs/v3/servers/transforms/resources-as-tools.mdx b/docs/v3/servers/transforms/resources-as-tools.mdx
new file mode 100644
index 000000000..b79980dcc
--- /dev/null
+++ b/docs/v3/servers/transforms/resources-as-tools.mdx
@@ -0,0 +1,111 @@
+---
+title: Resources as Tools
+sidebarTitle: Resources as Tools
+description: Expose resources to tool-only clients
+icon: toolbox
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Some MCP clients only support tools. They cannot list or read resources directly because they lack resource protocol support. The `ResourcesAsTools` transform bridges this gap by generating tools that provide access to your server's resources.
+
+When you add `ResourcesAsTools` to a server, it creates two tools that clients can call instead of using the resource protocol:
+
+- **`list_resources`** returns JSON describing all available resources and templates
+- **`read_resource`** reads a specific resource by URI
+
+This means any client that can call tools can now access resources, even if the client has no native resource support.
+
+## Basic Usage
+
+Pass your FastMCP server to `ResourcesAsTools` when adding the transform. The generated tools route through the server at runtime, which means all server middleware — auth, visibility, rate limiting — applies to resource operations automatically, exactly as it would for direct `resources/read` calls.
+
+
+`ResourcesAsTools` (and `PromptsAsTools`) should be applied to a FastMCP server instance, not a raw Provider. The generated tools call back into the server's middleware chain at runtime, so they need a server to route through. If you want to expose only a subset of resources, create a dedicated FastMCP server for those resources and apply the transform there.
+
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.transforms import ResourcesAsTools
+
+mcp = FastMCP("My Server")
+
+@mcp.resource("config://app")
+def app_config() -> str:
+ """Application configuration."""
+ return '{"app_name": "My App", "version": "1.0.0"}'
+
+@mcp.resource("user://{user_id}/profile")
+def user_profile(user_id: str) -> str:
+ """Get a user's profile by ID."""
+ return f'{{"user_id": "{user_id}", "name": "User {user_id}"}}'
+
+# Add the transform - creates list_resources and read_resource tools
+mcp.add_transform(ResourcesAsTools(mcp))
+```
+
+Clients now see three tools: whatever tools you defined directly, plus `list_resources` and `read_resource`.
+
+Both generated tools are annotated with `readOnlyHint=True`, since they only read data. Clients that respect tool annotations (like Cursor) can use this to auto-confirm these tool calls without prompting the user.
+
+## Static Resources vs Templates
+
+Resources come in two forms, and the `list_resources` tool distinguishes between them in its JSON output.
+
+Static resources have fixed URIs. They represent concrete data that exists at a known location. In the listing output, static resources include a `uri` field containing the exact URI to request.
+
+Resource templates have parameterized URIs with placeholders like `{user_id}`. They represent patterns for accessing dynamic data. In the listing output, templates include a `uri_template` field showing the pattern with its placeholders.
+
+When a client calls `list_resources`, it receives JSON like this:
+
+```json
+[
+ {
+ "uri": "config://app",
+ "name": "app_config",
+ "description": "Application configuration.",
+ "mime_type": "text/plain"
+ },
+ {
+ "uri_template": "user://{user_id}/profile",
+ "name": "user_profile",
+ "description": "Get a user's profile by ID."
+ }
+]
+```
+
+The client can distinguish resource types by checking which field is present: `uri` for static resources, `uri_template` for templates.
+
+## Reading Resources
+
+The `read_resource` tool accepts a single `uri` argument. For static resources, pass the exact URI. For templates, fill in the placeholders with actual values.
+
+```python
+# Reading a static resource
+result = await client.call_tool("read_resource", {"uri": "config://app"})
+print(result.data) # '{"app_name": "My App", "version": "1.0.0"}'
+
+# Reading a templated resource - fill in {user_id} with an actual ID
+result = await client.call_tool("read_resource", {"uri": "user://42/profile"})
+print(result.data) # '{"user_id": "42", "name": "User 42"}'
+```
+
+The transform handles template matching automatically. When you request `user://42/profile`, it matches against the `user://{user_id}/profile` template, extracts `user_id=42`, and calls your resource function with that parameter.
+
+## Binary Content
+
+Resources that return binary data (like images or files) are automatically base64-encoded when read through the `read_resource` tool. This ensures binary content can be transmitted as a string in the tool response.
+
+```python
+@mcp.resource("data://binary", mime_type="application/octet-stream")
+def binary_data() -> bytes:
+ return b"\x00\x01\x02\x03"
+
+# Client receives base64-encoded string
+result = await client.call_tool("read_resource", {"uri": "data://binary"})
+decoded = base64.b64decode(result.data) # b'\x00\x01\x02\x03'
+```
+
diff --git a/docs/v3/servers/transforms/tool-search.mdx b/docs/v3/servers/transforms/tool-search.mdx
new file mode 100644
index 000000000..204004f5c
--- /dev/null
+++ b/docs/v3/servers/transforms/tool-search.mdx
@@ -0,0 +1,173 @@
+---
+title: Tool Search
+sidebarTitle: Tool Search
+description: Replace large tool catalogs with on-demand search
+icon: magnifying-glass
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+When a server exposes hundreds or thousands of tools, sending the full catalog to an LLM wastes tokens and degrades tool selection accuracy. Search transforms solve this by replacing the tool listing with a search interface — the LLM discovers tools on demand instead of receiving everything upfront.
+
+## How It Works
+
+When you add a search transform, `list_tools()` returns just two synthetic tools instead of the full catalog:
+
+- **`search_tools`** finds tools matching a query and returns their full definitions
+- **`call_tool`** executes a discovered tool by name
+
+The original tools are still callable. They're hidden from the listing but remain fully functional — the search transform controls *discovery*, not *access*.
+
+Both synthetic tools search across tool names, descriptions, parameter names, and parameter descriptions. A search for `"email"` would match a tool named `send_email`, a tool with "email" in its description, or a tool with an `email_address` parameter.
+
+Search results are returned in the same JSON format as `list_tools`, including the full input schema, so the LLM can construct valid calls immediately without a second round-trip.
+
+## Search Strategies
+
+FastMCP provides two search transforms. They share the same interface — two synthetic tools, same configuration options — but differ in how they match queries to tools.
+
+### Regex Search
+
+`RegexSearchTransform` matches tools against a regex pattern using case-insensitive `re.search`. It has zero overhead and no index to build, making it a good default when the LLM knows roughly what it's looking for.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.transforms.search import RegexSearchTransform
+
+mcp = FastMCP("My Server", transforms=[RegexSearchTransform()])
+
+@mcp.tool
+def search_database(query: str, limit: int = 10) -> list[dict]:
+ """Search the database for records matching the query."""
+ ...
+
+@mcp.tool
+def delete_record(record_id: str) -> bool:
+ """Delete a record from the database by its ID."""
+ ...
+
+@mcp.tool
+def send_email(to: str, subject: str, body: str) -> bool:
+ """Send an email to the given recipient."""
+ ...
+```
+
+The LLM's `search_tools` call takes a `pattern` parameter — a regex string:
+
+```python
+# Exact substring match
+result = await client.call_tool("search_tools", {"pattern": "database"})
+# Returns: search_database, delete_record
+
+# Regex pattern
+result = await client.call_tool("search_tools", {"pattern": "send.*email|notify"})
+# Returns: send_email
+```
+
+Results are returned in catalog order. If the pattern is invalid regex, the search returns an empty list rather than raising an error.
+
+### BM25 Search
+
+`BM25SearchTransform` ranks tools by relevance using the [BM25 Okapi](https://en.wikipedia.org/wiki/Okapi_BM25) algorithm. It's better for natural language queries because it scores each tool based on term frequency and document rarity, returning results ranked by relevance rather than filtering by match/no-match.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.transforms.search import BM25SearchTransform
+
+mcp = FastMCP("My Server", transforms=[BM25SearchTransform()])
+
+# ... define tools ...
+```
+
+The LLM's `search_tools` call takes a `query` parameter — natural language:
+
+```python
+result = await client.call_tool("search_tools", {
+ "query": "tools for deleting things from the database"
+})
+# Returns: delete_record ranked first, search_database second
+```
+
+BM25 builds an in-memory index from the searchable text of all tools. The index is created lazily on the first search and automatically rebuilt whenever the tool catalog changes — for example, when tools are added, removed, or have their descriptions updated. The staleness check is based on a hash of all searchable text, so description changes are detected even when tool names stay the same.
+
+### Which to Choose
+
+Use **regex** when your LLM is good at constructing targeted patterns and you want deterministic, predictable results. Regex is also simpler to debug — you can see exactly what pattern was sent.
+
+Use **BM25** when your LLM tends to describe what it needs in natural language, or when your tool catalog has nuanced descriptions where relevance ranking adds value. BM25 handles partial matches and synonyms better because it scores on individual terms rather than requiring a single pattern to match.
+
+## Configuration
+
+Both search transforms accept the same configuration options.
+
+### Limiting Results
+
+By default, search returns at most 5 tools. Adjust `max_results` based on your catalog size and how much context you want the LLM to receive per search:
+
+```python
+mcp.add_transform(RegexSearchTransform(max_results=10))
+mcp.add_transform(BM25SearchTransform(max_results=3))
+```
+
+With regex, results stop as soon as the limit is reached (first N matches in catalog order). With BM25, all tools are scored and the top N by relevance are returned.
+
+### Pinning Tools
+
+Some tools should always be visible regardless of search. Use `always_visible` to pin them in the listing alongside the synthetic tools:
+
+```python
+mcp.add_transform(RegexSearchTransform(
+ always_visible=["help", "status"],
+))
+
+# list_tools returns: help, status, search_tools, call_tool
+```
+
+Pinned tools appear directly in `list_tools` so the LLM can call them without searching. They're excluded from search results to avoid duplication.
+
+### Custom Tool Names
+
+The default names `search_tools` and `call_tool` can be changed to avoid conflicts with real tools:
+
+```python
+mcp.add_transform(RegexSearchTransform(
+ search_tool_name="find_tools",
+ call_tool_name="run_tool",
+))
+```
+
+## The `call_tool` Proxy
+
+The `call_tool` proxy forwards calls to the real tool. When a client calls `call_tool(name="search_database", arguments={...})`, the proxy resolves `search_database` through the server's normal tool pipeline — including transforms and middleware — and executes it.
+
+The proxy rejects attempts to call the synthetic tools themselves. `call_tool(name="call_tool")` raises an error rather than recursing.
+
+
+Tools discovered through search can also be called directly via `client.call_tool("search_database", {...})` without going through the proxy. The proxy exists for LLMs that only know about the tools returned by `list_tools` and need a way to invoke discovered tools through a tool they can see.
+
+
+## Auth and Visibility
+
+Search results respect the full authorization pipeline. Tools filtered by middleware, visibility transforms, or component-level auth checks won't appear in search results.
+
+The search tool queries `list_tools()` through the complete pipeline at search time, so the same filtering that controls what a client sees in the listing also controls what they can discover through search.
+
+```python
+from fastmcp.server.transforms import Visibility
+from fastmcp.server.transforms.search import RegexSearchTransform
+
+mcp = FastMCP("My Server")
+
+# ... define tools ...
+
+# Disable admin tools globally
+mcp.add_transform(Visibility(False, tags={"admin"}))
+
+# Add search — admin tools won't appear in results
+mcp.add_transform(RegexSearchTransform())
+```
+
+Session-level visibility changes (via `ctx.disable_components()`) are also reflected immediately in search results.
diff --git a/docs/v3/servers/transforms/tool-transformation.mdx b/docs/v3/servers/transforms/tool-transformation.mdx
new file mode 100644
index 000000000..a50513f87
--- /dev/null
+++ b/docs/v3/servers/transforms/tool-transformation.mdx
@@ -0,0 +1,230 @@
+---
+title: Tool Transformation
+sidebarTitle: Tool Transformation
+description: Modify tool schemas - rename, reshape arguments, and customize behavior
+icon: wrench
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Tool transformation lets you modify tool schemas - renaming tools, changing descriptions, adjusting tags, and reshaping argument schemas. FastMCP provides two mechanisms that share the same configuration options but differ in timing.
+
+**Deferred transformation** with `ToolTransform` applies modifications when tools flow through a transform chain. Use this for tools from mounted servers, proxies, or other providers where you don't control the source directly.
+
+**Immediate transformation** with `Tool.from_tool()` creates a modified tool object right away. Use this when you have direct access to a tool and want to transform it before registration.
+
+## ToolTransform
+
+The `ToolTransform` class is a transform that modifies tools as they flow through a provider. Provide a dictionary mapping original tool names to their transformation configuration.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.transforms import ToolTransform
+from fastmcp.tools.tool_transform import ToolTransformConfig
+
+mcp = FastMCP("Server")
+
+@mcp.tool
+def verbose_internal_data_fetcher(query: str) -> str:
+ """Fetches data from the internal database."""
+ return f"Results for: {query}"
+
+# Rename the tool to something simpler
+mcp.add_transform(ToolTransform({
+ "verbose_internal_data_fetcher": ToolTransformConfig(
+ name="search",
+ description="Search the database.",
+ )
+}))
+
+# Clients see "search" with the cleaner description
+```
+
+`ToolTransform` is useful when you want to modify tools from mounted or proxied servers without changing the original source.
+
+## Tool.from_tool()
+
+Use `Tool.from_tool()` when you have the tool object and want to create a transformed version for registration.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.tools import Tool, tool
+from fastmcp.tools.tool_transform import ArgTransform
+
+# Create a tool without registering it
+@tool
+def search(q: str, limit: int = 10) -> list[str]:
+ """Search for items."""
+ return [f"Result {i} for {q}" for i in range(limit)]
+
+# Transform it before registration
+better_search = Tool.from_tool(
+ search,
+ name="find_items",
+ description="Find items matching your search query.",
+ transform_args={
+ "q": ArgTransform(
+ name="query",
+ description="The search terms to look for.",
+ ),
+ },
+)
+
+mcp = FastMCP("Server")
+mcp.add_tool(better_search)
+```
+
+The standalone `@tool` decorator (from `fastmcp.tools`) creates a Tool object without registering it to any server. This separates creation from registration, letting you transform tools before deciding where they go.
+
+## Modification Options
+
+Both mechanisms support the same modifications.
+
+**Tool-level options:**
+
+| Option | Description |
+|--------|-------------|
+| `name` | New name for the tool |
+| `description` | New description |
+| `title` | Human-readable title |
+| `tags` | Set of tags for categorization |
+| `annotations` | MCP ToolAnnotations |
+| `meta` | Custom metadata dictionary |
+| `enabled` | Whether the tool is visible to clients (default `True`) |
+
+**Argument-level options** (via `ArgTransform` or `ArgTransformConfig`):
+
+| Option | Description |
+|--------|-------------|
+| `name` | Rename the argument |
+| `description` | New description for the argument |
+| `default` | New default value |
+| `default_factory` | Callable that generates a default (requires `hide=True`) |
+| `hide` | Remove from client-visible schema |
+| `required` | Make an optional argument required |
+| `type` | Change the argument's type |
+| `examples` | Example values for the argument |
+
+## Hiding Arguments
+
+Hide arguments to simplify the interface or inject values the client shouldn't control.
+
+```python
+from fastmcp.tools.tool_transform import ArgTransform
+
+# Hide with a constant value
+transform_args = {
+ "api_key": ArgTransform(hide=True, default="secret-key"),
+}
+
+# Hide with a dynamic value
+import uuid
+transform_args = {
+ "request_id": ArgTransform(hide=True, default_factory=lambda: str(uuid.uuid4())),
+}
+```
+
+Hidden arguments disappear from the tool's schema. The client never sees them, but the underlying function receives the configured value.
+
+
+`default_factory` requires `hide=True`. Visible arguments need static defaults that can be represented in JSON Schema.
+
+
+## Renaming Arguments
+
+Rename arguments to make them more intuitive for LLMs or match your API conventions.
+
+```python
+from fastmcp.tools import Tool, tool
+from fastmcp.tools.tool_transform import ArgTransform
+
+@tool
+def search(q: str, n: int = 10) -> list[str]:
+ """Search for items."""
+ return []
+
+better_search = Tool.from_tool(
+ search,
+ transform_args={
+ "q": ArgTransform(name="query", description="Search terms"),
+ "n": ArgTransform(name="max_results", description="Maximum results to return"),
+ },
+)
+```
+
+## Custom Transform Functions
+
+For advanced scenarios, provide a `transform_fn` that intercepts tool execution. The function can validate inputs, modify outputs, or add custom logic while still calling the original tool via `forward()`.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.tools import Tool, tool
+from fastmcp.tools.tool_transform import forward, ArgTransform
+
+@tool
+def divide(a: float, b: float) -> float:
+ """Divide a by b."""
+ return a / b
+
+async def safe_divide(numerator: float, denominator: float) -> float:
+ if denominator == 0:
+ raise ValueError("Cannot divide by zero")
+ return await forward(numerator=numerator, denominator=denominator)
+
+safe_division = Tool.from_tool(
+ divide,
+ name="safe_divide",
+ transform_fn=safe_divide,
+ transform_args={
+ "a": ArgTransform(name="numerator"),
+ "b": ArgTransform(name="denominator"),
+ },
+)
+
+mcp = FastMCP("Server")
+mcp.add_tool(safe_division)
+```
+
+The `forward()` function handles argument mapping automatically. Call it with the transformed argument names, and it maps them back to the original function's parameters.
+
+For direct access to the original function without mapping, use `forward_raw()` with the original parameter names.
+
+## Context-Aware Tool Factories
+
+You can write functions that act as "factories," generating specialized versions of a tool for different contexts. For example, create a `get_my_data` tool for the current user by hiding the `user_id` parameter and providing it automatically.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.tools import Tool, tool
+from fastmcp.tools.tool_transform import ArgTransform
+
+# A generic tool that requires a user_id
+@tool
+def get_user_data(user_id: str, query: str) -> str:
+ """Fetch data for a specific user."""
+ return f"Data for user {user_id}: {query}"
+
+
+def create_user_tool(user_id: str) -> Tool:
+ """Factory that creates a user-specific version of get_user_data."""
+ return Tool.from_tool(
+ get_user_data,
+ name="get_my_data",
+ description="Fetch your data. No need to specify a user ID.",
+ transform_args={
+ "user_id": ArgTransform(hide=True, default=user_id),
+ },
+ )
+
+
+# Create a server with a tool customized for the current user
+mcp = FastMCP("User Server")
+current_user_id = "user-123" # e.g., from auth context
+mcp.add_tool(create_user_tool(current_user_id))
+
+# Clients see "get_my_data(query: str)" — user_id is injected automatically
+```
+
+This pattern is useful for multi-tenant servers where each connection gets tools pre-configured with their identity, or for wrapping generic tools with environment-specific defaults.
diff --git a/docs/v3/servers/transforms/transforms.mdx b/docs/v3/servers/transforms/transforms.mdx
new file mode 100644
index 000000000..4347b2f18
--- /dev/null
+++ b/docs/v3/servers/transforms/transforms.mdx
@@ -0,0 +1,173 @@
+---
+title: Transforms Overview
+sidebarTitle: Overview
+description: Modify components as they flow through your server
+icon: wand-magic-sparkles
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Transforms modify components as they flow from providers to clients. When a client asks "what tools do you have?", the request passes through each transform in the chain. Each transform can modify the components before passing them along.
+
+## Mental Model
+
+Think of transforms as filters in a pipeline. Components flow from providers through transforms to reach clients:
+
+```
+Provider → [Transform A] → [Transform B] → Client
+```
+
+When listing components, transforms receive sequences and return transformed sequences—a pure function pattern. When getting a specific component by name, transforms use a middleware pattern with `call_next`, working in reverse: mapping the client's requested name back to the original, then transforming the result.
+
+## Built-in Transforms
+
+FastMCP provides several transforms for common use cases:
+
+- **[Namespace](/servers/transforms/namespace)** - Prefix component names to prevent conflicts when composing servers
+- **[Tool Transformation](/servers/transforms/tool-transformation)** - Rename tools, modify descriptions, reshape arguments
+- **[Enabled](/servers/visibility)** - Control which components are visible at runtime
+- **[Tool Search](/servers/transforms/tool-search)** - Replace large tool catalogs with on-demand search
+- **[Resources as Tools](/servers/transforms/resources-as-tools)** - Expose resources to tool-only clients
+- **[Prompts as Tools](/servers/transforms/prompts-as-tools)** - Expose prompts to tool-only clients
+- **[Code Mode (Experimental)](/servers/transforms/code-mode)** - Replace many tools with programmable `search` + `execute`
+
+## Server vs Provider Transforms
+
+Transforms can be added at two levels, each serving different purposes.
+
+### Provider-Level Transforms
+
+Provider transforms apply to components from a specific provider. They run first, modifying components before they reach the server level.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.providers import FastMCPProvider
+from fastmcp.server.transforms import Namespace, ToolTransform
+from fastmcp.tools.tool_transform import ToolTransformConfig
+
+sub_server = FastMCP("Sub")
+
+@sub_server.tool
+def process(data: str) -> str:
+ return f"Processed: {data}"
+
+# Create provider and add transforms
+provider = FastMCPProvider(sub_server)
+provider.add_transform(Namespace("api"))
+provider.add_transform(ToolTransform({
+ "api_process": ToolTransformConfig(description="Process data through the API"),
+}))
+
+main = FastMCP("Main", providers=[provider])
+# Tool is now: api_process with updated description
+```
+
+When using `mount()`, the returned provider reference lets you add transforms directly.
+
+```python
+main = FastMCP("Main")
+mount = main.mount(sub_server, namespace="api")
+mount.add_transform(ToolTransform({...}))
+```
+
+### Server-Level Transforms
+
+Server transforms apply to all components from all providers. They run after provider transforms, seeing the already-transformed names.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.transforms import Namespace
+
+mcp = FastMCP("Server", transforms=[Namespace("v1")])
+
+@mcp.tool
+def greet(name: str) -> str:
+ return f"Hello, {name}!"
+
+# All tools become v1_toolname
+```
+
+Server-level transforms are useful for API versioning or applying consistent naming across your entire server.
+
+### Transform Order
+
+Transforms stack in the order they're added. The first transform added is innermost (closest to the provider), and subsequent transforms wrap it.
+
+```python
+from fastmcp.server.providers import FastMCPProvider
+from fastmcp.server.transforms import Namespace, ToolTransform
+from fastmcp.tools.tool_transform import ToolTransformConfig
+
+provider = FastMCPProvider(server)
+provider.add_transform(Namespace("api")) # Applied first
+provider.add_transform(ToolTransform({ # Sees namespaced names
+ "api_verbose_name": ToolTransformConfig(name="short"),
+}))
+
+# Flow: "verbose_name" -> "api_verbose_name" -> "short"
+```
+
+When a client requests "short", the transforms reverse the mapping: ToolTransform maps "short" to "api_verbose_name", then Namespace strips the prefix to find "verbose_name" in the provider.
+
+## Custom Transforms
+
+Create custom transforms by subclassing `Transform` and overriding the methods you need.
+
+```python
+from collections.abc import Sequence
+from fastmcp.server.transforms import Transform, GetToolNext
+from fastmcp.tools.tool import Tool
+
+class TagFilter(Transform):
+ """Filter tools to only those with specific tags."""
+
+ def __init__(self, required_tags: set[str]):
+ self.required_tags = required_tags
+
+ async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
+ return [t for t in tools if t.tags & self.required_tags]
+
+ async def get_tool(self, name: str, call_next: GetToolNext) -> Tool | None:
+ tool = await call_next(name)
+ if tool and tool.tags & self.required_tags:
+ return tool
+ return None
+```
+
+The `Transform` base class provides default implementations that pass through unchanged. Override only the methods relevant to your transform.
+
+Each component type has two methods with different patterns:
+
+| Method | Pattern | Purpose |
+|--------|---------|---------|
+| `list_tools(tools)` | Pure function | Transform the sequence of tools |
+| `get_tool(name, call_next)` | Middleware | Transform lookup by name |
+| `list_resources(resources)` | Pure function | Transform the sequence of resources |
+| `get_resource(uri, call_next)` | Middleware | Transform lookup by URI |
+| `list_resource_templates(templates)` | Pure function | Transform the sequence of templates |
+| `get_resource_template(uri, call_next)` | Middleware | Transform template lookup by URI |
+| `list_prompts(prompts)` | Pure function | Transform the sequence of prompts |
+| `get_prompt(name, call_next)` | Middleware | Transform lookup by name |
+
+List methods receive sequences directly and return transformed sequences. Get methods use `call_next` for routing flexibility—when a client requests "new_name", your transform maps it back to "original_name" before calling `call_next()`.
+
+```python
+class PrefixTransform(Transform):
+ def __init__(self, prefix: str):
+ self.prefix = prefix
+
+ async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
+ return [t.model_copy(update={"name": f"{self.prefix}_{t.name}"}) for t in tools]
+
+ async def get_tool(self, name: str, call_next: GetToolNext) -> Tool | None:
+ # Reverse the prefix to find the original
+ if not name.startswith(f"{self.prefix}_"):
+ return None
+ original = name[len(self.prefix) + 1:]
+ tool = await call_next(original)
+ if tool:
+ return tool.model_copy(update={"name": name})
+ return None
+```
diff --git a/docs/v3/servers/versioning.mdx b/docs/v3/servers/versioning.mdx
new file mode 100644
index 000000000..4c44a73bd
--- /dev/null
+++ b/docs/v3/servers/versioning.mdx
@@ -0,0 +1,336 @@
+---
+title: Versioning
+sidebarTitle: Versioning
+description: Serve multiple API versions from a single codebase
+icon: code-branch
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Component versioning lets you maintain multiple implementations of the same tool, resource, or prompt under a single identifier. You register each version, and FastMCP handles the rest: clients see the highest version by default, but you can filter to expose exactly the versions you want.
+
+The primary use case is serving different API versions from one codebase. Instead of maintaining separate deployments for v1 and v2 clients, you version your components and use `VersionFilter` to create distinct API surfaces.
+
+## Versioned API Surfaces
+
+Consider a server that needs to support both v1 and v2 clients. The v2 API adds new parameters to existing tools, and you want both versions to coexist cleanly. Define your components on a shared provider, then create separate servers with different version filters.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.providers import LocalProvider
+from fastmcp.server.transforms import VersionFilter
+
+# Define versioned components on a shared provider
+components = LocalProvider()
+
+@components.tool(version="1.0")
+def calculate(x: int, y: int) -> int:
+ """Add two numbers."""
+ return x + y
+
+@components.tool(version="2.0")
+def calculate(x: int, y: int, z: int = 0) -> int:
+ """Add two or three numbers."""
+ return x + y + z
+
+# Create servers that share the provider with different filters
+api_v1 = FastMCP("API v1", providers=[components])
+api_v1.add_transform(VersionFilter(version_lt="2.0"))
+
+api_v2 = FastMCP("API v2", providers=[components])
+api_v2.add_transform(VersionFilter(version_gte="2.0"))
+```
+
+Clients connecting to `api_v1` see the two-argument `calculate`. Clients connecting to `api_v2` see the three-argument version. Both servers share the same component definitions.
+
+`VersionFilter` accepts two keyword-only parameters that mirror comparison operators: `version_gte` (greater than or equal) and `version_lt` (less than). You can use either or both to define your version range.
+
+```python
+# Versions < 3.0 (v1.x and v2.x)
+VersionFilter(version_lt="3.0")
+
+# Versions >= 2.0 (v2.x and later)
+VersionFilter(version_gte="2.0")
+
+# Versions in range [2.0, 3.0) (only v2.x)
+VersionFilter(version_gte="2.0", version_lt="3.0")
+```
+
+
+**Unversioned components are exempt from version filtering by default.** Set `include_unversioned=False` to exclude them. Including them by default ensures that adding version filtering to a server with mixed versioned and unversioned components doesn't accidentally hide the unversioned ones. To prevent confusion, FastMCP forbids mixing versioned and unversioned components with the same name.
+
+
+### Filtering Mounted Servers
+
+When you mount child servers and apply a `VersionFilter` to the parent, the filter applies to components from mounted servers as well. Range filtering (`version_gte` and `version_lt`) is handled at the provider level, meaning mounted servers don't need to know about the parent's version constraints.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.transforms import VersionFilter
+
+# Child server with versioned components
+child = FastMCP("Child")
+
+@child.tool(version="1.0")
+def process(data: str) -> str:
+ return data.upper()
+
+@child.tool(version="2.0")
+def process(data: str, mode: str = "default") -> str:
+ return data.upper() if mode == "default" else data.lower()
+
+# Parent server mounts child and applies version filter
+parent = FastMCP("Parent")
+parent.mount(child, namespace="child")
+parent.add_transform(VersionFilter(version_lt="2.0"))
+
+# Clients see only child_process v1.0
+```
+
+The parent's `VersionFilter` sees components after they've been namespaced, but filters based on version regardless of namespace. This lets you apply version policies consistently across your entire server hierarchy.
+
+## Declaring Versions
+
+Add a `version` parameter to any component decorator. FastMCP stores versions as strings and groups components by their identifier (name for tools and prompts, URI for resources).
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP()
+
+@mcp.tool(version="1.0")
+def process(data: str) -> str:
+ """Original processing."""
+ return data.upper()
+
+@mcp.tool(version="2.0")
+def process(data: str, mode: str = "default") -> str:
+ """Enhanced processing with mode selection."""
+ if mode == "reverse":
+ return data[::-1].upper()
+ return data.upper()
+```
+
+Both versions are registered. When a client lists tools, they see only `process` with version 2.0 (the highest). When they invoke `process`, version 2.0 executes. The same pattern applies to resources and prompts.
+
+### Versioned vs Unversioned Components
+
+For any given component name, you must choose one approach: either version all implementations or version none of them. Mixing versioned and unversioned components with the same name raises an error at registration time.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP()
+
+@mcp.tool
+def calculate(x: int, y: int) -> int:
+ """Unversioned tool."""
+ return x + y
+
+@mcp.tool(version="2.0") # Raises ValueError
+def calculate(x: int, y: int, z: int = 0) -> int:
+ """Cannot mix versioned with unversioned."""
+ return x + y + z
+```
+
+The error message explains the conflict: "Cannot add versioned tool 'calculate' (version='2.0'): an unversioned tool with this name already exists. Either version all components or none."
+
+This restriction helps keep version filtering behavior predictable.
+
+Resources and prompts follow the same pattern.
+
+```python
+@mcp.resource("config://app", version="1.0")
+def config_v1() -> str:
+ return '{"format": "legacy"}'
+
+@mcp.resource("config://app", version="2.0")
+def config_v2() -> str:
+ return '{"format": "modern", "schema": "v2"}'
+
+@mcp.prompt(version="1.0")
+def summarize(text: str) -> str:
+ return f"Summarize: {text}"
+
+@mcp.prompt(version="2.0")
+def summarize(text: str, style: str = "concise") -> str:
+ return f"Summarize in a {style} style: {text}"
+```
+
+### Version Discovery
+
+When clients list components, each versioned component includes metadata about all available versions. This lets clients discover what versions exist before deciding which to use. The `meta.fastmcp.versions` field contains all registered versions sorted from highest to lowest.
+
+```python
+from fastmcp import Client
+
+async with Client(server) as client:
+ tools = await client.list_tools()
+
+ for tool in tools:
+ if tool.meta:
+ fastmcp_meta = tool.meta.get("fastmcp", {})
+ # Current version being returned (highest by default)
+ print(f"Version: {fastmcp_meta.get('version')}")
+ # All available versions for this component
+ print(f"Available: {fastmcp_meta.get('versions')}")
+```
+
+For a tool with versions `"1.0"` and `"2.0"`, listing returns the `2.0` implementation with `meta.fastmcp.version` set to `"2.0"` and `meta.fastmcp.versions` set to `["2.0", "1.0"]`. Unversioned components omit these fields entirely.
+
+This discovery mechanism enables clients to make informed decisions about which version to request, support graceful degradation when newer versions introduce breaking changes, or display version information in developer tools.
+
+## Requesting Specific Versions
+
+By default, clients receive and invoke the highest version of each component. When you need a specific version, FastMCP provides two approaches: the FastMCP client API for Python applications, and the MCP protocol mechanism for any MCP-compatible client.
+
+### FastMCP Client
+
+The FastMCP client's `call_tool` and `get_prompt` methods accept an optional `version` parameter. When specified, the server executes that exact version instead of the highest.
+
+```python
+from fastmcp import Client
+
+async with Client(server) as client:
+ # Call the highest version (default behavior)
+ result = await client.call_tool("calculate", {"x": 1, "y": 2})
+
+ # Call a specific version
+ result_v1 = await client.call_tool("calculate", {"x": 1, "y": 2}, version="1.0")
+
+ # Get a specific prompt version
+ prompt = await client.get_prompt("summarize", {"text": "..."}, version="1.0")
+```
+
+If the requested version doesn't exist, the server raises a `NotFoundError`. This ensures you get exactly what you asked for rather than silently falling back to a different version.
+
+### MCP Protocol
+
+For generic MCP clients that don't have built-in version support, pass the version through the `_meta` field in arguments. FastMCP servers extract the version from `_meta.fastmcp.version` before processing.
+
+
+```json Tool Call Arguments
+{
+ "x": 1,
+ "y": 2,
+ "_meta": {
+ "fastmcp": {
+ "version": "1.0"
+ }
+ }
+}
+```
+
+```json Prompt Arguments
+{
+ "text": "Summarize this document...",
+ "_meta": {
+ "fastmcp": {
+ "version": "1.0"
+ }
+ }
+}
+```
+
+
+The `_meta` field is part of the MCP request params, not arguments, so your component implementation never sees it. This convention allows version selection to work across any MCP client without requiring protocol changes. The FastMCP client handles this automatically when you pass the `version` parameter.
+
+## Version Comparison
+
+FastMCP compares versions to determine which is "highest" when multiple versions share an identifier. The comparison behavior depends on the version format.
+
+For [PEP 440](https://peps.python.org/pep-0440/) versions (like `"1.0"`, `"2.1.3"`, `"1.0a1"`), FastMCP uses semantic comparison where numeric segments are compared as numbers.
+
+```python
+# PEP 440 versions compare semantically
+"1" < "2" < "10" # Numeric order (not "1" < "10" < "2")
+"1.9" < "1.10" # Numeric order (not "1.10" < "1.9")
+"1.0a1" < "1.0b1" < "1.0" # Pre-releases sort before releases
+```
+
+For other formats (dates, custom schemes), FastMCP falls back to lexicographic string comparison. This works well for ISO dates and other naturally sortable formats.
+
+```python
+# Non-PEP 440 versions compare as strings
+"2025-01-15" < "2025-02-01" # ISO dates sort correctly
+"alpha" < "beta" # Alphabetical order
+```
+
+The `v` prefix is stripped before comparison, so `"v1.0"` and `"1.0"` are treated as equal for sorting purposes.
+
+## Retrieving Specific Versions
+
+Server-side code can retrieve specific versions rather than just the highest. This is useful during migrations when you need to compare behavior between versions or access legacy implementations.
+
+The `get_tool`, `get_resource`, and `get_prompt` methods accept an optional `version` parameter. Without it, they return the highest version. With it, they return exactly that version.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP()
+
+@mcp.tool(version="1.0")
+def add(x: int, y: int) -> int:
+ return x + y
+
+@mcp.tool(version="2.0")
+def add(x: int, y: int) -> int:
+ return x + y + 100 # Different behavior
+
+# Get highest version (default)
+tool = await mcp.get_tool("add")
+print(tool.version) # "2.0"
+
+# Get specific version
+tool_v1 = await mcp.get_tool("add", version="1.0")
+print(tool_v1.version) # "1.0"
+```
+
+If the requested version doesn't exist, a `NotFoundError` is raised.
+
+## Removing Versions
+
+The `remove_tool`, `remove_resource`, and `remove_prompt` methods on the server's [local provider](/servers/providers/local) accept an optional `version` parameter that controls what gets removed.
+
+```python
+# Remove ALL versions of a component
+mcp.local_provider.remove_tool("calculate")
+
+# Remove only a specific version
+mcp.local_provider.remove_tool("calculate", version="1.0")
+```
+
+When you remove a specific version, other versions remain registered. When you remove without specifying a version, all versions are removed.
+
+## Migration Workflow
+
+Versioning supports gradual migration when updating component behavior. You can deploy new versions alongside old ones, verify the new behavior works correctly, then clean up.
+
+When migrating an existing unversioned component to use versioning, start by assigning an initial version to your existing implementation. Then add the new version alongside it.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP()
+
+@mcp.tool(version="1.0")
+def process_data(input: str) -> str:
+ """Original implementation, now versioned."""
+ return legacy_process(input)
+
+@mcp.tool(version="2.0")
+def process_data(input: str, options: dict | None = None) -> str:
+ """Updated implementation with new options parameter."""
+ return modern_process(input, options or {})
+```
+
+Clients automatically see version 2.0 (the highest). During the transition, your server code can still access the original implementation via `get_tool("process_data", version="1.0")`.
+
+Once the migration is complete, remove the old version.
+
+```python
+mcp.local_provider.remove_tool("process_data", version="1.0")
+```
diff --git a/docs/v3/servers/visibility.mdx b/docs/v3/servers/visibility.mdx
new file mode 100644
index 000000000..509bd7068
--- /dev/null
+++ b/docs/v3/servers/visibility.mdx
@@ -0,0 +1,452 @@
+---
+title: Component Visibility
+sidebarTitle: Visibility
+description: Control which components are available to clients
+icon: toggle-on
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Components can be dynamically enabled or disabled at runtime. A disabled tool disappears from listings and cannot be called. This enables runtime access control, feature flags, and context-aware component exposure.
+
+## Component Visibility
+
+Every FastMCP server provides `enable()` and `disable()` methods for controlling component availability.
+
+### Disabling Components
+
+The `disable()` method marks components as disabled. Disabled components are filtered out from all client queries.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("Server")
+
+@mcp.tool(tags={"admin"})
+def delete_everything() -> str:
+ """Delete all data."""
+ return "Deleted"
+
+@mcp.tool(tags={"admin"})
+def reset_system() -> str:
+ """Reset the system."""
+ return "Reset"
+
+@mcp.tool
+def get_status() -> str:
+ """Get system status."""
+ return "OK"
+
+# Disable admin tools
+mcp.disable(tags={"admin"})
+
+# Clients only see: get_status
+```
+
+### Enabling Components
+
+The `enable()` method re-enables previously disabled components.
+
+```python
+# Re-enable admin tools
+mcp.enable(tags={"admin"})
+
+# Clients now see all three tools
+```
+
+## Keys and Tags
+
+Visibility filtering works with two identifiers: keys (for specific components) and tags (for groups).
+
+### Component Keys
+
+Every component has a unique key in the format `{type}:{identifier}`.
+
+| Component | Key Format | Example |
+|-----------|------------|---------|
+| Tool | `tool:{name}` | `tool:delete_everything` |
+| Resource | `resource:{uri}` | `resource:data://config` |
+| Template | `template:{uri}` | `template:file://{path}` |
+| Prompt | `prompt:{name}` | `prompt:analyze` |
+
+Use keys to target specific components.
+
+```python
+# Disable a specific tool
+mcp.disable(keys={"tool:delete_everything"})
+
+# Disable multiple specific components
+mcp.disable(keys={"tool:reset_system", "resource:data://secrets"})
+```
+
+### Tags
+
+Tags group components for bulk operations. Define tags when creating components, then filter by them.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("Server")
+
+@mcp.tool(tags={"public", "read"})
+def get_data() -> str:
+ return "data"
+
+@mcp.tool(tags={"admin", "write"})
+def set_data(value: str) -> str:
+ return f"Set: {value}"
+
+@mcp.tool(tags={"admin", "dangerous"})
+def delete_data() -> str:
+ return "Deleted"
+
+# Disable all admin tools
+mcp.disable(tags={"admin"})
+
+# Disable all dangerous tools (some overlap with admin)
+mcp.disable(tags={"dangerous"})
+```
+
+A component is disabled if it has **any** of the disabled tags. The component doesn't need all the tags; one match is enough.
+
+### Combining Keys and Tags
+
+You can specify both keys and tags in a single call. The filters combine additively.
+
+```python
+# Disable specific tools AND all dangerous-tagged components
+mcp.disable(keys={"tool:debug_info"}, tags={"dangerous"})
+```
+
+## Allowlist Mode
+
+By default, visibility filtering uses blocklist mode: everything is enabled unless explicitly disabled. The `only=True` parameter switches to allowlist mode, where **only** specified components are enabled.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("Server")
+
+@mcp.tool(tags={"safe"})
+def read_only_operation() -> str:
+ return "Read"
+
+@mcp.tool(tags={"safe"})
+def list_items() -> list[str]:
+ return ["a", "b", "c"]
+
+@mcp.tool(tags={"dangerous"})
+def delete_all() -> str:
+ return "Deleted"
+
+@mcp.tool
+def untagged_tool() -> str:
+ return "Untagged"
+
+# Only enable safe tools - everything else is disabled
+mcp.enable(tags={"safe"}, only=True)
+
+# Clients see: read_only_operation, list_items
+# Disabled: delete_all, untagged_tool
+```
+
+Allowlist mode is useful for restrictive environments where you want to explicitly opt-in components rather than opt-out.
+
+### Allowlist Behavior
+
+When you call `enable(only=True)`:
+
+1. Default visibility state switches to "disabled"
+2. Previous allowlists are cleared
+3. Only specified keys/tags become enabled
+
+```python
+# Start fresh - only enable these specific tools
+mcp.enable(keys={"tool:safe_read", "tool:safe_write"}, only=True)
+
+# Later, switch to a different allowlist
+mcp.enable(tags={"production"}, only=True)
+```
+
+### Ordering and Overrides
+
+Later `enable()` and `disable()` calls override earlier ones. This lets you create broad rules with specific exceptions.
+
+```python
+mcp.enable(tags={"api"}, only=True) # Allow all api-tagged
+mcp.disable(keys={"tool:api_admin"}) # Later disable overrides for this tool
+
+# api_admin is disabled because the later disable() overrides the allowlist
+```
+
+You can always re-enable something that was disabled by adding another `enable()` call after it.
+
+## Server vs Provider
+
+Visibility state operates at two levels: the server and individual providers.
+
+### Server-Level
+
+Server-level visibility state applies to all components from all providers. When you call `mcp.enable()` or `mcp.disable()`, you're filtering the final view that clients see.
+
+```python
+from fastmcp import FastMCP
+
+main = FastMCP("Main")
+main.mount(sub_server, namespace="api")
+
+@main.tool(tags={"internal"})
+def local_debug() -> str:
+ return "Debug"
+
+# Disable internal tools from ALL sources
+main.disable(tags={"internal"})
+```
+
+### Provider-Level
+
+Each provider can add its own visibility transforms. These run before server-level transforms, so the server can override provider-level disables.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.providers import LocalProvider
+
+# Create provider with visibility control
+admin_tools = LocalProvider()
+
+@admin_tools.tool(tags={"admin"})
+def admin_action() -> str:
+ return "Admin"
+
+@admin_tools.tool
+def regular_action() -> str:
+ return "Regular"
+
+# Disable at provider level
+admin_tools.disable(tags={"admin"})
+
+# Server can override if needed
+mcp = FastMCP("Server", providers=[admin_tools])
+mcp.enable(names={"admin_action"}) # Re-enables despite provider disable
+```
+
+Provider-level transforms are useful for setting default visibility that servers can selectively override.
+
+### Layered Transforms
+
+Provider transforms run first, then server transforms. Later transforms override earlier ones, so the server has final say.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.providers import LocalProvider
+
+provider = LocalProvider()
+
+@provider.tool(tags={"feature", "beta"})
+def new_feature() -> str:
+ return "New"
+
+# Provider enables feature-tagged
+provider.enable(tags={"feature"}, only=True)
+
+# Server disables beta-tagged (runs after provider)
+mcp = FastMCP("Server", providers=[provider])
+mcp.disable(tags={"beta"})
+
+# new_feature is disabled (server's later disable overrides provider's enable)
+```
+
+## Per-Session Visibility
+
+Server-level visibility changes affect all connected clients simultaneously. When you need different clients to see different components, use per-session visibility instead.
+
+Session visibility lets individual sessions customize their view of available components. When a tool calls `ctx.enable_components()` or `ctx.disable_components()`, those rules apply only to the current session. Other sessions continue to see the global defaults. This enables patterns like progressive disclosure, role-based access, and on-demand feature activation.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.context import Context
+
+mcp = FastMCP("Session-Aware Server")
+
+@mcp.tool(tags={"premium"})
+def premium_analysis(data: str) -> str:
+ """Advanced analysis available to premium users."""
+ return f"Premium analysis of: {data}"
+
+@mcp.tool
+async def unlock_premium(ctx: Context) -> str:
+ """Unlock premium features for this session."""
+ await ctx.enable_components(tags={"premium"})
+ return "Premium features unlocked"
+
+@mcp.tool
+async def reset_features(ctx: Context) -> str:
+ """Reset to default feature set."""
+ await ctx.reset_visibility()
+ return "Features reset to defaults"
+
+# Premium tools are disabled globally by default
+mcp.disable(tags={"premium"})
+```
+
+All sessions start with `premium_analysis` hidden. When a session calls `unlock_premium`, that session gains access to premium tools while other sessions remain unaffected. Calling `reset_features` returns the session to the global defaults.
+
+### How Session Rules Work
+
+Session rules override global transforms. When listing components, FastMCP first applies global enable/disable rules, then applies session-specific rules on top. Rules within a session accumulate, and later rules override earlier ones for the same component.
+
+```python
+@mcp.tool
+async def customize_session(ctx: Context) -> str:
+ # Enable finance tools for this session
+ await ctx.enable_components(tags={"finance"})
+
+ # Also enable admin tools
+ await ctx.enable_components(tags={"admin"})
+
+ # Later: disable a specific admin tool
+ await ctx.disable_components(names={"dangerous_admin_tool"})
+
+ return "Session customized"
+```
+
+Each call adds a rule to the session. The `dangerous_admin_tool` ends up disabled because its disable rule was added after the admin enable rule.
+
+### Filter Criteria
+
+The session visibility methods accept the same filter criteria as `server.enable()` and `server.disable()`:
+
+| Parameter | Description |
+|-----------|-------------|
+| `names` | Component names or URIs to match |
+| `keys` | Component keys (e.g., `{"tool:my_tool"}`) |
+| `tags` | Tags to match (component must have at least one) |
+| `version` | Version specification to match |
+| `components` | Component types (`{"tool"}`, `{"resource"}`, `{"prompt"}`, `{"template"}`) |
+| `match_all` | If `True`, matches all components regardless of other criteria |
+
+```python
+from fastmcp.utilities.versions import VersionSpec
+
+@mcp.tool
+async def enable_recent_tools(ctx: Context) -> str:
+ """Enable only tools from version 2.0.0 or later."""
+ await ctx.enable_components(
+ version=VersionSpec(gte="2.0.0"),
+ components={"tool"}
+ )
+ return "Recent tools enabled"
+```
+
+### Automatic Notifications
+
+When session visibility changes, FastMCP automatically sends notifications to that session. Clients receive `ToolListChangedNotification`, `ResourceListChangedNotification`, and `PromptListChangedNotification` so they can refresh their component lists. These notifications go only to the affected session.
+
+When you specify the `components` parameter, FastMCP optimizes by sending only the relevant notifications:
+
+```python
+# Only sends ToolListChangedNotification
+await ctx.enable_components(tags={"finance"}, components={"tool"})
+
+# Sends all three notifications (no components filter)
+await ctx.enable_components(tags={"finance"})
+```
+
+### Namespace Activation Pattern
+
+A common pattern organizes tools into namespaces using tag prefixes, disables them globally, then provides activation tools that unlock namespaces on demand:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.context import Context
+
+server = FastMCP("Multi-Domain Assistant")
+
+# Finance namespace
+@server.tool(tags={"namespace:finance"})
+def analyze_portfolio(symbols: list[str]) -> str:
+ return f"Analysis for: {', '.join(symbols)}"
+
+@server.tool(tags={"namespace:finance"})
+def get_market_data(symbol: str) -> dict:
+ return {"symbol": symbol, "price": 150.25}
+
+# Admin namespace
+@server.tool(tags={"namespace:admin"})
+def list_users() -> list[str]:
+ return ["alice", "bob", "charlie"]
+
+# Activation tools - always visible
+@server.tool
+async def activate_finance(ctx: Context) -> str:
+ await ctx.enable_components(tags={"namespace:finance"})
+ return "Finance tools activated"
+
+@server.tool
+async def activate_admin(ctx: Context) -> str:
+ await ctx.enable_components(tags={"namespace:admin"})
+ return "Admin tools activated"
+
+@server.tool
+async def deactivate_all(ctx: Context) -> str:
+ await ctx.reset_visibility()
+ return "All namespaces deactivated"
+
+# Disable namespace tools globally
+server.disable(tags={"namespace:finance", "namespace:admin"})
+```
+
+Sessions start seeing only the activation tools. Calling `activate_finance` reveals finance tools for that session only. Multiple namespaces can be activated independently, and `deactivate_all` returns to the initial state.
+
+### Method Reference
+
+- **`await ctx.enable_components(...) -> None`**: Enable matching components for this session
+- **`await ctx.disable_components(...) -> None`**: Disable matching components for this session
+- **`await ctx.reset_visibility() -> None`**: Clear all session rules, returning to global defaults
+
+## Client Notifications
+
+When visibility state changes, FastMCP automatically notifies connected clients. Clients supporting the MCP notification protocol receive `list_changed` events and can refresh their component lists.
+
+This happens automatically. You don't need to trigger notifications manually.
+
+```python
+# This automatically notifies clients
+mcp.disable(tags={"maintenance"})
+
+# Clients receive: tools/list_changed, resources/list_changed, etc.
+```
+
+## Filtering Logic
+
+Understanding the filtering logic helps when debugging visibility state issues.
+
+The `is_enabled()` function checks a component's internal metadata:
+
+1. If the component has `meta.fastmcp._internal.visibility = False`, it's disabled
+2. If the component has `meta.fastmcp._internal.visibility = True`, it's enabled
+3. If no visibility state is set, the component is enabled by default
+
+When multiple `enable()` and `disable()` calls are made, transforms are applied in order. **Later transforms override earlier ones**, so the last matching transform wins.
+
+## The Visibility Transform
+
+Under the hood, `enable()` and `disable()` add `Visibility` transforms to the server or provider. The `Visibility` transform marks components with visibility metadata, and the server applies the final filter after all provider and server transforms complete.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.transforms import Visibility
+
+mcp = FastMCP("Server")
+
+# Using the convenience method (recommended)
+mcp.disable(names={"secret_tool"})
+
+# Equivalent to:
+mcp.add_transform(Visibility(False, names={"secret_tool"}))
+```
+
+Server-level transforms override provider-level transforms. If a component is disabled at the provider level but enabled at the server level, the server-level `enable()` can re-enable it.
diff --git a/docs/v3/tutorials/create-mcp-server.mdx b/docs/v3/tutorials/create-mcp-server.mdx
new file mode 100644
index 000000000..de1000703
--- /dev/null
+++ b/docs/v3/tutorials/create-mcp-server.mdx
@@ -0,0 +1,198 @@
+---
+title: "How to Create an MCP Server in Python"
+sidebarTitle: "Creating an MCP Server"
+description: "A step-by-step guide to building a Model Context Protocol (MCP) server using Python and FastMCP, from basic tools to dynamic resources."
+icon: server
+---
+
+So you want to build a Model Context Protocol (MCP) server in Python. The goal is to create a service that can provide tools and data to AI models like Claude, Gemini, or others that support the protocol. While the [MCP specification](https://modelcontextprotocol.io/specification/) is powerful, implementing it from scratch involves a lot of boilerplate: handling JSON-RPC, managing session state, and correctly formatting requests and responses.
+
+This is where **FastMCP** comes in. It's a high-level framework that handles all the protocol complexities for you, letting you focus on what matters: writing the Python functions that power your server.
+
+This guide will walk you through creating a fully-featured MCP server from scratch using FastMCP.
+
+
+Every code block in this tutorial is a complete, runnable example. You can copy and paste it into a file and run it, or paste it directly into a Python REPL like IPython to try it out.
+
+
+### Prerequisites
+
+Make sure you have FastMCP installed. If not, follow the [installation guide](/getting-started/installation).
+
+```bash
+pip install fastmcp
+```
+
+
+## Step 1: Create the Basic Server
+
+Every FastMCP application starts with an instance of the `FastMCP` class. This object acts as the container for all your tools and resources.
+
+Create a new file called `my_mcp_server.py`:
+
+```python my_mcp_server.py
+from fastmcp import FastMCP
+
+# Create a server instance with a descriptive name
+mcp = FastMCP(name="My First MCP Server")
+```
+
+That's it! You have a valid (though empty) MCP server. Now, let's add some functionality.
+
+## Step 2: Add a Tool
+
+Tools are functions that an LLM can execute. Let's create a simple tool that adds two numbers.
+
+To do this, simply write a standard Python function and decorate it with `@mcp.tool`.
+
+```python my_mcp_server.py {5-8}
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="My First MCP Server")
+
+@mcp.tool
+def add(a: int, b: int) -> int:
+ """Adds two integer numbers together."""
+ return a + b
+```
+
+FastMCP automatically handles the rest:
+- **Tool Name:** It uses the function name (`add`) as the tool's name.
+- **Description:** It uses the function's docstring as the tool's description for the LLM.
+- **Schema:** It inspects the type hints (`a: int`, `b: int`) to generate a JSON schema for the inputs.
+
+This is the core philosophy of FastMCP: **write Python, not protocol boilerplate.**
+
+## Step 3: Expose Data with Resources
+
+Resources provide read-only data to the LLM. You can define a resource by decorating a function with `@mcp.resource`, providing a unique URI.
+
+Let's expose a simple configuration dictionary as a resource.
+
+```python my_mcp_server.py {10-13}
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="My First MCP Server")
+
+@mcp.tool
+def add(a: int, b: int) -> int:
+ """Adds two integer numbers together."""
+ return a + b
+
+@mcp.resource("resource://config")
+def get_config() -> dict:
+ """Provides the application's configuration."""
+ return {"version": "1.0", "author": "MyTeam"}
+```
+
+When a client requests the URI `resource://config`, FastMCP will execute the `get_config` function and return its output (serialized as JSON) to the client. The function is only called when the resource is requested, enabling lazy-loading of data.
+
+## Step 4: Generate Dynamic Content with Resource Templates
+
+Sometimes, you need to generate resources based on parameters. This is what **Resource Templates** are for. You define them using the same `@mcp.resource` decorator but with placeholders in the URI.
+
+Let's create a template that provides a personalized greeting.
+
+```python my_mcp_server.py {15-17}
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="My First MCP Server")
+
+@mcp.tool
+def add(a: int, b: int) -> int:
+ """Adds two integer numbers together."""
+ return a + b
+
+@mcp.resource("resource://config")
+def get_config() -> dict:
+ """Provides the application's configuration."""
+ return {"version": "1.0", "author": "MyTeam"}
+
+@mcp.resource("greetings://{name}")
+def personalized_greeting(name: str) -> str:
+ """Generates a personalized greeting for the given name."""
+ return f"Hello, {name}! Welcome to the MCP server."
+```
+
+Now, clients can request dynamic URIs:
+- `greetings://Ford` will call `personalized_greeting(name="Ford")`.
+- `greetings://Marvin` will call `personalized_greeting(name="Marvin")`.
+
+FastMCP automatically maps the `{name}` placeholder in the URI to the `name` parameter in your function.
+
+## Step 5: Run the Server
+
+To make your server executable, add a `__main__` block to your script that calls `mcp.run()`.
+
+```python my_mcp_server.py {19-20}
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="My First MCP Server")
+
+@mcp.tool
+def add(a: int, b: int) -> int:
+ """Adds two integer numbers together."""
+ return a + b
+
+@mcp.resource("resource://config")
+def get_config() -> dict:
+ """Provides the application's configuration."""
+ return {"version": "1.0", "author": "MyTeam"}
+
+@mcp.resource("greetings://{name}")
+def personalized_greeting(name: str) -> str:
+ """Generates a personalized greeting for the given name."""
+ return f"Hello, {name}! Welcome to the MCP server."
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+Now you can run your server from the command line:
+```bash
+python my_mcp_server.py
+```
+This starts the server using the default **STDIO transport**, which is how clients like Claude Desktop communicate with local servers. To learn about other transports, like HTTP, see the [Running Your Server](/deployment/running-server) guide.
+
+## The Complete Server
+
+Here is the full code for `my_mcp_server.py` (click to expand):
+
+```python my_mcp_server.py [expandable]
+from fastmcp import FastMCP
+
+# 1. Create the server
+mcp = FastMCP(name="My First MCP Server")
+
+# 2. Add a tool
+@mcp.tool
+def add(a: int, b: int) -> int:
+ """Adds two integer numbers together."""
+ return a + b
+
+# 3. Add a static resource
+@mcp.resource("resource://config")
+def get_config() -> dict:
+ """Provides the application's configuration."""
+ return {"version": "1.0", "author": "MyTeam"}
+
+# 4. Add a resource template for dynamic content
+@mcp.resource("greetings://{name}")
+def personalized_greeting(name: str) -> str:
+ """Generates a personalized greeting for the given name."""
+ return f"Hello, {name}! Welcome to the MCP server."
+
+# 5. Make the server runnable
+if __name__ == "__main__":
+ mcp.run()
+```
+
+## Next Steps
+
+You've successfully built an MCP server! From here, you can explore more advanced topics:
+
+- [**Tools in Depth**](/servers/tools): Learn about asynchronous tools, error handling, and custom return types.
+- [**Resources & Templates**](/servers/resources): Discover different resource types, including files and HTTP endpoints.
+- [**Prompts**](/servers/prompts): Create reusable prompt templates for your LLM.
+- [**Running Your Server**](/deployment/running-server): Deploy your server with different transports like HTTP.
+
diff --git a/docs/v3/tutorials/mcp.mdx b/docs/v3/tutorials/mcp.mdx
new file mode 100644
index 000000000..fd3995fff
--- /dev/null
+++ b/docs/v3/tutorials/mcp.mdx
@@ -0,0 +1,120 @@
+---
+title: "What is the Model Context Protocol (MCP)?"
+sidebarTitle: "What is MCP?"
+description: "An introduction to the core concepts of the Model Context Protocol (MCP), explaining what it is, why it's useful, and how it works."
+icon: "diagram-project"
+---
+
+The Model Context Protocol (MCP) is an open standard designed to solve a fundamental problem in AI development: how can Large Language Models (LLMs) reliably and securely interact with external tools, data, and services?
+
+It's the **bridge between the probabilistic, non-deterministic world of AI and the deterministic, reliable world of your code and data.**
+
+While you could build a custom REST API for your LLM, MCP provides a specialized, standardized "port" for AI-native communication. Think of it as **USB-C for AI**: a single, well-defined interface for connecting any compliant LLM to any compliant tool or data source.
+
+This guide provides a high-level overview of the protocol itself. We'll use **FastMCP**, the leading Python framework for MCP, to illustrate the concepts with simple code examples.
+
+## Why Do We Need a Protocol?
+
+With countless APIs already in existence, the most common question is: "Why do we need another one?"
+
+The answer lies in **standardization**. The AI ecosystem is fragmented. Every model provider has its own way of defining and calling tools. MCP's goal is to create a common language that offers several key advantages:
+
+1. **Interoperability:** Build one MCP server, and it can be used by any MCP-compliant client (Claude, Gemini, OpenAI, custom agents, etc.) without custom integration code. This is the protocol's most important promise.
+2. **Discoverability:** Clients can dynamically ask a server what it's capable of at runtime. They receive a structured, machine-readable "menu" of tools and resources.
+3. **Security & Safety:** MCP provides a clear, sandboxed boundary. An LLM can't execute arbitrary code on your server; it can only *request* to run the specific, typed, and validated functions you explicitly expose.
+4. **Composability:** You can build small, specialized MCP servers and combine them to create powerful, complex applications.
+
+## Core MCP Components
+
+An MCP server exposes its capabilities through three primary components: Tools, Resources, and Prompts.
+
+### Tools: Executable Actions
+
+Tools are functions that the LLM can ask the server to execute. They are the action-oriented part of MCP.
+
+In the spirit of a REST API, you can think of **Tools as being like `POST` requests.** They are used to *perform an action*, *change state*, or *trigger a side effect*, like sending an email, adding a user to a database, or making a calculation.
+
+With FastMCP, creating a tool is as simple as decorating a Python function.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP()
+
+# This function is now an MCP tool named "get_weather"
+@mcp.tool
+def get_weather(city: str) -> dict:
+ """Gets the current weather for a specific city."""
+ # In a real app, this would call a weather API
+ return {"city": city, "temperature": "72F", "forecast": "Sunny"}
+```
+
+[**Learn more about Tools →**](/servers/tools)
+
+### Resources: Read-Only Data
+
+Resources are data sources that the LLM can read. They are used to load information into the LLM's context, providing it with knowledge it doesn't have from its training data.
+
+Following the REST API analogy, **Resources are like `GET` requests.** Their purpose is to *retrieve information* idempotently, ideally without causing side effects. A resource can be anything from a static text file to a dynamic piece of data from a database. Each resource is identified by a unique URI.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP()
+
+# This function provides a resource at the URI "system://status"
+@mcp.resource("system://status")
+def get_system_status() -> dict:
+ """Returns the current operational status of the service."""
+ return {"status": "all systems normal"}
+```
+
+#### Resource Templates
+
+You can also create **Resource Templates** for dynamic data. A client could request `users://42/profile` to get the profile for a specific user.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP()
+
+# This template provides user data for any given user ID
+@mcp.resource("users://{user_id}/profile")
+def get_user_profile(user_id: str) -> dict:
+ """Returns the profile for a specific user."""
+ # Fetch user from a database...
+ return {"id": user_id, "name": "Zaphod Beeblebrox"}
+```
+
+[**Learn more about Resources & Templates →**](/servers/resources)
+
+### Prompts: Reusable Instructions
+
+Prompts are reusable, parameterized message templates. They provide a way to define consistent, structured instructions that a client can request to guide the LLM's behavior for a specific task.
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP()
+
+@mcp.prompt
+def summarize_text(text_to_summarize: str) -> str:
+ """Creates a prompt asking the LLM to summarize a piece of text."""
+ return f"""
+ Please provide a concise, one-paragraph summary of the following text:
+
+ {text_to_summarize}
+ """
+```
+
+[**Learn more about Prompts →**](/servers/prompts)
+
+## Advanced Capabilities
+
+Beyond the core components, MCP also supports more advanced interaction patterns, such as a server requesting that the *client's* LLM generate a completion (known as **sampling**), or a server sending asynchronous **notifications** to a client. These features enable more complex, bidirectional workflows and are fully supported by FastMCP.
+
+## Next Steps
+
+Now that you understand the core concepts of the Model Context Protocol, you're ready to start building. The best place to begin is our step-by-step tutorial.
+
+[**Tutorial: How to Create an MCP Server in Python →**](/tutorials/create-mcp-server)
diff --git a/docs/v3/tutorials/rest-api.mdx b/docs/v3/tutorials/rest-api.mdx
new file mode 100644
index 000000000..90872c950
--- /dev/null
+++ b/docs/v3/tutorials/rest-api.mdx
@@ -0,0 +1,203 @@
+---
+title: "How to Connect an LLM to a REST API"
+sidebarTitle: "Connect LLMs to REST APIs"
+description: "A step-by-step guide to making any REST API with an OpenAPI spec available to LLMs using FastMCP."
+icon: "plug"
+---
+
+You've built a powerful REST API, and now you want your LLM to be able to use it. Manually writing a wrapper function for every single endpoint is tedious, error-prone, and hard to maintain.
+
+This is where **FastMCP** shines. If your API has an OpenAPI (or Swagger) specification, FastMCP can automatically convert your entire API into a fully-featured MCP server, making every endpoint available as a secure, typed tool for your AI model.
+
+This guide will walk you through converting a public REST API into an MCP server in just a few lines of code.
+
+
+Every code block in this tutorial is a complete, runnable example. You can copy and paste it into a file and run it, or paste it directly into a Python REPL like IPython to try it out.
+
+
+### Prerequisites
+
+Make sure you have FastMCP installed. If not, follow the [installation guide](/getting-started/installation).
+
+```bash
+pip install fastmcp
+```
+
+## Step 1: Choose a Target API
+
+For this tutorial, we'll use the [JSONPlaceholder API](https://jsonplaceholder.typicode.com/), a free, fake online REST API for testing and prototyping. It's perfect because it's simple and has a public OpenAPI specification.
+
+- **API Base URL:** `https://jsonplaceholder.typicode.com`
+- **OpenAPI Spec URL:** We'll use a community-provided spec for it.
+
+## Step 2: Create the MCP Server
+
+Now for the magic. We'll use `FastMCP.from_openapi`. This method takes an `httpx.AsyncClient` configured for your API and its OpenAPI specification, and automatically converts **every endpoint** into a callable MCP `Tool`.
+
+
+Learn more about working with OpenAPI specs in the [OpenAPI integration docs](/integrations/openapi).
+
+
+
+For this tutorial, we'll use a simplified OpenAPI spec directly in the code. In a real project, you would typically load the spec from a URL or local file.
+
+
+Create a file named `api_server.py`:
+
+```python api_server.py {31-35}
+import httpx
+from fastmcp import FastMCP
+
+# Create an HTTP client for the target API
+client = httpx.AsyncClient(base_url="https://jsonplaceholder.typicode.com")
+
+# Define a simplified OpenAPI spec for JSONPlaceholder
+openapi_spec = {
+ "openapi": "3.0.0",
+ "info": {"title": "JSONPlaceholder API", "version": "1.0"},
+ "paths": {
+ "/users": {
+ "get": {
+ "summary": "Get all users",
+ "operationId": "get_users",
+ "responses": {"200": {"description": "A list of users."}}
+ }
+ },
+ "/users/{id}": {
+ "get": {
+ "summary": "Get a user by ID",
+ "operationId": "get_user_by_id",
+ "parameters": [{"name": "id", "in": "path", "required": True, "schema": {"type": "integer"}}],
+ "responses": {"200": {"description": "A single user."}}
+ }
+ }
+ }
+}
+
+# Create the MCP server from the OpenAPI spec
+mcp = FastMCP.from_openapi(
+ openapi_spec=openapi_spec,
+ client=client,
+ name="JSONPlaceholder MCP Server"
+)
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
+```
+
+And that's it! With just a few lines of code, you've created an MCP server that exposes the entire JSONPlaceholder API as a collection of tools.
+
+## Step 3: Test the Generated Server
+
+Let's verify that our new MCP server works. We can use the `fastmcp.Client` to connect to it and inspect its tools.
+
+
+Learn more about the FastMCP client in the [client docs](/clients/client).
+
+
+Create a separate file, `api_client.py`:
+
+```python api_client.py {2, 6, 9, 16}
+import asyncio
+from fastmcp import Client
+
+async def main():
+ # Connect to the MCP server we just created
+ async with Client("http://127.0.0.1:8000/mcp") as client:
+
+ # List the tools that were automatically generated
+ tools = await client.list_tools()
+ print("Generated Tools:")
+ for tool in tools:
+ print(f"- {tool.name}")
+
+ # Call one of the generated tools
+ print("\n\nCalling tool 'get_user_by_id'...")
+ user = await client.call_tool("get_user_by_id", {"id": 1})
+ print(f"Result:\n{user.data}")
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+First, run your server:
+```bash
+python api_server.py
+```
+
+Then, in another terminal, run the client:
+```bash
+python api_client.py
+```
+
+You should see a list of generated tools (`get_users`, `get_user_by_id`) and the result of calling the `get_user_by_id` tool, which fetches data from the live JSONPlaceholder API.
+
+
+
+
+## Step 4: Customizing Route Maps
+
+By default, FastMCP converts every API endpoint into an MCP `Tool`. This ensures maximum compatibility with contemporary LLM clients, many of which **only support the `tools` part of the MCP specification.**
+
+However, for clients that support the full MCP spec, representing `GET` requests as `Resources` can be more semantically correct and efficient.
+
+FastMCP allows users to customize this behavior using the concept of "route maps". A `RouteMap` is a mapping of an API route to an MCP type. FastMCP checks each API route against your custom maps in order. If a route matches a map, it's converted to the specified `mcp_type`. Any route that doesn't match your custom maps will fall back to the default behavior (becoming a `Tool`).
+
+
+Learn more about route maps in the [OpenAPI integration docs](/integrations/openapi#route-mapping).
+
+
+Here’s how you can add custom route maps to turn `GET` requests into `Resources` and `ResourceTemplates` (if they have path parameters):
+
+```python api_server_with_resources.py {3, 37-42}
+import httpx
+from fastmcp import FastMCP
+from fastmcp.server.providers.openapi import RouteMap, MCPType
+
+
+# Create an HTTP client for the target API
+client = httpx.AsyncClient(base_url="https://jsonplaceholder.typicode.com")
+
+# Define a simplified OpenAPI spec for JSONPlaceholder
+openapi_spec = {
+ "openapi": "3.0.0",
+ "info": {"title": "JSONPlaceholder API", "version": "1.0"},
+ "paths": {
+ "/users": {
+ "get": {
+ "summary": "Get all users",
+ "operationId": "get_users",
+ "responses": {"200": {"description": "A list of users."}}
+ }
+ },
+ "/users/{id}": {
+ "get": {
+ "summary": "Get a user by ID",
+ "operationId": "get_user_by_id",
+ "parameters": [{"name": "id", "in": "path", "required": True, "schema": {"type": "integer"}}],
+ "responses": {"200": {"description": "A single user."}}
+ }
+ }
+ }
+}
+
+# Create the MCP server with custom route mapping
+mcp = FastMCP.from_openapi(
+ openapi_spec=openapi_spec,
+ client=client,
+ name="JSONPlaceholder MCP Server",
+ route_maps=[
+ # Map GET requests with path parameters (e.g., /users/{id}) to ResourceTemplate
+ RouteMap(methods=["GET"], pattern=r".*\{.*\}.*", mcp_type=MCPType.RESOURCE_TEMPLATE),
+ # Map all other GET requests to Resource
+ RouteMap(methods=["GET"], mcp_type=MCPType.RESOURCE),
+ ]
+)
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
+```
+With this configuration:
+- `GET /users/{id}` becomes a `ResourceTemplate`.
+- `GET /users` becomes a `Resource`.
+- Any `POST`, `PUT`, etc. endpoints would still become `Tools` by default.
\ No newline at end of file
diff --git a/docs/v3/updates.mdx b/docs/v3/updates.mdx
new file mode 100644
index 000000000..0faf12da9
--- /dev/null
+++ b/docs/v3/updates.mdx
@@ -0,0 +1,743 @@
+---
+title: "FastMCP Updates"
+sidebarTitle: "Updates"
+icon: "sparkles"
+tag: NEW
+---
+
+
+
+A compatibility patch for HTTP deployments affected by the 3.4.3 Host/Origin guard defaults. FastMCP 3.x now keeps strict Host and Origin validation available for explicit opt-in deployments without rejecting existing ASGI, serverless, and reverse-proxy traffic by default.
+
+🌐 **HTTP compatibility restored** — existing hosted deployments keep accepting their public Host headers unless strict host/origin protection is configured.
+
+🔐 **Guard remains available** — deployments that know their public host and browser origins can still enable strict validation with `host_origin_protection=True`, `allowed_hosts`, and `allowed_origins`.
+
+🤗 **Hugging Face auth** — new OAuth provider support covers public and private Hugging Face apps, with docs and examples for PKCE, Dynamic Client Registration, and CIMD.
+
+
+
+
+
+A month of SSRF and OAuth hardening lands in one patch. NAT64, 6to4, Teredo, and ISATAP transition addresses can no longer smuggle private IPv4 targets past the SSRF allow-list, Streamable HTTP validates Host and Origin before session handling to block DNS rebinding, and OAuth redirect validation rejects unsafe schemes and unregistered DCR redirect URIs.
+
+🛡️ **SSRF allow-list hardening** — every IPv6 transition form (NAT64, 6to4, Teredo, ISATAP) now unwraps to its embedded IPv4 target and gets checked against the same policy.
+
+🌐 **DNS rebinding protection** — Streamable HTTP validates `Host` and browser `Origin` before session handling, closing a path to localhost-bound unauthenticated servers.
+
+🔐 **Stricter OAuth redirects** — unsafe redirect schemes are rejected before registration, and DCR clients are bound to the redirect URIs they registered.
+
+🧵 **Reliability fixes** — proxy session teardown races, discriminator-tag handling in JSON schema conversion, and several smaller fixes across middleware and resource templates.
+
+
+
+
+
+A compatibility patch. `JWTVerifier` now accepts JWTs carrying private, non-critical JWS header parameters (like Clerk's `cat`) instead of rejecting them before signature and claim validation, while unsupported critical headers are still rejected.
+
+
+
+
+
+A security patch. FastMCP now floors Starlette at `>=1.0.1`, so installs can no longer resolve to a version affected by CVE-2026-48710 — previously the dependency was only constrained transitively through `mcp`. OAuthProxy also logs refresh-token cache misses instead of failing silently.
+
+
+
+
+
+The remote release. `fastmcp-remote` is a standalone bridge that connects stdio-only MCP hosts to servers hosted over HTTP, with OAuth enabled automatically for HTTPS. The proxy layer underneath it is hardened so bridges fail loudly on a missing or misconfigured upstream, and FastMCP-issued tokens can now outlive short-lived upstream tokens to survive long idle periods.
+
+🌉 **fastmcp-remote** — `uvx fastmcp-remote https://example.com/mcp` bridges a remote server back to a stdio-only host.
+
+🔌 **Bridges fail loudly** — proxies forward `initialize` upstream, so a missing backend or wrong URL fails the handshake instead of returning an empty-but-connected proxy.
+
+🔐 **Longer-lived tokens** — `fastmcp_access_token_expiry_seconds` decouples the client-facing token lifetime from a short upstream `expires_in`.
+
+⚠️ **Returnable tool errors** — `ToolResult(..., is_error=True)` hands back rich errors the model can act on instead of only raising.
+
+
+
+
+
+Hotfix for the 3.3 packaging split: standalone component imports like `from fastmcp.tools import tool` no longer pull in the server stack or trip a circular import. Component-level auth and task primitives moved to lightweight utility modules, with the old import paths preserved as compatibility re-exports.
+
+
+
+
+
+The `fastmcp-slim` release. A dependency-light distribution that ships FastMCP's client and transport layer without Starlette, Uvicorn, or the server stack — the import namespace is unchanged. It also closes out a backlog of OAuth proxy hardening, MCP-compliant OTEL instrumentation, and auth additions.
+
+🪶 **fastmcp-slim** — install the client without the server footprint for CI, agents, and library dependencies.
+
+🔐 **OAuth proxy hardening** — silent-consent AS-in-the-middle guard, dot-segment redirect rejection, and per-token response cache partitioning.
+
+🔑 **Auth additions** — `AzureB2CProvider` user flows and a public `update_scopes()` API on `OAuthProxy`.
+
+🧵 **Thread affinity** — `@mcp.tool(run_in_thread=False)` for tools bound to a specific thread.
+
+
+
+
+
+A grab bag of fixes and hardening. Background tasks are now scoped to the authorization context instead of the MCP session — a breaking change for anyone relying on session-scoped semantics — and parameter descriptions are extracted from docstrings automatically.
+
+🔐 **Security** — `FileUpload` validates decoded base64 size, the proxy stops forwarding inbound headers to unrelated servers, and AuthKit binds token audience per RFC 8707.
+
+🔑 **Keycloak** — new OAuth provider for enterprise auth and local dev.
+
+
+
+
+
+Pins `fakeredis<2.35.0` in the `tasks` extra: a 2.35.0 rename broke pydocket's `memory://` backend and made `fastmcp[tasks]` installs fail at startup with an `ImportError`.
+
+
+
+
+
+Fixes the Azure audience regression from 3.2.1 — both the bare client ID GUID and a custom `identifier_uri` are now accepted as the token audience.
+
+
+
+
+
+A patch focused on auth-provider audience validation: Cognito validates on `client_id`, Azure honors `identifier_uri`, and consent cookies are LRU-capped to avoid header overflow. Also fixes OpenAPI 3.0 `nullable` fields leaking into tool input schemas and server-variable substitution in base URLs.
+
+
+
+
+
+The Apps release. Your tools can return interactive UIs — charts, dashboards, forms, maps — rendered right inside the conversation.
+
+🎨 **FastMCPApp** — separate the tools the LLM sees (`@app.ui()`) from the backend tools the UI calls (`@app.tool()`), built on Prefab.
+
+🧩 **Built-in providers** — FileUpload, Approval, Choice, FormInput, and GenerativeUI.
+
+🖥️ **Dev server** — `fastmcp dev apps` previews app tools in the browser with an MCP message inspector.
+
+🔒 **Security pass** — SSRF/path-traversal prevention, JWT algorithm restrictions, OAuth scope enforcement, and CSRF fixes.
+
+
+
+
+
+Pins `pydantic-monty<0.0.8` to fix a breaking change in Monty that affects code mode.
+
+
+
+
+
+The Code Mode release. Instead of loading the entire tool catalog into context, `CodeMode` gives LLMs meta-tools: search for relevant tools on demand, inspect their schemas, then write Python that chains `call_tool()` calls in a sandbox. Also ships search transforms, early Prefab Apps integration, `MultiAuth` for composing multiple token verification sources, and PropelAuth support.
+
+
+
+
+
+Two community-contributed fixes: auth headers from MCP transport no longer leak through to downstream OpenAPI APIs, and background task workers now correctly receive the originating request ID. Plus a new docs example for context-aware tool factories.
+
+
+
+
+
+First patch after 3.0 — mostly smoothing out rough edges discovered in the wild. The big ones: middleware state that wasn't surviving the trip to tool handlers now does, `Tool.from_tool()` accepts callables again, OpenAPI schemas with circular references no longer crash discovery, and decorator overloads now return the correct types in function mode.
+
+🔐 **OIDC `verify_id_token`** — New option for providers that issue opaque access tokens but standard JWT id_tokens. Verifies identity via the id_token while using the access_token for upstream API calls.
+
+🐞 **11 bug fixes** — State serialization, future annotations with `Context`/`Depends`, OpenAI handler deprecation warnings, type checker compatibility, and more.
+
+
+
+
+
+FastMCP 3.0 is stable. Two betas, two release candidates, 21 new contributors, and more than 100,000 pre-release installs later — the architecture held up, the upgrade path was smooth, and we're shipping it.
+
+The surface API is largely unchanged — `@mcp.tool()` still works exactly as before. What changed is everything underneath: a provider/transform architecture that makes FastMCP extensible, observable, and composable in ways v2 couldn't support.
+
+🔌 **Build servers from anything** — `FileSystemProvider`, `OpenAPIProvider`, `ProxyProvider`, `SkillsProvider`, and composable transforms that rename, namespace, filter, version, and secure components as they flow to clients.
+
+🔐 **Ship to production** — Component versioning, granular authorization with async auth checks, CIMD, Static Client Registration, Azure OBO, OpenTelemetry tracing, and background tasks with distributed Redis notification.
+
+💾 **Adapt per session** — Session state persists across requests, and `ctx.enable_components()` / `ctx.disable_components()` let servers adapt dynamically per client.
+
+⚡ **Develop faster** — `--reload`, standalone decorators, automatic threadpool dispatch, tool timeouts, pagination, and concurrent tool execution.
+
+🖥️ **CLI** — `fastmcp list`, `fastmcp call`, `fastmcp discover`, `fastmcp generate-cli`, and `fastmcp install` for Claude Desktop, Cursor, and Goose.
+
+
+
+
+
+FastMCP 3 RC1 means we believe the API is stable. Beta 2 drew a wave of real-world adoption — production deployments, migration reports, integration testing — and the feedback overwhelmingly confirmed that the architecture works. This release closes gaps that surfaced under load: auth flows that needed to be async, background tasks that needed reliable notification delivery, and APIs still carrying beta-era naming. If nothing unexpected surfaces, this is what 3.0.0 looks like.
+
+🚨 **Breaking Changes** — The `ui=` parameter is now `app=` with a unified `AppConfig` class, and 16 `FastMCP()` constructor kwargs have been removed after months of deprecation warnings.
+
+🔐 **Auth Improvements** — Async `auth=` checks, Static Client Registration for servers without DCR, and declarative Azure OBO flows via dependency injection.
+
+⚡ **Concurrent Sampling** — `context.sample()` can now execute multiple tool calls in parallel with `tool_concurrency=0`.
+
+📡 **Background Task Notifications** — A distributed Redis queue replaces polling for progress updates and elicitation relay.
+
+✅ **OpenAPI Output Validation** — `validate_output=False` disables strict schema checking for imperfect backend APIs.
+
+
+
+
+
+Beta 2 reflects the huge number of people that kicked the tires on Beta 1. Seven new contributors landed changes, and early migration reports went smoother than expected. Most of Beta 2 is refinement — fixing what people found, filling gaps from real usage, hardening edges — but a few new features landed along the way.
+
+🖥️ **Client CLI** — `fastmcp list`, `fastmcp call`, `fastmcp discover`, and `fastmcp generate-cli` turn any MCP server into something you can poke at from a terminal.
+
+🔐 **CIMD** (Client ID Metadata Documents) adds an alternative to Dynamic Client Registration for OAuth.
+
+📱 **MCP Apps** — Spec-level compliance for the MCP Apps extension with `ui://` resource scheme and typed UI metadata.
+
+⏳ **Background Task Context** — `Context` now works transparently in Docket workers with Redis-based coordination.
+
+🛡️ **ResponseLimitingMiddleware** caps tool response sizes with UTF-8-safe truncation.
+
+🪿 **Goose Integration** — `fastmcp install goose` for one-command server installation into Goose.
+
+
+
+
+
+FastMCP 3.0 rebuilds the framework around three primitives: components, providers, and transforms. Providers source components dynamically—from decorators, filesystems, OpenAPI specs, remote servers, or anywhere else. Transforms modify components as they flow to clients. The features that required specialized subsystems in v2 now compose naturally from these building blocks.
+
+🔌 **Provider Architecture** unifies how components are sourced with `FileSystemProvider`, `SkillsProvider`, `OpenAPIProvider`, and `ProxyProvider`.
+
+🔄 **Transforms** add middleware for components—namespace, rename, filter by version, control visibility.
+
+📋 **Component Versioning** lets you register multiple versions of the same tool with automatic highest-version selection.
+
+💾 **Session-Scoped State** persists across requests, with per-session visibility control.
+
+⚡ **DX Improvements** include `--reload` for development, automatic threadpool dispatch, tool timeouts, pagination, and OpenTelemetry tracing.
+
+
+
+
+
+A 2.x backport of the fakeredis pin: fakeredis 2.35.0 renamed a connection class that pydocket's `memory://` backend relied on, crashing `fastmcp[tasks]` installs at startup. Caps `fakeredis<2.35.0` on the 2.x line.
+
+
+
+
+
+v2.14.4 backported `dereference_refs()` but never wired it into the tool schema pipeline — `$ref` and `$defs` were still sent to MCP clients. Now fixed: schemas are fully inlined before reaching clients.
+
+
+
+
+
+Fixes a memory leak in the memory:// docket broker where cancelled tasks accumulated instead of being cleaned up. Bumps pydocket to ≥0.17.2.
+
+
+
+
+
+Fixes a fresh install bug where the packaging library was missing as a direct dependency, plus backports $ref dereferencing in tool schemas and a task capabilities location fix.
+
+
+
+
+
+Sometimes five seconds just isn't enough. This release fixes an HTTP transport bug that was cutting connections short, along with OAuth and Redis fixes, better ASGI support, and CLI update notifications so you never miss a beat.
+
+⏱️ **HTTP transport timeout fix** restores MCP's 30-second default connect timeout, which was incorrectly defaulting to 5 seconds.
+
+🔧 **Infrastructure fixes** including OAuth token storage TTL, Redis key prefixing for ACL isolation, and ContextVar propagation for ASGI-mounted servers with background tasks.
+
+
+
+
+
+A wave of community contributions arrives safely in the 2.x line. Important backports from 3.0 improve OpenAPI 3.1 compatibility, MCP spec compliance for output schemas and elicitation, and correct a subtle base_url fallback issue.
+
+🔧 **OpenAPI 3.1 support** fixes version detection to properly handle 3.1 specs alongside 3.0.
+
+📋 **MCP spec compliance** for root-level `$ref` resolution in output schemas and titled enum elicitation schemas.
+
+
+
+
+
+FastMCP 2.14.1 introduces sampling with tools (SEP-1577), enabling servers to pass tools to `ctx.sample()` for agentic workflows where the LLM can automatically execute tool calls in a loop.
+
+🤖 **Sampling with tools** lets servers leverage client LLM capabilities for multi-step agentic workflows. The new `ctx.sample_step()` method provides single LLM calls with tool inspection, while `result_type` enables structured outputs via validated Pydantic models.
+
+🔧 **AnthropicSamplingHandler** joins the existing OpenAI handler, and both are now promoted from experimental to production-ready status with a unified API.
+
+
+
+
+
+FastMCP 2.14 begins adopting the MCP 2025-11-25 specification, introducing protocol-native background tasks that enable long-running operations to report progress without blocking clients.
+
+⏳ **Background Tasks (SEP-1686)** let you add `task=True` to any async tool decorator. Powered by [Docket](https://github.com/chrisguidry/docket) for enterprise task scheduling—in-memory backends work out-of-the-box, Redis enables persistence and horizontal scaling.
+
+🔧 **OpenAPI Parser Promoted** from experimental to standard with improved performance through single-pass schema processing.
+
+📋 **MCP Spec Updates** including SSE polling (SEP-1699), multi-select elicitation (SEP-1330), and tool name validation (SEP-986). Also removes deprecated APIs accumulated across 2.x.
+
+
+
+
+
+Pins `mcp<1.23` as a precaution due to MCP SDK changes related to the 11/25/25 protocol update that break certain FastMCP patches and workarounds. FastMCP 2.14 introduces proper support for the updated protocol.
+
+
+
+
+
+Polishes the authentication stack with improvements to token refresh, scope handling, and multi-instance deployments.
+
+🎮 **Discord OAuth provider** added as a built-in authentication option.
+
+🔄 **Token refresh fixes** for Azure and Google providers, plus OAuth proxy improvements for multi-instance deployments.
+
+🎨 **Icon support** added to proxy classes for richer UX.
+
+
+
+
+
+Introduces meta parameter support for `ToolResult`, enabling tools to return supplementary metadata alongside results for patterns like OpenAI's Apps SDK.
+
+🏷️ **Meta parameters** let tools return supplementary metadata alongside results.
+
+🔐 **New auth providers** for OCI and Supabase, plus custom token verifiers with DebugTokenVerifier for development.
+
+🔒 **Security fixes** for CVE-2025-61920 and safer Cursor deeplink URL validation on Windows.
+
+
+
+
+
+FastMCP 2.13 "Cache Me If You Can" represents a fundamental maturation of the framework. After months of community feedback on authentication and state management, this release delivers the infrastructure FastMCP needs to handle production workloads: persistent storage, response caching, and pragmatic OAuth improvements that reflect real-world deployment challenges.
+
+💾 **Pluggable storage backends** bring persistent state to FastMCP servers. Built on [py-key-value-aio](https://github.com/strawgate/py-key-value), a new library from FastMCP maintainer Bill Easton ([@strawgate](https://github.com/strawgate)), the storage layer provides encrypted disk storage by default, platform-aware token management, and a simple key-value interface for application state. We're excited to bring this elegantly designed library into the FastMCP ecosystem - it's both powerful and remarkably easy to use, including wrappers to add encryption, TTLs, caching, and more to backends ranging from Elasticsearch, Redis, DynamoDB, filesystem, in-memory, and more!
+
+🔐 **OAuth maturity** brings months of production learnings into the framework. The new consent screen prevents confused deputy and authorization bypass attacks discovered in earlier versions, while the OAuth proxy now issues its own tokens with automatic key derivation. RFC 7662 token introspection support enables enterprise auth flows, and path prefix mounting enables OAuth-protected servers to integrate into existing web applications. FastMCP now supports out-of-the-box authentication with [WorkOS](https://gofastmcp.com/integrations/workos) and [AuthKit](https://gofastmcp.com/integrations/authkit), [GitHub](https://gofastmcp.com/integrations/github), [Google](https://gofastmcp.com/integrations/google), [Azure](https://gofastmcp.com/integrations/azure) (Entra ID), [AWS Cognito](https://gofastmcp.com/integrations/aws-cognito), [Auth0](https://gofastmcp.com/integrations/auth0), [Descope](https://gofastmcp.com/integrations/descope), [Scalekit](https://gofastmcp.com/integrations/scalekit), [JWTs](https://gofastmcp.com/servers/auth/token-verification#jwt-token-verification), and [RFC 7662 token introspection](https://gofastmcp.com/servers/auth/token-verification#token-introspection-protocol).
+
+⚡ **Response Caching Middleware** dramatically improves performance for expensive operations, while **Server lifespans** provide proper initialization and cleanup hooks that run once per server instance instead of per client session.
+
+✨ **Developer experience improvements** include Pydantic input validation, icon support, RFC 6570 query parameters for resource templates, improved Context API methods, and async file/directory resources.
+
+
+
+
+
+Pins MCP SDK version below 1.17 to ensure the `.well-known` payload appears in the expected location when using FastMCP auth providers with composite applications.
+
+
+
+
+
+FastMCP 2.12.4 adds comprehensive OIDC support and expands authentication options with AWS Cognito and Descope providers. The release also includes improvements to logging middleware, URL handling for nested resources, persistent OAuth client registration storage, and various fixes to the experimental OpenAPI parser.
+
+🔐 **OIDC Configuration** brings native support for OpenID Connect, enabling seamless integration with enterprise identity providers.
+
+🏢 **Enterprise Authentication** expands with AWS Cognito and Descope providers, broadening the authentication ecosystem.
+
+🛠️ **Improved Reliability** through enhanced URL handling, persistent OAuth storage, and numerous parser fixes based on community feedback.
+
+
+
+
+
+FastMCP 2.12.3 focuses on performance and developer experience improvements. This release includes optimized auth provider imports that reduce server startup time, enhanced OIDC authentication flows, and automatic inline snapshot creation for testing.
+
+
+
+
+
+Hotfix for streamable-http transport validation in fastmcp.json configuration files, resolving a parsing error when CLI arguments were merged against the configuration spec.
+
+
+
+
+
+FastMCP 2.12.1 strengthens OAuth proxy implementation with improved client storage reliability, PKCE forwarding, configurable token endpoint authentication methods, and expanded scope handling based on extensive community testing.
+
+
+
+
+
+FastMCP 2.12 represents one of our most significant releases to date. After extensive testing and iteration with the community, we're shipping major improvements to authentication, configuration, and MCP feature adoption.
+
+🔐 **OAuth Proxy** bridges the gap for providers that don't support Dynamic Client Registration, enabling authentication with GitHub, Google, WorkOS, and Azure through minimal configuration.
+
+📋 **Declarative JSON Configuration** introduces `fastmcp.json` as the single source of truth for server settings, making MCP servers as portable and shareable as container images.
+
+🧠 **Sampling API Fallback** tackles adoption challenges by letting servers generate completions server-side when clients don't support the feature, encouraging innovation while maintaining compatibility.
+
+
+
+
+
+FastMCP 2.11 brings enterprise-ready authentication and dramatic performance improvements.
+
+🔒 **Comprehensive OAuth 2.1 Support** with WorkOS AuthKit integration, Dynamic Client Registration, and support for separate resource and authorization servers.
+
+⚡ **Experimental OpenAPI Parser** delivers dramatic performance gains through single-pass schema processing and optimized memory usage (enable with environment variable).
+
+💾 **Enhanced State Management** provides persistent state across tool calls with a simple dictionary interface, improving context handling and type annotations.
+
+This release emphasizes speed and simplicity while setting the foundation for future enterprise features.
+
+
+
+
+
+FastMCP 2.10 achieves full compliance with the 6/18/2025 MCP specification update, introducing powerful new communication patterns.
+
+💬 **Elicitation Support** enables dynamic server-client communication and "human-in-the-loop" workflows, allowing servers to request additional information during execution.
+
+📊 **Output Schemas** provide structured outputs for tools, making results more predictable and easier to parse programmatically.
+
+🛠️ **Enhanced HTTP Routing** with OpenAPI extensions support and configurable algorithms for more flexible API integration.
+
+This release includes a breaking change to `client.call_tool()` return signatures but significantly expands the interaction capabilities of MCP servers.
+
+
+
+
+
+FastMCP 2.9 is a major release that, among other things, introduces two important features that push beyond the basic MCP protocol.
+
+🤝 *MCP Middleware* brings a flexible middleware system for intercepting and controlling server operations - think authentication, logging, rate limiting, and custom business logic without touching core protocol code.
+
+✨ *Server-side type conversion* for prompts solves a major developer pain point: while MCP requires string arguments, your functions can now work with native Python types like lists and dictionaries, with automatic conversion handling the complexity.
+
+These features transform FastMCP from a simple protocol implementation into a powerful framework for building sophisticated MCP applications. Combined with the new `File` utility for binary data and improvements to authentication and serialization, this release makes FastMCP significantly more flexible and developer-friendly while maintaining full protocol compliance.
+
+
+
+
+
+FastMCP 2.8 is here, and it's all about taking control of your tools.
+
+This release is packed with new features for curating the perfect LLM experience:
+
+🛠️ Tool Transformation
+
+The headline feature lets you wrap any tool—from your own code, a third-party library, or an OpenAPI spec—to create an enhanced, LLM-friendly version. You can rename arguments, rewrite descriptions, and hide parameters without touching the original code.
+
+This feature was developed in close partnership with Bill Easton. As Bill brilliantly [put it](https://www.linkedin.com/posts/williamseaston_huge-thanks-to-william-easton-for-providing-activity-7338011349525983232-Mw6T?utm_source=share&utm_medium=member_desktop&rcm=ACoAAAAd6d0B3uL9zpCsq9eYWKi3HIvb8eN_r_Q), "Tool transformation flips Prompt Engineering on its head: stop writing tool-friendly LLM prompts and start providing LLM-friendly tools."
+
+🏷️ Component Control
+
+Now that you're transforming tools, you need a way to hide the old ones! In FastMCP 2.8 you can programmatically enable/disable any component, and for everyone who's been asking what FastMCP's tags are for—they finally have a purpose! You can now use tags to declaratively filter which components are exposed to your clients.
+
+🚀 Pragmatic by Default
+
+Lastly, to ensure maximum compatibility with the ecosystem, we've made the pragmatic decision to default all OpenAPI routes to Tools, making your entire API immediately accessible to any tool-using agent. When the industry catches up and supports resources, we'll restore the old default -- but no reason you should do extra work before OpenAI, Anthropic, or Google!
+
+
+
+
+
+
+FastMCP 2.7 has been released!
+
+Most notably, it introduces the highly requested (and Pythonic) "naked" decorator usage:
+
+```python {3}
+mcp = FastMCP()
+
+@mcp.tool
+def add(a: int, b: int) -> int:
+ return a + b
+```
+
+In addition, decorators now return the objects they create, instead of the decorated function. This is an important usability enhancement.
+
+The bulk of the update is focused on improving the FastMCP internals, including a few breaking internal changes to private APIs. A number of functions that have clung on since 1.0 are now deprecated.
+
+
+
+
+
+
+
+FastMCP 2.6 is here!
+
+This release introduces first-class authentication for MCP servers and clients, including pragmatic Bearer token support and seamless OAuth 2.1 integration. This release aligns with how major AI platforms are adopting MCP today, making it easier than ever to securely connect your tools to real-world AI models. Dive into the update and secure your stack with minimal friction.
+
+
+
+
+
+
+Your tests are bad and you should feel bad.
+
+Stop vibe-testing your MCP server through LLM guesswork. FastMCP 2.0 introduces in-memory testing for fast, deterministic, and fully Pythonic validation of your MCP logic—no network, no subprocesses, no vibes.
+
+
+
+
+
+
+
+
+In just six weeks since its relaunch, FastMCP has surpassed 10,000 GitHub stars—becoming the fastest-growing OSS project in our orbit. What started as a personal itch has become the backbone of Python-based MCP servers, powering a rapidly expanding ecosystem. While the protocol itself evolves, FastMCP continues to lead with clarity, developer experience, and opinionated tooling. Here’s to what’s next.
+
+
+
+
+
+
+
+FastMCP 2.3 introduces full support for Streamable HTTP, a modern alternative to SSE that simplifies MCP deployments over the web. It’s efficient, reliable, and now the default HTTP transport. Just run your server with transport="http" and connect clients via a standard URL—FastMCP handles the rest. No special setup required. This release makes deploying MCP servers easier and more portable than ever.
+
+
+
+
+
+
+
+Even AI needs a good travel adapter 🔌
+
+
+FastMCP now supports proxying arbitrary MCP servers, letting you run a local FastMCP instance that transparently forwards requests to any remote or third-party server—regardless of transport. This enables transport bridging (e.g., stdio ⇄ SSE), simplified client configuration, and powerful gateway patterns. Proxies are fully composable with other FastMCP servers, letting you mount or import them just like local servers. Use `FastMCP.from_client()` to wrap any backend in a clean, Pythonic proxy.
+
+
+
+
+
+
+This major release reimagines FastMCP as a full ecosystem platform, with powerful new features for composition, integration, and client interaction. You can now compose local and remote servers, proxy arbitrary MCP servers (with transport translation), and generate MCP servers from OpenAPI or FastAPI apps. A new client infrastructure supports advanced workflows like LLM sampling.
+
+FastMCP 2.0 builds on the success of v1 with a cleaner, more flexible foundation—try it out today!
+
+
+
+
+
+
+
+FastMCP 1.0 will become part of the official MCP Python SDK!
+
+
+
+
+
+
+
+Because life's too short for boilerplate.
+
+This is where it all started. FastMCP’s launch post introduced a clean, Pythonic way to build MCP servers without the protocol overhead. Just write functions; FastMCP handles the rest. What began as a weekend project quickly became the foundation of a growing ecosystem.
+
+
diff --git a/examples/apps/approval/approval_server.py b/examples/apps/approval/approval_server.py
new file mode 100644
index 000000000..3c82da5bc
--- /dev/null
+++ b/examples/apps/approval/approval_server.py
@@ -0,0 +1,13 @@
+"""Approval gate — require human sign-off before the agent acts.
+
+Usage:
+ uv run python approval_server.py
+"""
+
+from fastmcp import FastMCP
+from fastmcp.apps.approval import Approval
+
+mcp = FastMCP("Approval Demo", providers=[Approval()])
+
+if __name__ == "__main__":
+ mcp.run()
diff --git a/examples/apps/approvals/approvals_server.py b/examples/apps/approvals/approvals_server.py
new file mode 100644
index 000000000..c9204809b
--- /dev/null
+++ b/examples/apps/approvals/approvals_server.py
@@ -0,0 +1,333 @@
+"""Approval workflow — a FastMCPApp example with tabs, status badges, and action chaining.
+
+Demonstrates a multi-step interactive workflow:
+- @app.ui() entry point showing a pending approvals dashboard
+- @app.tool() backend tools that the UI calls via CallTool
+- @app.tool(model=True) for tools accessible from both model and UI
+- Tabs with filtered lists and counter badges
+- Action chaining: approve → update state → show toast
+
+Usage:
+ uv run python approvals_server.py
+"""
+
+from __future__ import annotations
+
+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,
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+ Column,
+ ForEach,
+ Heading,
+ If,
+ Muted,
+ Row,
+ Separator,
+ Tab,
+ Tabs,
+ Text,
+)
+from prefab_ui.rx import ERROR, RESULT, Rx
+
+from fastmcp import FastMCP, FastMCPApp
+
+# ---------------------------------------------------------------------------
+# Data
+# ---------------------------------------------------------------------------
+
+_requests: list[dict] = [
+ {
+ "id": "REQ-001",
+ "type": "expense",
+ "title": "Client dinner — Acme Corp",
+ "submitter": "Alice Chen",
+ "description": "Business dinner with Acme Corp stakeholders to discuss Q3 partnership.",
+ "amount": 284.50,
+ "status": "pending",
+ "created_at": "2026-03-18",
+ },
+ {
+ "id": "REQ-002",
+ "type": "access",
+ "title": "Production database read access",
+ "submitter": "Bob Martinez",
+ "description": "Need read access to prod DB for quarterly analytics report.",
+ "amount": None,
+ "status": "pending",
+ "created_at": "2026-03-19",
+ },
+ {
+ "id": "REQ-003",
+ "type": "time_off",
+ "title": "Vacation — Apr 7-11",
+ "submitter": "Carol Johnson",
+ "description": "Family vacation, all deliverables handed off to David.",
+ "amount": None,
+ "status": "approved",
+ "created_at": "2026-03-15",
+ },
+ {
+ "id": "REQ-004",
+ "type": "expense",
+ "title": "Conference registration — PyCon 2026",
+ "submitter": "David Kim",
+ "description": "PyCon US 2026 early-bird registration plus tutorial day.",
+ "amount": 650.00,
+ "status": "pending",
+ "created_at": "2026-03-20",
+ },
+ {
+ "id": "REQ-005",
+ "type": "access",
+ "title": "AWS staging account access",
+ "submitter": "Eva Mueller",
+ "description": "Staging environment access for load testing new API endpoints.",
+ "amount": None,
+ "status": "rejected",
+ "created_at": "2026-03-14",
+ },
+ {
+ "id": "REQ-006",
+ "type": "expense",
+ "title": "Team offsite lunch",
+ "submitter": "Frank Okafor",
+ "description": "Catering for 12-person engineering offsite planning session.",
+ "amount": 420.00,
+ "status": "pending",
+ "created_at": "2026-03-21",
+ },
+ {
+ "id": "REQ-007",
+ "type": "time_off",
+ "title": "Personal day — Mar 28",
+ "submitter": "Grace Liu",
+ "description": "Personal appointment, will be available on Slack for emergencies.",
+ "amount": None,
+ "status": "pending",
+ "created_at": "2026-03-20",
+ },
+ {
+ "id": "REQ-008",
+ "type": "expense",
+ "title": "Software license — Figma annual",
+ "submitter": "Hassan Ali",
+ "description": "Annual Figma Professional license renewal for design team.",
+ "amount": 144.00,
+ "status": "approved",
+ "created_at": "2026-03-12",
+ },
+]
+
+
+def _by_status(status: str) -> list[dict]:
+ return [r for r in _requests if r["status"] == status]
+
+
+def _find_request(request_id: str) -> dict | None:
+ for r in _requests:
+ if r["id"] == request_id:
+ return r
+ return None
+
+
+# ---------------------------------------------------------------------------
+# App
+# ---------------------------------------------------------------------------
+
+app = FastMCPApp("Approvals")
+
+
+def _all_lists() -> dict[str, list[dict]]:
+ """Return state updates for all three status lists."""
+ return {
+ "pending_requests": _by_status("pending"),
+ "approved_requests": _by_status("approved"),
+ "rejected_requests": _by_status("rejected"),
+ }
+
+
+@app.tool()
+def approve_request(request_id: str) -> dict[str, list[dict]]:
+ """Approve a pending request and return updated lists."""
+ req = _find_request(request_id)
+ if req is None:
+ raise ValueError(f"Request {request_id} not found")
+ if req["status"] != "pending":
+ raise ValueError(f"Request {request_id} is already {req['status']}")
+ req["status"] = "approved"
+ return _all_lists()
+
+
+@app.tool()
+def reject_request(request_id: str) -> dict[str, list[dict]]:
+ """Reject a pending request and return updated lists."""
+ req = _find_request(request_id)
+ if req is None:
+ raise ValueError(f"Request {request_id} not found")
+ if req["status"] != "pending":
+ raise ValueError(f"Request {request_id} is already {req['status']}")
+ req["status"] = "rejected"
+ return _all_lists()
+
+
+@app.tool()
+def add_comment(request_id: str, comment: str) -> dict:
+ """Add a comment to a request. Returns the updated request."""
+ req = _find_request(request_id)
+ if req is None:
+ raise ValueError(f"Request {request_id} not found")
+ comments = req.setdefault("comments", [])
+ comments.append(comment)
+ return req
+
+
+@app.tool(model=True)
+def get_request_details(request_id: str) -> dict:
+ """Get full details for a single request. Available to both model and UI."""
+ req = _find_request(request_id)
+ if req is None:
+ raise ValueError(f"Request {request_id} not found")
+ return req
+
+
+@app.tool()
+def list_requests(status: str | None = None) -> list[dict]:
+ """List requests, optionally filtered by status."""
+ if status is not None:
+ return _by_status(status)
+ return list(_requests)
+
+
+def _update_all_lists() -> list:
+ """Actions to update all three status lists from a tool result."""
+ return [
+ SetState("pending_requests", RESULT.pending_requests),
+ SetState("approved_requests", RESULT.approved_requests),
+ SetState("rejected_requests", RESULT.rejected_requests),
+ ]
+
+
+def _build_request_card(
+ item: Rx,
+ *,
+ status_variant: str = "warning",
+ show_actions: bool = False,
+) -> None:
+ """Build a card for a single request inside a ForEach context."""
+ request_id = str(item.id)
+
+ with Card():
+ with CardHeader():
+ with Row(gap=2, align="center", justify="between"):
+ CardTitle(item.title)
+ Badge(item.status, variant=status_variant)
+ with CardContent(css_class="space-y-2"):
+ with Row(gap=2, align="center"):
+ Badge(item.type, variant="secondary")
+ Text(item.submitter, css_class="font-medium")
+ Muted(item.created_at)
+
+ with If(item.amount):
+ Text(item.amount.currency(), css_class="text-lg font-semibold")
+
+ Muted(item.description)
+
+ if show_actions:
+ Separator()
+ with Row(gap=2):
+ Button(
+ "Approve",
+ variant="default",
+ on_click=CallTool(
+ approve_request,
+ arguments={"request_id": request_id},
+ on_success=_update_all_lists()
+ + [
+ ShowToast(
+ "Request approved",
+ variant="success",
+ ),
+ ],
+ on_error=ShowToast(
+ ERROR,
+ variant="error",
+ ),
+ ),
+ )
+ Button(
+ "Reject",
+ variant="destructive",
+ on_click=CallTool(
+ reject_request,
+ arguments={"request_id": request_id},
+ on_success=_update_all_lists()
+ + [
+ ShowToast(
+ "Request rejected",
+ variant="warning",
+ ),
+ ],
+ on_error=ShowToast(
+ ERROR,
+ variant="error",
+ ),
+ ),
+ )
+
+
+@app.ui()
+def approval_dashboard() -> PrefabApp:
+ """Open the approval dashboard. The model calls this to launch the app."""
+ pending_count = Rx("pending_requests").length()
+ approved_count = Rx("approved_requests").length()
+ rejected_count = Rx("rejected_requests").length()
+
+ with Column(gap=6, css_class="p-6") as view:
+ with Row(gap=3, align="center"):
+ Heading("Approval Dashboard")
+ Badge(pending_count, variant="warning")
+ Muted("pending")
+
+ with Tabs(value="pending"):
+ with Tab(title="Pending"):
+ with If(pending_count):
+ with ForEach("pending_requests") as item:
+ _build_request_card(item, show_actions=True)
+ with If(~pending_count):
+ Muted("No pending requests.")
+
+ with Tab(title="Approved"):
+ with If(approved_count):
+ with ForEach("approved_requests") as item:
+ _build_request_card(item, status_variant="success")
+ with If(~approved_count):
+ Muted("No approved requests.")
+
+ with Tab(title="Rejected"):
+ with If(rejected_count):
+ with ForEach("rejected_requests") as item:
+ _build_request_card(item, status_variant="destructive")
+ with If(~rejected_count):
+ Muted("No rejected requests.")
+
+ return PrefabApp(
+ view=view,
+ state={
+ "pending_requests": _by_status("pending"),
+ "approved_requests": _by_status("approved"),
+ "rejected_requests": _by_status("rejected"),
+ },
+ )
+
+
+mcp = FastMCP("Approvals Server", providers=[app])
+
+if __name__ == "__main__":
+ mcp.run(transport="http")
diff --git a/examples/apps/chart_server.py b/examples/apps/chart_server.py
index 94130f777..566cd3ea4 100644
--- a/examples/apps/chart_server.py
+++ b/examples/apps/chart_server.py
@@ -1,33 +1,11 @@
-"""Chart MCP App — interactive data visualizations with Prefab.
-
-Demonstrates `fastmcp[apps]` with Prefab chart components:
-- `BarChart` and `LineChart` for categorical and trend data
-- Multiple series, stacking, and curve styles
-- Layout composition with `Column`, `Heading`, and `Muted`
-- Custom text fallback via `ToolResult`
-
-Usage:
- uv run python chart_server.py # HTTP (port 8000)
- uv run python chart_server.py --stdio # stdio for MCP clients
-"""
-
-from __future__ import annotations
-
-from prefab_ui.app import PrefabApp
-from prefab_ui.components import (
- BarChart,
- ChartSeries,
- Column,
- Heading,
- LineChart,
- Muted,
-)
+from prefab_ui.components import Column, Heading, Muted
+from prefab_ui.components.charts import BarChart, ChartSeries
from fastmcp import FastMCP
mcp = FastMCP("Sales Dashboard")
-MONTHLY_SALES = [
+DATA = [
{"month": "Jan", "online": 4200, "retail": 2400},
{"month": "Feb", "online": 3800, "retail": 2100},
{"month": "Mar", "online": 5100, "retail": 2800},
@@ -38,21 +16,13 @@ MONTHLY_SALES = [
@mcp.tool(app=True)
-def sales_overview(stacked: bool = False) -> PrefabApp:
- """View monthly sales broken down by channel.
-
- Args:
- stacked: Stack bars to show total revenue per month.
- """
- total = sum(row["online"] + row["retail"] for row in MONTHLY_SALES)
-
- with Column(gap=6, css_class="p-6") as view:
- with Column(gap=1):
- Heading("Monthly Sales")
- Muted(f"${total:,} total revenue")
-
+def sales_chart(stacked: bool = False) -> Column:
+ """Show monthly online vs. retail sales as a bar chart."""
+ with Column(gap=4, css_class="p-6") as view:
+ Heading("Monthly Sales")
+ Muted("Online vs. retail — hover bars for details")
BarChart(
- data=MONTHLY_SALES,
+ data=DATA,
series=[
ChartSeries(data_key="online", label="Online"),
ChartSeries(data_key="retail", label="Retail"),
@@ -61,41 +31,7 @@ def sales_overview(stacked: bool = False) -> PrefabApp:
stacked=stacked,
show_legend=True,
)
-
- return PrefabApp(
- title="Sales Dashboard",
- view=view,
- )
-
-
-@mcp.tool(app=True)
-def sales_trend(curve: str = "linear") -> PrefabApp:
- """View sales trends over time as a line chart.
-
- Args:
- curve: Line style — "linear", "smooth", or "step".
- """
- with Column(gap=6, css_class="p-6") as view:
- with Column(gap=1):
- Heading("Sales Trend")
- Muted("Online vs. retail over 6 months")
-
- LineChart(
- data=MONTHLY_SALES,
- series=[
- ChartSeries(data_key="online", label="Online"),
- ChartSeries(data_key="retail", label="Retail"),
- ],
- x_axis="month",
- curve=curve,
- show_dots=True,
- show_legend=True,
- )
-
- return PrefabApp(
- title="Sales Trend",
- view=view,
- )
+ return view
if __name__ == "__main__":
diff --git a/examples/apps/choice/choice_server.py b/examples/apps/choice/choice_server.py
new file mode 100644
index 000000000..b91dfb726
--- /dev/null
+++ b/examples/apps/choice/choice_server.py
@@ -0,0 +1,13 @@
+"""Multiple choice — let the user pick from options instead of typing.
+
+Usage:
+ uv run python choice_server.py
+"""
+
+from fastmcp import FastMCP
+from fastmcp.apps.choice import Choice
+
+mcp = FastMCP("Choice Demo", providers=[Choice()])
+
+if __name__ == "__main__":
+ mcp.run()
diff --git a/examples/apps/contacts/contacts_server.py b/examples/apps/contacts/contacts_server.py
new file mode 100644
index 000000000..66c2ba597
--- /dev/null
+++ b/examples/apps/contacts/contacts_server.py
@@ -0,0 +1,148 @@
+"""Contact manager — a FastMCPApp example with forms and callable tool references.
+
+Demonstrates the full FastMCPApp stack:
+- @app.ui() entry point that the model calls to open the app
+- @app.tool() backend tools that the UI calls via CallTool
+- CallTool(fn) with function references (not strings) that resolve to global keys
+- Form.from_model() for auto-generated Pydantic model forms
+- Manual form construction with the context-manager pattern
+
+Usage:
+ uv run python contacts_server.py
+"""
+
+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 ERROR, RESULT, STATE
+from pydantic import BaseModel, Field
+
+from fastmcp import FastMCP, FastMCPApp
+
+# ---------------------------------------------------------------------------
+# Data
+# ---------------------------------------------------------------------------
+
+_contacts: list[dict] = [
+ {
+ "name": "Arthur Dent",
+ "email": "arthur@earth.com",
+ "category": "Customer",
+ "notes": "",
+ },
+ {
+ "name": "Ford Prefect",
+ "email": "ford@betelgeuse.org",
+ "category": "Partner",
+ "notes": "Researcher",
+ },
+]
+
+
+# ---------------------------------------------------------------------------
+# Pydantic model for auto-generated forms
+# ---------------------------------------------------------------------------
+
+
+class ContactModel(BaseModel):
+ name: str = Field(title="Full Name", min_length=1)
+ email: str = Field(title="Email")
+ category: Literal["Customer", "Vendor", "Partner", "Other"] = "Other"
+ notes: str = Field(
+ default="",
+ title="Notes",
+ json_schema_extra={"ui": {"type": "textarea"}},
+ )
+
+
+# ---------------------------------------------------------------------------
+# App
+# ---------------------------------------------------------------------------
+
+app = FastMCPApp("Contacts")
+
+
+@app.tool()
+def save_contact(data: ContactModel) -> list[dict]:
+ """Save a new contact and return the updated list."""
+ _contacts.append(data.model_dump())
+ return list(_contacts)
+
+
+@app.tool()
+def search_contacts(query: str) -> list[dict]:
+ """Filter contacts by name or email."""
+ q = query.lower()
+ return [c for c in _contacts 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)
+
+
+@app.ui()
+def contact_manager() -> PrefabApp:
+ """Open the contact manager. The model calls this to launch the app."""
+ 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(ERROR, variant="error"),
+ ),
+ )
+
+ Separator()
+
+ Heading("Search", level=3)
+ with Form(
+ on_submit=CallTool(
+ search_contacts,
+ arguments={"query": STATE.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)})
+
+
+mcp = FastMCP("Contacts Server", providers=[app])
+
+if __name__ == "__main__":
+ mcp.run(transport="http")
diff --git a/examples/apps/datatable_server.py b/examples/apps/datatable_server.py
index 1f79c51dd..dc78b4984 100644
--- a/examples/apps/datatable_server.py
+++ b/examples/apps/datatable_server.py
@@ -1,144 +1,108 @@
-"""DataTable MCP App — interactive, sortable data views with Prefab.
-
-Demonstrates `fastmcp[apps]` with Prefab UI components:
-- `app=True` for automatic renderer wiring
-- `PrefabApp` with `DataTable` for rich tabular views
-- Searchable, sortable, paginated tables
-- Layout composition with `Column`, `Heading`, `Text`, and `Badge`
-
-Usage:
- uv run python datatable_server.py # HTTP (port 8000)
- uv run python datatable_server.py --stdio # stdio for MCP clients
-"""
-
-from __future__ import annotations
+from collections import Counter
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
Badge,
+ Card,
+ CardContent,
Column,
- DataTable,
- DataTableColumn,
+ Grid,
Heading,
- Muted,
Row,
+ Separator,
+ Text,
)
+from prefab_ui.components.charts import BarChart, ChartSeries, PieChart
+from prefab_ui.components.data_table import DataTable, DataTableColumn
from fastmcp import FastMCP
mcp = FastMCP("Team Directory")
-EMPLOYEES = [
+TEAM = [
{
"name": "Alice Chen",
"role": "Engineering",
"level": "Senior",
"location": "San Francisco",
- "status": "active",
- },
- {
- "name": "Bob Martinez",
- "role": "Design",
- "level": "Lead",
- "location": "New York",
- "status": "active",
},
+ {"name": "Bob Martinez", "role": "Design", "level": "Lead", "location": "New York"},
{
"name": "Carol Johnson",
"role": "Engineering",
"level": "Staff",
"location": "London",
- "status": "active",
},
{
"name": "David Kim",
"role": "Product",
"level": "Senior",
"location": "San Francisco",
- "status": "away",
- },
- {
- "name": "Eva Müller",
- "role": "Engineering",
- "level": "Mid",
- "location": "Berlin",
- "status": "active",
},
+ {"name": "Eva Müller", "role": "Engineering", "level": "Mid", "location": "Berlin"},
{
"name": "Frank Okafor",
"role": "Data Science",
"level": "Senior",
"location": "Lagos",
- "status": "active",
},
{
"name": "Grace Liu",
"role": "Engineering",
"level": "Junior",
"location": "Singapore",
- "status": "active",
- },
- {
- "name": "Hassan Ali",
- "role": "Design",
- "level": "Senior",
- "location": "Dubai",
- "status": "away",
- },
- {
- "name": "Iris Tanaka",
- "role": "Product",
- "level": "Lead",
- "location": "Tokyo",
- "status": "active",
- },
- {
- "name": "James Wright",
- "role": "Engineering",
- "level": "Senior",
- "location": "London",
- "status": "inactive",
- },
- {
- "name": "Karen Petrov",
- "role": "Data Science",
- "level": "Lead",
- "location": "Berlin",
- "status": "active",
- },
- {
- "name": "Liam O'Brien",
- "role": "Engineering",
- "level": "Mid",
- "location": "Dublin",
- "status": "active",
},
+ {"name": "Hassan Ali", "role": "Design", "level": "Senior", "location": "Dubai"},
]
@mcp.tool(app=True)
-def list_team(department: str | None = None) -> PrefabApp:
- """Browse the team directory with sorting and search.
+def team_directory(department: str | None = None) -> PrefabApp:
+ """Browse the team directory — sortable, searchable, with department breakdown."""
+ rows = [p for p in TEAM if not department or p["role"] == department]
- Args:
- department: Filter by department (e.g. "Engineering", "Design").
- Leave empty to show everyone.
- """
- if department:
- rows = [e for e in EMPLOYEES if e["role"].lower() == department.lower()]
- else:
- rows = EMPLOYEES
+ dept_counts = Counter(p["role"] for p in rows)
+ chart_data = [{"department": k, "count": v} for k, v in dept_counts.items()]
- active = sum(1 for e in rows if e["status"] == "active")
+ level_counts = Counter(p["level"] for p in rows)
+ level_data = [{"level": k, "count": v} for k, v in level_counts.items()]
with Column(gap=6, css_class="p-6") as view:
- with Column(gap=1):
+ with Row(gap=2, align="center"):
Heading("Team Directory")
- with Row(gap=2):
- Muted(f"{len(rows)} members")
- Muted(f"{active} active", css_class="text-success")
- if department:
- Badge(department, variant="outline")
+ Badge(f"{len(rows)} people", variant="secondary")
+
+ with Grid(columns=2, gap=6):
+ with Card():
+ with CardContent():
+ Text(
+ "By Department",
+ css_class="text-sm font-medium text-muted-foreground mb-2",
+ )
+ PieChart(
+ data=chart_data,
+ data_key="count",
+ name_key="department",
+ show_legend=True,
+ inner_radius=40,
+ height=200,
+ )
+
+ with Card():
+ with CardContent():
+ Text(
+ "By Level",
+ css_class="text-sm font-medium text-muted-foreground mb-2",
+ )
+ BarChart(
+ data=level_data,
+ series=[ChartSeries(data_key="count", label="People")],
+ x_axis="level",
+ height=200,
+ horizontal=True,
+ )
+
+ Separator()
DataTable(
columns=[
@@ -146,19 +110,13 @@ def list_team(department: str | None = None) -> PrefabApp:
DataTableColumn(key="role", header="Department", sortable=True),
DataTableColumn(key="level", header="Level", sortable=True),
DataTableColumn(key="location", header="Location", sortable=True),
- DataTableColumn(key="status", header="Status", sortable=True),
],
rows=rows,
- searchable=True,
+ search=True,
paginated=True,
- page_size=10,
)
- return PrefabApp(
- title="Team Directory",
- view=view,
- state={"total": len(rows), "active": active},
- )
+ return PrefabApp(view=view)
if __name__ == "__main__":
diff --git a/examples/apps/explorer/explorer_server.py b/examples/apps/explorer/explorer_server.py
new file mode 100644
index 000000000..41896f211
--- /dev/null
+++ b/examples/apps/explorer/explorer_server.py
@@ -0,0 +1,590 @@
+"""Data explorer — a FastMCPApp example with tables, charts, and filtering.
+
+Demonstrates the full FastMCPApp stack:
+- @app.ui() entry point with a tabbed data exploration interface
+- @app.tool() backend tools for analysis, summaries, and filtering
+- DataTable with sorting, search, and pagination
+- BarChart and PieChart for data visualization
+- Metric cards for summary statistics
+- Select-driven filtering with CallTool
+- State management with PrefabApp state dict and Rx()
+
+Usage:
+ uv run python explorer_server.py # HTTP (default)
+ uv run python explorer_server.py --stdio # stdio for MCP clients
+"""
+
+from __future__ import annotations
+
+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,
+ Card,
+ CardContent,
+ Column,
+ DataTable,
+ DataTableColumn,
+ Grid,
+ Heading,
+ Metric,
+ Muted,
+ Row,
+ Select,
+ SelectOption,
+ Separator,
+ Tab,
+ Tabs,
+ Text,
+)
+from prefab_ui.components.charts import BarChart, ChartSeries, PieChart
+from prefab_ui.rx import ERROR, RESULT, STATE, Rx
+
+from fastmcp import FastMCP, FastMCPApp
+
+# ---------------------------------------------------------------------------
+# Sample data
+# ---------------------------------------------------------------------------
+
+SALES_DATA: list[dict] = [
+ {
+ "date": "2025-01-05",
+ "product": "Widget A",
+ "region": "North",
+ "amount": 1200,
+ "quantity": 10,
+ },
+ {
+ "date": "2025-01-12",
+ "product": "Widget B",
+ "region": "South",
+ "amount": 850,
+ "quantity": 7,
+ },
+ {
+ "date": "2025-01-18",
+ "product": "Gadget X",
+ "region": "East",
+ "amount": 2300,
+ "quantity": 15,
+ },
+ {
+ "date": "2025-01-25",
+ "product": "Gadget Y",
+ "region": "West",
+ "amount": 1750,
+ "quantity": 12,
+ },
+ {
+ "date": "2025-02-02",
+ "product": "Widget A",
+ "region": "East",
+ "amount": 1400,
+ "quantity": 11,
+ },
+ {
+ "date": "2025-02-09",
+ "product": "Widget B",
+ "region": "North",
+ "amount": 920,
+ "quantity": 8,
+ },
+ {
+ "date": "2025-02-15",
+ "product": "Gadget X",
+ "region": "South",
+ "amount": 2100,
+ "quantity": 14,
+ },
+ {
+ "date": "2025-02-22",
+ "product": "Gadget Y",
+ "region": "West",
+ "amount": 1600,
+ "quantity": 11,
+ },
+ {
+ "date": "2025-03-01",
+ "product": "Widget A",
+ "region": "South",
+ "amount": 1350,
+ "quantity": 10,
+ },
+ {
+ "date": "2025-03-08",
+ "product": "Widget B",
+ "region": "West",
+ "amount": 780,
+ "quantity": 6,
+ },
+ {
+ "date": "2025-03-14",
+ "product": "Gadget X",
+ "region": "North",
+ "amount": 2500,
+ "quantity": 17,
+ },
+ {
+ "date": "2025-03-21",
+ "product": "Gadget Y",
+ "region": "East",
+ "amount": 1900,
+ "quantity": 13,
+ },
+ {
+ "date": "2025-04-03",
+ "product": "Widget A",
+ "region": "West",
+ "amount": 1100,
+ "quantity": 9,
+ },
+ {
+ "date": "2025-04-10",
+ "product": "Widget B",
+ "region": "East",
+ "amount": 960,
+ "quantity": 8,
+ },
+ {
+ "date": "2025-04-17",
+ "product": "Gadget X",
+ "region": "South",
+ "amount": 2400,
+ "quantity": 16,
+ },
+ {
+ "date": "2025-04-24",
+ "product": "Gadget Y",
+ "region": "North",
+ "amount": 1850,
+ "quantity": 12,
+ },
+ {
+ "date": "2025-05-01",
+ "product": "Widget A",
+ "region": "North",
+ "amount": 1500,
+ "quantity": 12,
+ },
+ {
+ "date": "2025-05-08",
+ "product": "Widget B",
+ "region": "South",
+ "amount": 890,
+ "quantity": 7,
+ },
+ {
+ "date": "2025-05-15",
+ "product": "Gadget X",
+ "region": "West",
+ "amount": 2200,
+ "quantity": 15,
+ },
+ {
+ "date": "2025-05-22",
+ "product": "Gadget Y",
+ "region": "East",
+ "amount": 1700,
+ "quantity": 11,
+ },
+ {
+ "date": "2025-06-05",
+ "product": "Widget A",
+ "region": "East",
+ "amount": 1300,
+ "quantity": 10,
+ },
+ {
+ "date": "2025-06-12",
+ "product": "Widget B",
+ "region": "North",
+ "amount": 1050,
+ "quantity": 9,
+ },
+ {
+ "date": "2025-06-19",
+ "product": "Gadget X",
+ "region": "North",
+ "amount": 2600,
+ "quantity": 18,
+ },
+ {
+ "date": "2025-06-26",
+ "product": "Gadget Y",
+ "region": "South",
+ "amount": 1650,
+ "quantity": 11,
+ },
+ {
+ "date": "2025-07-03",
+ "product": "Widget A",
+ "region": "South",
+ "amount": 1450,
+ "quantity": 11,
+ },
+ {
+ "date": "2025-07-10",
+ "product": "Widget B",
+ "region": "West",
+ "amount": 830,
+ "quantity": 7,
+ },
+ {
+ "date": "2025-07-17",
+ "product": "Gadget X",
+ "region": "East",
+ "amount": 2350,
+ "quantity": 16,
+ },
+ {
+ "date": "2025-07-24",
+ "product": "Gadget Y",
+ "region": "West",
+ "amount": 1800,
+ "quantity": 12,
+ },
+ {
+ "date": "2025-08-01",
+ "product": "Widget A",
+ "region": "West",
+ "amount": 1250,
+ "quantity": 10,
+ },
+ {
+ "date": "2025-08-08",
+ "product": "Widget B",
+ "region": "East",
+ "amount": 970,
+ "quantity": 8,
+ },
+ {
+ "date": "2025-08-15",
+ "product": "Gadget X",
+ "region": "South",
+ "amount": 2450,
+ "quantity": 16,
+ },
+ {
+ "date": "2025-08-22",
+ "product": "Gadget Y",
+ "region": "North",
+ "amount": 1950,
+ "quantity": 13,
+ },
+]
+
+REGIONS = ["All", "North", "South", "East", "West"]
+PRODUCTS = ["All", "Widget A", "Widget B", "Gadget X", "Gadget Y"]
+
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+
+def _filter_rows(
+ rows: list[dict],
+ region: str = "All",
+ product: str = "All",
+) -> list[dict]:
+ filtered = rows
+ if region != "All":
+ filtered = [r for r in filtered if r["region"] == region]
+ if product != "All":
+ filtered = [r for r in filtered if r["product"] == product]
+ return filtered
+
+
+def _compute_summary(rows: list[dict]) -> dict:
+ if not rows:
+ return {
+ "count": 0,
+ "total_amount": 0,
+ "avg_amount": 0,
+ "min_amount": 0,
+ "max_amount": 0,
+ "total_quantity": 0,
+ }
+ amounts = [r["amount"] for r in rows]
+ return {
+ "count": len(rows),
+ "total_amount": sum(amounts),
+ "avg_amount": round(sum(amounts) / len(amounts)),
+ "min_amount": min(amounts),
+ "max_amount": max(amounts),
+ "total_quantity": sum(r["quantity"] for r in rows),
+ }
+
+
+def _aggregate_by(rows: list[dict], key: str) -> list[dict]:
+ totals: dict[str, int] = {}
+ for row in rows:
+ label = row[key]
+ totals[label] = totals.get(label, 0) + row["amount"]
+ return [{key: label, "amount": total} for label, total in sorted(totals.items())]
+
+
+# ---------------------------------------------------------------------------
+# App
+# ---------------------------------------------------------------------------
+
+app = FastMCPApp("Data Explorer")
+
+
+@app.tool()
+def analyze_data(region: str = "All", product: str = "All") -> dict:
+ """Filter and analyze sales data. Returns rows, summary, and chart data."""
+ filtered = _filter_rows(SALES_DATA, region, product)
+ return {
+ "rows": filtered,
+ "summary": _compute_summary(filtered),
+ "by_region": _aggregate_by(filtered, "region"),
+ "by_product": _aggregate_by(filtered, "product"),
+ }
+
+
+@app.tool(model=True)
+def get_summary() -> dict:
+ """Return summary statistics for the full dataset."""
+ return _compute_summary(SALES_DATA)
+
+
+@app.tool()
+def filter_data(region: str = "All", product: str = "All") -> list[dict]:
+ """Filter sales data by region and/or product."""
+ return _filter_rows(SALES_DATA, region, product)
+
+
+@app.ui()
+def data_explorer() -> PrefabApp:
+ """Open the data explorer. Browse, filter, and visualize sales data."""
+
+ initial = analyze_data()
+
+ with Column(gap=6, css_class="p-6") as view:
+ Heading("Sales Data Explorer")
+ Muted(f"{len(SALES_DATA)} records loaded")
+
+ Separator()
+
+ # ----- Filters -----
+ with Row(gap=4, align="center"):
+ Text("Filters", css_class="font-semibold")
+
+ with Select(
+ name="selected_region",
+ placeholder="Region",
+ value="All",
+ on_change=[
+ SetState("loading", True),
+ CallTool(
+ analyze_data,
+ arguments={
+ "region": STATE.selected_region,
+ "product": STATE.selected_product,
+ },
+ on_success=[
+ SetState("rows", RESULT.rows),
+ SetState("summary", RESULT.summary),
+ SetState("by_region", RESULT.by_region),
+ SetState("by_product", RESULT.by_product),
+ SetState("loading", False),
+ ShowToast("Data updated", variant="success"),
+ ],
+ on_error=[
+ SetState("loading", False),
+ ShowToast(ERROR, variant="error"),
+ ],
+ ),
+ ],
+ ):
+ for region in REGIONS:
+ SelectOption(value=region, label=region)
+
+ with Select(
+ name="selected_product",
+ placeholder="Product",
+ value="All",
+ on_change=[
+ SetState("loading", True),
+ CallTool(
+ analyze_data,
+ arguments={
+ "region": STATE.selected_region,
+ "product": STATE.selected_product,
+ },
+ on_success=[
+ SetState("rows", RESULT.rows),
+ SetState("summary", RESULT.summary),
+ SetState("by_region", RESULT.by_region),
+ SetState("by_product", RESULT.by_product),
+ SetState("loading", False),
+ ShowToast("Data updated", variant="success"),
+ ],
+ on_error=[
+ SetState("loading", False),
+ ShowToast(ERROR, variant="error"),
+ ],
+ ),
+ ],
+ ):
+ for product in PRODUCTS:
+ SelectOption(value=product, label=product)
+
+ Button(
+ Rx("loading").then("Loading...", "Reset"),
+ disabled=Rx("loading"),
+ on_click=[
+ SetState("selected_region", "All"),
+ SetState("selected_product", "All"),
+ SetState("loading", True),
+ CallTool(
+ analyze_data,
+ arguments={"region": "All", "product": "All"},
+ on_success=[
+ SetState("rows", RESULT.rows),
+ SetState("summary", RESULT.summary),
+ SetState("by_region", RESULT.by_region),
+ SetState("by_product", RESULT.by_product),
+ SetState("loading", False),
+ ],
+ on_error=[
+ SetState("loading", False),
+ ShowToast(ERROR, variant="error"),
+ ],
+ ),
+ ],
+ )
+
+ Separator()
+
+ # ----- Tabs -----
+ with Tabs():
+ # ---- Summary ----
+ with Tab("Summary"):
+ with Grid(columns=3, gap=4):
+ with Card():
+ with CardContent():
+ Metric(
+ label="Total Revenue",
+ value=Rx("summary.total_amount"),
+ )
+ with Card():
+ with CardContent():
+ Metric(
+ label="Average Sale",
+ value=Rx("summary.avg_amount"),
+ )
+ with Card():
+ with CardContent():
+ Metric(
+ label="Total Quantity",
+ value=Rx("summary.total_quantity"),
+ )
+
+ with Grid(columns=3, gap=4, css_class="mt-4"):
+ with Card():
+ with CardContent():
+ Metric(
+ label="Transactions",
+ value=Rx("summary.count"),
+ )
+ with Card():
+ with CardContent():
+ Metric(
+ label="Min Sale",
+ value=Rx("summary.min_amount"),
+ )
+ with Card():
+ with CardContent():
+ Metric(
+ label="Max Sale",
+ value=Rx("summary.max_amount"),
+ )
+
+ with Row(gap=2, css_class="mt-4"):
+ Badge(f"Region: {STATE.selected_region}")
+ Badge(f"Product: {STATE.selected_product}")
+
+ # ---- Table ----
+ with Tab("Table"):
+ DataTable(
+ columns=[
+ DataTableColumn(key="date", header="Date", sortable=True),
+ DataTableColumn(key="product", header="Product", sortable=True),
+ DataTableColumn(key="region", header="Region", sortable=True),
+ DataTableColumn(
+ key="amount", header="Amount ($)", sortable=True
+ ),
+ DataTableColumn(key="quantity", header="Qty", sortable=True),
+ ],
+ rows="{{ rows }}",
+ search=True,
+ paginated=True,
+ page_size=10,
+ )
+
+ # ---- Charts ----
+ with Tab("Charts"):
+ with Grid(columns=2, gap=6):
+ with Column(gap=2):
+ Heading("Revenue by Region", level=3)
+ BarChart(
+ data=Rx("by_region"),
+ series=[ChartSeries(data_key="amount", label="Revenue")],
+ x_axis="region",
+ show_legend=True,
+ )
+
+ with Column(gap=2):
+ Heading("Revenue by Product", level=3)
+ BarChart(
+ data=Rx("by_product"),
+ series=[ChartSeries(data_key="amount", label="Revenue")],
+ x_axis="product",
+ show_legend=True,
+ )
+
+ Separator(css_class="my-4")
+
+ with Grid(columns=2, gap=6):
+ with Column(gap=2):
+ Heading("Region Breakdown", level=3)
+ PieChart(
+ data=Rx("by_region"),
+ data_key="amount",
+ name_key="region",
+ show_legend=True,
+ inner_radius=60,
+ )
+
+ with Column(gap=2):
+ Heading("Product Breakdown", level=3)
+ PieChart(
+ data=Rx("by_product"),
+ data_key="amount",
+ name_key="product",
+ show_legend=True,
+ inner_radius=60,
+ )
+
+ return PrefabApp(
+ view=view,
+ state={
+ "rows": initial["rows"],
+ "summary": initial["summary"],
+ "by_region": initial["by_region"],
+ "by_product": initial["by_product"],
+ "selected_region": "All",
+ "selected_product": "All",
+ "loading": False,
+ },
+ )
+
+
+mcp = FastMCP("Data Explorer", providers=[app])
+
+if __name__ == "__main__":
+ mcp.run(transport="http")
diff --git a/examples/apps/file_upload/file_upload_server.py b/examples/apps/file_upload/file_upload_server.py
new file mode 100644
index 000000000..c567f7820
--- /dev/null
+++ b/examples/apps/file_upload/file_upload_server.py
@@ -0,0 +1,13 @@
+"""File upload — bypass the LLM context window to get files onto the server.
+
+Usage:
+ uv run python file_upload_server.py
+"""
+
+from fastmcp import FastMCP
+from fastmcp.apps.file_upload import FileUpload
+
+mcp = FastMCP("File Upload Server", providers=[FileUpload()])
+
+if __name__ == "__main__":
+ mcp.run()
diff --git a/examples/apps/form/form_server.py b/examples/apps/form/form_server.py
new file mode 100644
index 000000000..bfa2ab27e
--- /dev/null
+++ b/examples/apps/form/form_server.py
@@ -0,0 +1,41 @@
+"""Form input — collect structured data from users via Pydantic models.
+
+Usage:
+ uv run python form_server.py
+"""
+
+from typing import Literal
+
+from pydantic import BaseModel, Field
+
+from fastmcp import FastMCP
+from fastmcp.apps.form import FormInput
+
+
+class ShippingAddress(BaseModel):
+ name: str = Field(description="Full name")
+ street: str = Field(description="Street address")
+ city: str
+ state: str = Field(description="Two-letter state code")
+ zip_code: str = Field(description="5-digit ZIP")
+
+
+class BugReport(BaseModel):
+ title: str = Field(description="Brief summary")
+ severity: Literal["low", "medium", "high", "critical"]
+ description: str = Field(
+ description="Detailed description",
+ json_schema_extra={"ui": {"type": "textarea"}},
+ )
+
+
+mcp = FastMCP(
+ "Form Demo",
+ providers=[
+ FormInput(model=ShippingAddress),
+ FormInput(model=BugReport),
+ ],
+)
+
+if __name__ == "__main__":
+ mcp.run()
diff --git a/examples/apps/generative_ui.py b/examples/apps/generative_ui.py
new file mode 100644
index 000000000..e03f6ed85
--- /dev/null
+++ b/examples/apps/generative_ui.py
@@ -0,0 +1,22 @@
+"""Generative UI — let the LLM build custom Prefab UIs on the fly.
+
+The GenerativeUI provider registers two tools:
+- generate_prefab_ui: the LLM writes Prefab Python code, it runs in a sandbox, the result renders
+- search_prefab_components: the LLM searches the Prefab component library
+
+The generative renderer supports streaming: as the LLM writes code into
+the `code` argument, the host forwards partial arguments to the app via
+ontoolinputpartial, and the user watches the UI build up in real time.
+
+Usage:
+ uv run python generative_ui.py
+"""
+
+from fastmcp import FastMCP
+from fastmcp.apps.generative import GenerativeUI
+
+mcp = FastMCP("Prefab Studio")
+mcp.add_provider(GenerativeUI())
+
+if __name__ == "__main__":
+ mcp.run()
diff --git a/examples/apps/greet_server.py b/examples/apps/greet_server.py
new file mode 100644
index 000000000..bb3ad8321
--- /dev/null
+++ b/examples/apps/greet_server.py
@@ -0,0 +1,64 @@
+"""Minimal example demonstrating a @app=True tool with arguments.
+
+Usage:
+ uv run python greet_server.py
+"""
+
+from __future__ import annotations
+
+from typing import Literal
+
+from prefab_ui.components import Badge, Column, Heading, Muted
+
+from fastmcp import FastMCP
+
+mcp = FastMCP("Greeter")
+
+GREETINGS: dict[str, str] = {
+ "English": "Hello",
+ "Spanish": "¡Hola",
+ "French": "Bonjour",
+ "Japanese": "こんにちは",
+ "Arabic": "مرحبا",
+}
+
+
+@mcp.tool(app=True)
+def greet(
+ name: str,
+ language: Literal["English", "Spanish", "French", "Japanese", "Arabic"] = "English",
+) -> Column:
+ """Greet someone in their language."""
+ word = GREETINGS[language]
+ with Column(gap=3, css_class="p-8") as view:
+ Heading(f"{word}, {name}!")
+ Muted("Greeting rendered by FastMCP")
+ Badge(language)
+ return view
+
+
+FAREWELLS: dict[str, str] = {
+ "English": "Goodbye",
+ "Spanish": "Adiós",
+ "French": "Au revoir",
+ "Japanese": "さようなら",
+ "Arabic": "مع السلامة",
+}
+
+
+@mcp.tool(app=True)
+def farewell(
+ name: str,
+ language: Literal["English", "Spanish", "French", "Japanese", "Arabic"] = "English",
+) -> Column:
+ """Say farewell in their language."""
+ word = FAREWELLS[language]
+ with Column(gap=3, css_class="p-8") as view:
+ Heading(f"{word}, {name}!")
+ Muted("Farewell rendered by FastMCP")
+ Badge(language)
+ return view
+
+
+if __name__ == "__main__":
+ mcp.run()
diff --git a/examples/apps/inspector_demo.py b/examples/apps/inspector_demo.py
new file mode 100644
index 000000000..2d5d244bc
--- /dev/null
+++ b/examples/apps/inspector_demo.py
@@ -0,0 +1,120 @@
+"""Demo server for testing the dev apps MCP message inspector.
+
+Exercises tool calls, server notifications (ctx.log), and errors
+so you can verify all message types appear in the inspector panel.
+
+Usage:
+ fastmcp dev apps examples/apps/inspector_demo.py
+"""
+
+from __future__ import annotations
+
+from prefab_ui.actions import ShowToast
+from prefab_ui.actions.mcp import CallTool, SendMessage, UpdateContext
+from prefab_ui.components import (
+ Badge,
+ Button,
+ Column,
+ Heading,
+ Muted,
+ Row,
+)
+from prefab_ui.rx import ERROR
+
+from fastmcp import FastMCP
+from fastmcp.server.context import Context
+
+mcp = FastMCP("Inspector Demo")
+
+
+@mcp.tool(app=True)
+def demo() -> Column:
+ """A demo app that exercises various MCP message types."""
+ with Column(gap=6, css_class="p-8 max-w-lg") as view:
+ Heading("Inspector Demo")
+ Muted("Click the buttons and watch the inspector panel on the right.")
+
+ with Column(gap=3):
+ with Row(gap=2, align="center"):
+ Button(
+ "Call Tool",
+ variant="default",
+ on_click=CallTool(
+ "echo",
+ arguments={"message": "Hello from the inspector!"},
+ on_success=ShowToast("Tool call succeeded", variant="success"),
+ on_error=ShowToast(ERROR, variant="error"),
+ ),
+ )
+ Badge("tools/call + response", variant="secondary")
+
+ with Row(gap=2, align="center"):
+ Button(
+ "Call with Logging",
+ variant="default",
+ on_click=CallTool(
+ "echo_with_logs",
+ arguments={"message": "Watch the notifications!"},
+ on_success=ShowToast("Done (check logs)", variant="success"),
+ on_error=ShowToast(ERROR, variant="error"),
+ ),
+ )
+ Badge("tools/call + notifications", variant="secondary")
+
+ with Row(gap=2, align="center"):
+ Button(
+ "Trigger Error",
+ variant="destructive",
+ on_click=CallTool(
+ "fail",
+ arguments={},
+ on_error=ShowToast(ERROR, variant="error"),
+ ),
+ )
+ Badge("error response", variant="destructive")
+
+ with Row(gap=2, align="center"):
+ Button(
+ "Update Context",
+ variant="outline",
+ on_click=[
+ UpdateContext(content="Demo context from inspector"),
+ ShowToast("Context updated", variant="success"),
+ ],
+ )
+ Badge("bridge: UpdateContext", variant="outline")
+
+ with Row(gap=2, align="center"):
+ Button(
+ "Send Message",
+ variant="outline",
+ on_click=SendMessage("Tell me about this demo app"),
+ )
+ Badge("bridge: SendMessage", variant="outline")
+
+ return view
+
+
+@mcp.tool()
+def echo(message: str) -> str:
+ """Echo a message back."""
+ return f"Echo: {message}"
+
+
+@mcp.tool()
+async def echo_with_logs(message: str, ctx: Context) -> str:
+ """Echo a message and emit log notifications."""
+ await ctx.log(f"Processing: {message}", level="info")
+ await ctx.log("Step 1: validated input", level="debug")
+ await ctx.log("Step 2: generating response", level="debug")
+ return f"Logged echo: {message}"
+
+
+@mcp.tool()
+def fail() -> str:
+ """Always raises an error."""
+ raise ValueError("This is a deliberate error for testing the inspector")
+
+
+if __name__ == "__main__":
+ mcp.run()
diff --git a/examples/apps/inventory/inventory_server.py b/examples/apps/inventory/inventory_server.py
new file mode 100644
index 000000000..f00862006
--- /dev/null
+++ b/examples/apps/inventory/inventory_server.py
@@ -0,0 +1,445 @@
+"""Inventory tracker -- a FastMCPApp example with CRUD operations and rich UI.
+
+Demonstrates the full FastMCPApp stack:
+- @app.ui() entry point that the model calls to open the app
+- @app.tool() backend tools for add, update, delete, and search
+- DataTable with sortable columns and built-in search
+- Form.from_model() for auto-generated Pydantic model forms
+- Tabs, Select filtering, ForEach results, and Toast notifications
+- State management with PrefabApp state dict and Rx()
+
+Usage:
+ uv run python inventory_server.py # HTTP (default)
+ uv run python inventory_server.py --stdio # stdio for MCP clients
+"""
+
+from __future__ import annotations
+
+from datetime import datetime
+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,
+ Card,
+ CardContent,
+ Column,
+ DataTable,
+ DataTableColumn,
+ ForEach,
+ Form,
+ Grid,
+ Heading,
+ Input,
+ Muted,
+ Row,
+ Select,
+ SelectOption,
+ Separator,
+ Tab,
+ Tabs,
+ Text,
+)
+from prefab_ui.rx import ERROR, RESULT, STATE, Rx
+from pydantic import BaseModel, Field
+
+from fastmcp import FastMCP, FastMCPApp
+
+# ---------------------------------------------------------------------------
+# Data store
+# ---------------------------------------------------------------------------
+
+_next_id = 11
+
+_inventory: list[dict] = [
+ {
+ "id": 1,
+ "name": "Wireless Mouse",
+ "category": "Electronics",
+ "quantity": 45,
+ "price": 29.99,
+ "last_updated": "2026-03-20",
+ },
+ {
+ "id": 2,
+ "name": "Mechanical Keyboard",
+ "category": "Electronics",
+ "quantity": 32,
+ "price": 89.99,
+ "last_updated": "2026-03-19",
+ },
+ {
+ "id": 3,
+ "name": "USB-C Hub",
+ "category": "Electronics",
+ "quantity": 18,
+ "price": 49.99,
+ "last_updated": "2026-03-18",
+ },
+ {
+ "id": 4,
+ "name": "A4 Copy Paper (500 sheets)",
+ "category": "Office Supplies",
+ "quantity": 200,
+ "price": 8.50,
+ "last_updated": "2026-03-21",
+ },
+ {
+ "id": 5,
+ "name": "Ballpoint Pens (box)",
+ "category": "Office Supplies",
+ "quantity": 150,
+ "price": 12.00,
+ "last_updated": "2026-03-20",
+ },
+ {
+ "id": 6,
+ "name": "Sticky Notes (pack)",
+ "category": "Office Supplies",
+ "quantity": 85,
+ "price": 5.99,
+ "last_updated": "2026-03-17",
+ },
+ {
+ "id": 7,
+ "name": "Standing Desk",
+ "category": "Furniture",
+ "quantity": 8,
+ "price": 499.00,
+ "last_updated": "2026-03-15",
+ },
+ {
+ "id": 8,
+ "name": "Ergonomic Chair",
+ "category": "Furniture",
+ "quantity": 12,
+ "price": 349.00,
+ "last_updated": "2026-03-16",
+ },
+ {
+ "id": 9,
+ "name": "Monitor Arm",
+ "category": "Furniture",
+ "quantity": 25,
+ "price": 79.99,
+ "last_updated": "2026-03-22",
+ },
+ {
+ "id": 10,
+ "name": "Webcam HD",
+ "category": "Electronics",
+ "quantity": 60,
+ "price": 69.99,
+ "last_updated": "2026-03-21",
+ },
+]
+
+CATEGORIES = ["All", "Electronics", "Office Supplies", "Furniture"]
+
+
+# ---------------------------------------------------------------------------
+# Pydantic model for add-item form
+# ---------------------------------------------------------------------------
+
+
+class NewItem(BaseModel):
+ name: str = Field(title="Item Name", min_length=1)
+ category: Literal["Electronics", "Office Supplies", "Furniture"] = Field(
+ title="Category",
+ default="Electronics",
+ )
+ quantity: int = Field(title="Quantity", ge=0, default=1)
+ price: float = Field(title="Unit Price ($)", ge=0.0, default=0.0)
+
+
+# ---------------------------------------------------------------------------
+# App and tools
+# ---------------------------------------------------------------------------
+
+app = FastMCPApp("Inventory")
+
+
+@app.tool()
+def add_item(data: NewItem) -> list[dict]:
+ """Add a new item to inventory and return the full list."""
+ global _next_id
+ item = {
+ "id": _next_id,
+ "name": data.name,
+ "category": data.category,
+ "quantity": data.quantity,
+ "price": data.price,
+ "last_updated": datetime.now().strftime("%Y-%m-%d"),
+ }
+ _next_id += 1
+ _inventory.append(item)
+ return list(_inventory)
+
+
+@app.tool()
+def update_quantity(item_id: int, delta: int) -> list[dict]:
+ """Adjust an item's quantity by delta (+/-) and return the full list."""
+ for item in _inventory:
+ if item["id"] == item_id:
+ new_qty = max(0, item["quantity"] + delta)
+ item["quantity"] = new_qty
+ item["last_updated"] = datetime.now().strftime("%Y-%m-%d")
+ break
+ return list(_inventory)
+
+
+@app.tool()
+def delete_item(item_id: int) -> list[dict]:
+ """Remove an item by ID and return the remaining inventory."""
+ for i, item in enumerate(_inventory):
+ if item["id"] == item_id:
+ _inventory.pop(i)
+ break
+ return list(_inventory)
+
+
+@app.tool()
+def search_items(query: str) -> list[dict]:
+ """Search items by name (case-insensitive). Returns matching items."""
+ q = query.lower()
+ return [item for item in _inventory if q in item["name"].lower()]
+
+
+@app.tool()
+def filter_by_category(category: str) -> list[dict]:
+ """Filter inventory by category. Pass 'All' to show everything."""
+ if category == "All":
+ return list(_inventory)
+ return [item for item in _inventory if item["category"] == category]
+
+
+# ---------------------------------------------------------------------------
+# UI helpers
+# ---------------------------------------------------------------------------
+
+
+def _build_inventory_table() -> None:
+ """Render the main DataTable with all current items."""
+ DataTable(
+ columns=[
+ DataTableColumn(key="name", header="Name", sortable=True),
+ DataTableColumn(key="category", header="Category", sortable=True),
+ DataTableColumn(key="quantity", header="Qty", sortable=True),
+ DataTableColumn(key="price", header="Price ($)", sortable=True),
+ DataTableColumn(key="last_updated", header="Updated", sortable=True),
+ ],
+ rows=list(_inventory),
+ search=True,
+ paginated=True,
+ page_size=10,
+ )
+
+
+def _build_search_section() -> None:
+ """Render the search form with ForEach results."""
+ Heading("Search Items", level=3)
+ Muted("Search by name across all inventory items.")
+
+ with Form(
+ on_submit=CallTool(
+ search_items,
+ arguments={"query": STATE.query},
+ on_success=SetState("search_results", RESULT),
+ )
+ ):
+ Input(name="query", placeholder="Search by name...")
+ Button("Search")
+
+ with ForEach("search_results") as result:
+ with Card(css_class="mb-2"):
+ with CardContent():
+ with Row(gap=3, align="center"):
+ Text(result.name, css_class="font-medium")
+ Badge(result.category)
+ Text(result.quantity)
+ Muted("in stock")
+
+
+def _build_add_form() -> None:
+ """Render the add-item form using Form.from_model()."""
+ Heading("Add New Item", level=3)
+ Muted("Fill out the form below to add a new item to inventory.")
+
+ Form.from_model(
+ NewItem,
+ submit_label="Add Item",
+ on_submit=CallTool(
+ add_item,
+ on_success=[
+ SetState("recent_additions", RESULT),
+ ShowToast("Item added!", variant="success"),
+ ],
+ on_error=ShowToast(ERROR, variant="error"),
+ ),
+ )
+
+
+def _build_actions_section() -> None:
+ """Render category filter, quantity adjustment, and delete controls."""
+
+ # Category filter
+ Heading("Filter by Category", level=3)
+ Muted("Select a category to see matching items.")
+
+ with Form(
+ on_submit=CallTool(
+ filter_by_category,
+ arguments={"category": STATE.selected_category},
+ on_success=SetState("filtered_items", RESULT),
+ )
+ ):
+ with Select(name="selected_category", placeholder="Choose a category..."):
+ for cat in CATEGORIES:
+ SelectOption(cat, value=cat)
+ Button("Apply Filter")
+
+ with ForEach("filtered_items") as item:
+ with Row(gap=3, align="center", css_class="py-1"):
+ Badge(item.id, variant="outline")
+ Text(item.name, css_class="font-medium")
+ Badge(item.category)
+ Muted(item.quantity)
+
+ Separator()
+
+ # Quantity adjustment
+ Heading("Adjust Quantity", level=3)
+ Muted("Enter an item ID and use the buttons to adjust stock levels.")
+
+ Input(name="adjust_id", input_type="number", placeholder="Item ID (e.g. 1)")
+
+ with Row(gap=2):
+ Button(
+ "- 1",
+ variant="outline",
+ on_click=CallTool(
+ update_quantity,
+ arguments={"item_id": STATE.adjust_id, "delta": -1},
+ on_success=[
+ SetState("filtered_items", RESULT),
+ ShowToast("Quantity decreased", variant="default"),
+ ],
+ on_error=ShowToast(ERROR, variant="error"),
+ ),
+ )
+ Button(
+ "+ 1",
+ variant="outline",
+ on_click=CallTool(
+ update_quantity,
+ arguments={"item_id": STATE.adjust_id, "delta": 1},
+ on_success=[
+ SetState("filtered_items", RESULT),
+ ShowToast("Quantity increased", variant="default"),
+ ],
+ on_error=ShowToast(ERROR, variant="error"),
+ ),
+ )
+ Button(
+ "+ 10",
+ on_click=CallTool(
+ update_quantity,
+ arguments={"item_id": STATE.adjust_id, "delta": 10},
+ on_success=[
+ SetState("filtered_items", RESULT),
+ ShowToast("Restocked +10", variant="success"),
+ ],
+ on_error=ShowToast(ERROR, variant="error"),
+ ),
+ )
+
+ Separator()
+
+ # Delete
+ Heading("Delete Item", level=3)
+ Muted("Permanently remove an item by its ID.")
+
+ with Form(
+ on_submit=CallTool(
+ delete_item,
+ arguments={"item_id": STATE.delete_id},
+ on_success=[
+ SetState("filtered_items", RESULT),
+ ShowToast("Item deleted", variant="warning"),
+ ],
+ on_error=ShowToast(ERROR, variant="error"),
+ )
+ ):
+ Input(name="delete_id", input_type="number", placeholder="Item ID to delete")
+ Button("Delete", variant="destructive")
+
+
+# ---------------------------------------------------------------------------
+# Entry point UI
+# ---------------------------------------------------------------------------
+
+
+@app.ui()
+def inventory_manager() -> PrefabApp:
+ """Open the inventory manager. The model calls this to launch the app."""
+ with Column(gap=6, css_class="p-6") as view:
+ with Row(gap=3, align="center"):
+ Heading("Inventory Tracker")
+ Badge(
+ Rx("filtered_items.length"),
+ variant="secondary",
+ )
+ Muted("items tracked")
+
+ Separator()
+
+ # Summary cards per category
+ with Grid(columns=3, gap=4):
+ for cat in ["Electronics", "Office Supplies", "Furniture"]:
+ count = sum(1 for it in _inventory if it["category"] == cat)
+ total_qty = sum(
+ it["quantity"] for it in _inventory if it["category"] == cat
+ )
+ with Card():
+ with CardContent():
+ Text(cat, css_class="font-medium")
+ Muted(f"{count} items, {total_qty} units")
+
+ with Tabs():
+ with Tab("All Items"):
+ _build_inventory_table()
+
+ with Tab("Search"):
+ _build_search_section()
+
+ with Tab("Add Item"):
+ _build_add_form()
+
+ with Tab("Actions"):
+ _build_actions_section()
+
+ return PrefabApp(
+ view=view,
+ state={
+ "search_results": [],
+ "filtered_items": list(_inventory),
+ "recent_additions": [],
+ "selected_category": "All",
+ "adjust_id": "",
+ "delete_id": "",
+ "query": "",
+ },
+ )
+
+
+# ---------------------------------------------------------------------------
+# Server
+# ---------------------------------------------------------------------------
+
+mcp = FastMCP("Inventory Server", providers=[app])
+
+if __name__ == "__main__":
+ mcp.run(transport="http")
diff --git a/examples/apps/map/map_server.py b/examples/apps/map/map_server.py
new file mode 100644
index 000000000..3711a5584
--- /dev/null
+++ b/examples/apps/map/map_server.py
@@ -0,0 +1,164 @@
+"""Interactive Map — geocode addresses and render on an interactive map.
+
+Accepts plain addresses (or place names), geocodes them via
+OpenStreetMap Nominatim, and renders an interactive Leaflet map.
+
+Usage:
+ fastmcp dev apps map_server.py
+"""
+
+from __future__ import annotations
+
+from textwrap import dedent
+
+import httpx2
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import (
+ Badge,
+ Card,
+ Column,
+ Embed,
+ Heading,
+ Muted,
+)
+from prefab_ui.components.data_table import DataTable, DataTableColumn
+
+from fastmcp import FastMCP
+
+mcp = FastMCP("Interactive Map")
+
+NOMINATIM_URL = "https://nominatim.openstreetmap.org/search"
+
+
+def _geocode(query: str) -> dict | None:
+ """Geocode an address using OpenStreetMap Nominatim (free, no key)."""
+ resp = httpx2.get(
+ NOMINATIM_URL,
+ params={"q": query, "format": "json", "limit": 1},
+ headers={"User-Agent": "fastmcp-map-example/1.0"},
+ timeout=10,
+ )
+ results = resp.json()
+ if results:
+ r = results[0]
+ return {
+ "name": r.get("display_name", query).split(",")[0],
+ "address": query,
+ "lat": float(r["lat"]),
+ "lng": float(r["lon"]),
+ }
+ return None
+
+
+def _build_map_html(
+ locations: list[dict],
+ zoom: int,
+) -> str:
+ markers_js = ""
+ for loc in locations:
+ name = str(loc["name"]).replace("\\", "\\\\").replace("'", "\\'")
+ markers_js += (
+ f"L.marker([{loc['lat']}, {loc['lng']}]).addTo(map).bindPopup('{name}');\n"
+ )
+
+ avg_lat = sum(loc["lat"] for loc in locations) / len(locations)
+ avg_lng = sum(loc["lng"] for loc in locations) / len(locations)
+
+ return dedent(f"""\
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """)
+
+
+@mcp.tool(app=True)
+def show_map(
+ locations: list[str] | None = None,
+ title: str = "Map",
+ zoom: int = 2,
+) -> PrefabApp:
+ """Show locations on an interactive map.
+
+ Accepts addresses, place names, or landmarks. Each location is
+ geocoded via OpenStreetMap and displayed as a marker on an
+ interactive Leaflet map.
+
+ Args:
+ locations: List of addresses or place names. Defaults to
+ sample US landmarks if not provided.
+ title: Heading for the map.
+ zoom: Initial zoom level (1-18, higher = closer).
+ """
+ if not locations:
+ locations = [
+ "Statue of Liberty, New York",
+ "Golden Gate Bridge, San Francisco",
+ "Space Needle, Seattle",
+ "Willis Tower, Chicago",
+ "Gateway Arch, St. Louis",
+ ]
+
+ geocoded = []
+ failed = []
+ for loc in locations:
+ result = _geocode(loc)
+ if result:
+ geocoded.append(result)
+ else:
+ failed.append(loc)
+
+ with PrefabApp() as app:
+ with Column(gap=4, css_class="p-6"):
+ Heading(title)
+ Muted(f"{len(geocoded)} locations mapped")
+ if failed:
+ for f in failed:
+ Badge(f"Could not find: {f}", variant="destructive")
+
+ if geocoded:
+ map_html = _build_map_html(geocoded, zoom)
+ with Card():
+ Embed(
+ html=map_html,
+ width="100%",
+ height="500px",
+ sandbox="allow-scripts",
+ )
+ DataTable(
+ columns=[
+ DataTableColumn(key="name", header="Name", sortable=True),
+ DataTableColumn(key="address", header="Address", sortable=True),
+ DataTableColumn(key="lat", header="Latitude", sortable=True),
+ DataTableColumn(key="lng", header="Longitude", sortable=True),
+ ],
+ rows=geocoded,
+ search=True,
+ )
+
+ return app
+
+
+if __name__ == "__main__":
+ mcp.run()
diff --git a/examples/apps/patterns_server.py b/examples/apps/patterns_server.py
index 9b5b8ac79..abf888c1c 100644
--- a/examples/apps/patterns_server.py
+++ b/examples/apps/patterns_server.py
@@ -18,13 +18,10 @@ from prefab_ui.components import (
Accordion,
AccordionItem,
Alert,
- AreaChart,
Badge,
- BarChart,
Button,
Card,
CardContent,
- ChartSeries,
Column,
DataTable,
DataTableColumn,
@@ -35,7 +32,6 @@ from prefab_ui.components import (
If,
Input,
Muted,
- PieChart,
Progress,
Row,
Select,
@@ -46,6 +42,8 @@ from prefab_ui.components import (
Text,
Textarea,
)
+from prefab_ui.components.charts import AreaChart, BarChart, ChartSeries, PieChart
+from prefab_ui.rx import ERROR, Rx
from fastmcp import FastMCP
@@ -297,7 +295,7 @@ def employee_directory() -> PrefabApp:
DataTableColumn(key="location", header="Office", sortable=True),
],
rows=EMPLOYEES,
- searchable=True,
+ search=True,
paginated=True,
page_size=15,
)
@@ -316,11 +314,11 @@ def contact_form() -> PrefabApp:
with Column(gap=6, css_class="p-6") as view:
Heading("Contacts")
- with ForEach("contacts"):
+ with ForEach("contacts") as item:
with Row(gap=2, align="center"):
- Text("{{ name }}", css_class="font-medium")
- Muted("{{ email }}")
- Badge("{{ category }}")
+ Text(item.name, css_class="font-medium")
+ Muted(item.email)
+ Badge(item.category)
Separator()
@@ -330,7 +328,7 @@ def contact_form() -> PrefabApp:
"save_contact",
result_key="contacts",
on_success=ShowToast("Contact saved!", variant="success"),
- on_error=ShowToast("{{ $error }}", variant="error"),
+ on_error=ShowToast(ERROR, variant="error"),
)
):
Input(name="name", label="Full Name", required=True)
@@ -411,9 +409,9 @@ def feature_flags() -> PrefabApp:
Separator()
- with If("{{ dark_mode }}"):
+ with If(Rx("dark_mode")):
Alert(title="Dark mode enabled", description="UI will use dark theme.")
- with If("{{ beta_features }}"):
+ with If(Rx("beta_features")):
Alert(
title="Beta features active",
description="Experimental features are now visible.",
@@ -451,10 +449,10 @@ def project_overview() -> PrefabApp:
)
with Tab("Activity"):
- with ForEach("activity"):
+ with ForEach("activity") as item:
with Row(gap=2):
- Muted("{{ timestamp }}")
- Text("{{ message }}")
+ Muted(item.timestamp)
+ Text(item.message)
return PrefabApp(view=view, state={"activity": PROJECT["activity"]})
diff --git a/examples/apps/qr_server/qr_server.py b/examples/apps/qr_server/qr_server.py
index 7a3d5ee64..28ea8d4d1 100644
--- a/examples/apps/qr_server/qr_server.py
+++ b/examples/apps/qr_server/qr_server.py
@@ -23,10 +23,10 @@ import base64
import io
import qrcode # type: ignore[import-untyped]
-from mcp import types
+from mcp_types import ImageContent
from fastmcp import FastMCP
-from fastmcp.server.apps import AppConfig, ResourceCSP
+from fastmcp.apps import AppConfig, ResourceCSP
from fastmcp.tools import ToolResult
VIEW_URI: str = "ui://qr-server/view.html"
@@ -153,7 +153,7 @@ def generate_qr(
img.save(buffer, format="PNG")
b64 = base64.b64encode(buffer.getvalue()).decode()
return ToolResult(
- content=[types.ImageContent(type="image", data=b64, mimeType="image/png")]
+ content=[ImageContent(type="image", data=b64, mime_type="image/png")]
)
diff --git a/examples/apps/quiz/quiz_server.py b/examples/apps/quiz/quiz_server.py
new file mode 100644
index 000000000..f4eacedc3
--- /dev/null
+++ b/examples/apps/quiz/quiz_server.py
@@ -0,0 +1,266 @@
+"""Quiz / trivia app — a FastMCPApp example with multi-turn state.
+
+Demonstrates building state over a conversation:
+- The LLM generates quiz questions and calls `take_quiz` to launch the UI
+- The user answers via multiple-choice buttons (no forms)
+- Each answer calls `submit_answer`, which returns correctness + updated score
+- After the final question, a SendMessage pushes the score back to the LLM
+
+Usage:
+ uv run python quiz_server.py
+"""
+
+from __future__ import annotations
+
+from prefab_ui.actions import SetState, ShowToast
+from prefab_ui.actions.mcp import CallTool, SendMessage
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import (
+ Badge,
+ Button,
+ Card,
+ Column,
+ Heading,
+ If,
+ Muted,
+ Progress,
+ Row,
+ Text,
+)
+from prefab_ui.rx import ERROR, RESULT, Rx
+from typing_extensions import TypedDict
+
+from fastmcp import FastMCP, FastMCPApp
+
+app = FastMCPApp("Quiz")
+
+
+class Question(TypedDict):
+ question: str
+ options: list[str]
+ correct: int
+
+
+DEFAULT_QUESTIONS: list[Question] = [
+ {
+ "question": "What is the capital of Australia?",
+ "options": ["Sydney", "Melbourne", "Canberra", "Perth"],
+ "correct": 2,
+ },
+ {
+ "question": "Which planet has the most moons?",
+ "options": ["Jupiter", "Saturn", "Uranus", "Neptune"],
+ "correct": 1,
+ },
+ {
+ "question": "What year did the Berlin Wall fall?",
+ "options": ["1987", "1989", "1991", "1993"],
+ "correct": 1,
+ },
+ {
+ "question": "Which element has the chemical symbol 'Au'?",
+ "options": ["Silver", "Aluminum", "Gold", "Argon"],
+ "correct": 2,
+ },
+ {
+ "question": "What is the deepest ocean?",
+ "options": ["Atlantic", "Indian", "Arctic", "Pacific"],
+ "correct": 3,
+ },
+]
+
+
+# ---------------------------------------------------------------------------
+# Backend tool — grade an answer and advance state
+# ---------------------------------------------------------------------------
+
+
+@app.tool()
+def submit_answer(
+ question_index: int,
+ selected: int,
+ correct: int,
+ total_questions: int,
+ current_score: int,
+) -> dict:
+ """Grade an answer and return the updated quiz state.
+
+ Returns a dict with:
+ - is_correct: whether the selected answer matched the correct index
+ - new_score: the updated cumulative score
+ - answered_index: the question that was just answered
+ - finished: whether this was the last question
+ """
+ is_correct = selected == correct
+ new_score = current_score + (1 if is_correct else 0)
+ finished = (question_index + 1) >= total_questions
+ return {
+ "is_correct": is_correct,
+ "new_score": new_score,
+ "answered_index": question_index,
+ "finished": finished,
+ }
+
+
+# ---------------------------------------------------------------------------
+# UI entry point — the LLM calls this with a topic and generated questions
+# ---------------------------------------------------------------------------
+
+
+@app.ui()
+def take_quiz(
+ topic: str = "General Knowledge",
+ questions: list[Question] | None = None,
+) -> PrefabApp:
+ """Launch a quiz UI.
+
+ The LLM generates the questions and passes them in:
+ - topic: displayed as the heading (e.g. "World Capitals")
+ - questions: list of dicts, each with:
+ - "question": the question text
+ - "options": list of answer strings
+ - "correct": index of the correct option
+
+ If no questions are provided, a built-in set is used.
+ """
+ if questions is None:
+ questions = DEFAULT_QUESTIONS
+ total = len(questions)
+ score = Rx("score")
+ current_q = Rx("current_question")
+ answered = Rx("answered")
+
+ with Column(gap=6, css_class="p-6 max-w-2xl") as view:
+ Heading(f"Quiz: {topic}")
+
+ with Row(gap=3, align="center"):
+ Badge(f"{score}/{total} correct", variant="secondary")
+ Progress(value=current_q, max=total, size="sm")
+
+ for i, q in enumerate(questions):
+ visible = current_q == i
+ options = q["options"]
+ correct_idx = q["correct"]
+
+ with If(visible):
+ with Card():
+ with Column(gap=4, css_class="p-4"):
+ Text(
+ f"Question {i + 1} of {total}",
+ css_class="text-sm font-medium text-muted-foreground",
+ )
+ Heading(q["question"], level=3)
+
+ with If(~answered):
+ with Column(gap=2):
+ for opt_idx, option in enumerate(options):
+ on_success_actions = [
+ SetState("answered", True),
+ SetState(
+ "last_correct",
+ RESULT.is_correct,
+ ),
+ SetState("score", RESULT.new_score),
+ ]
+ is_last = (i + 1) >= total
+ if is_last:
+ on_success_actions.append(
+ SetState("finished", True),
+ )
+
+ Button(
+ option,
+ variant="outline",
+ css_class="w-full justify-start",
+ on_click=CallTool(
+ submit_answer,
+ arguments={
+ "question_index": i,
+ "selected": opt_idx,
+ "correct": correct_idx,
+ "total_questions": total,
+ "current_score": str(score),
+ },
+ on_success=on_success_actions,
+ on_error=ShowToast(
+ ERROR,
+ variant="error",
+ ),
+ ),
+ )
+
+ with If(answered):
+ with Column(gap=2):
+ for opt_idx, option in enumerate(options):
+ if opt_idx == correct_idx:
+ Button(
+ f"{option}",
+ variant="success",
+ css_class="w-full justify-start",
+ disabled=True,
+ )
+ else:
+ Button(
+ option,
+ variant="ghost",
+ css_class="w-full justify-start opacity-50",
+ disabled=True,
+ )
+
+ with If(Rx("last_correct")):
+ Badge("Correct!", variant="success")
+ with If(~Rx("last_correct")):
+ Badge(
+ f"Incorrect — answer: {options[correct_idx]}",
+ variant="destructive",
+ )
+
+ with If(answered & ~Rx("finished")):
+ Button(
+ "Next Question",
+ variant="default",
+ on_click=[
+ SetState("current_question", current_q + 1),
+ SetState("answered", False),
+ SetState("last_correct", False),
+ ],
+ )
+
+ with If(Rx("finished") & answered):
+ with Card(css_class="border-2 border-primary"):
+ with Column(gap=3, css_class="p-4 items-center text-center"):
+ Heading("Quiz Complete!", level=2)
+ Text(
+ f"{score}/{total} correct",
+ css_class="text-2xl font-bold",
+ )
+ Progress(
+ value=score,
+ max=total,
+ variant="success",
+ size="lg",
+ )
+ Muted("Click below to send your results to the conversation.")
+ Button(
+ "Send Results",
+ variant="default",
+ on_click=SendMessage(
+ f'Quiz complete! Topic: "{topic}" '
+ f"— Final score: {score}/{total} correct.",
+ ),
+ )
+
+ initial_state = {
+ "score": 0,
+ "current_question": 0,
+ "answered": False,
+ "last_correct": False,
+ "finished": False,
+ }
+ return PrefabApp(view=view, state=initial_state)
+
+
+mcp = FastMCP("Quiz Server", providers=[app])
+
+if __name__ == "__main__":
+ mcp.run(transport="http")
diff --git a/examples/apps/sales_dashboard/sales_dashboard_server.py b/examples/apps/sales_dashboard/sales_dashboard_server.py
new file mode 100644
index 000000000..04f3781a1
--- /dev/null
+++ b/examples/apps/sales_dashboard/sales_dashboard_server.py
@@ -0,0 +1,243 @@
+from typing import TypedDict
+
+from prefab_ui.components import (
+ Card,
+ CardContent,
+ Column,
+ Grid,
+ Heading,
+ Metric,
+ Muted,
+ Row,
+ Separator,
+ Text,
+)
+from prefab_ui.components.charts import AreaChart, ChartSeries, PieChart
+from prefab_ui.components.data_table import DataTable, DataTableColumn
+
+from fastmcp import FastMCP
+
+mcp = FastMCP("Sales Dashboard")
+
+
+class MonthlyRevenue(TypedDict):
+ month: str
+ new_business: int
+ expansion: int
+ renewal: int
+
+
+MONTHLY_REVENUE: list[MonthlyRevenue] = [
+ {"month": "Jul", "new_business": 182_000, "expansion": 74_000, "renewal": 210_000},
+ {"month": "Aug", "new_business": 195_000, "expansion": 81_000, "renewal": 215_000},
+ {"month": "Sep", "new_business": 224_000, "expansion": 93_000, "renewal": 208_000},
+ {"month": "Oct", "new_business": 210_000, "expansion": 88_000, "renewal": 222_000},
+ {"month": "Nov", "new_business": 248_000, "expansion": 102_000, "renewal": 230_000},
+ {"month": "Dec", "new_business": 271_000, "expansion": 115_000, "renewal": 238_000},
+ {"month": "Jan", "new_business": 235_000, "expansion": 97_000, "renewal": 241_000},
+ {"month": "Feb", "new_business": 262_000, "expansion": 108_000, "renewal": 245_000},
+ {"month": "Mar", "new_business": 289_000, "expansion": 121_000, "renewal": 252_000},
+ {"month": "Apr", "new_business": 305_000, "expansion": 134_000, "renewal": 258_000},
+ {"month": "May", "new_business": 318_000, "expansion": 142_000, "renewal": 263_000},
+ {"month": "Jun", "new_business": 342_000, "expansion": 156_000, "renewal": 270_000},
+]
+
+REVENUE_BY_SEGMENT = [
+ {"segment": "Enterprise", "revenue": 3_840_000},
+ {"segment": "Mid-Market", "revenue": 2_160_000},
+ {"segment": "SMB", "revenue": 1_440_000},
+ {"segment": "Startup", "revenue": 720_000},
+]
+
+RECENT_DEALS = [
+ {
+ "company": "Meridian Health Systems",
+ "amount": "$485,000",
+ "stage": "Closed Won",
+ "rep": "Sarah Chen",
+ "close_date": "Jun 12, 2026",
+ },
+ {
+ "company": "Atlas Financial Group",
+ "amount": "$372,000",
+ "stage": "Closed Won",
+ "rep": "Marcus Rivera",
+ "close_date": "Jun 10, 2026",
+ },
+ {
+ "company": "Pinnacle Manufacturing",
+ "amount": "$298,000",
+ "stage": "Negotiation",
+ "rep": "Aisha Patel",
+ "close_date": "Jun 28, 2026",
+ },
+ {
+ "company": "Crestview Logistics",
+ "amount": "$264,000",
+ "stage": "Proposal Sent",
+ "rep": "James O'Brien",
+ "close_date": "Jul 5, 2026",
+ },
+ {
+ "company": "Northstar Retail",
+ "amount": "$215,000",
+ "stage": "Closed Won",
+ "rep": "Sarah Chen",
+ "close_date": "Jun 8, 2026",
+ },
+ {
+ "company": "Ironclad Security",
+ "amount": "$189,000",
+ "stage": "Negotiation",
+ "rep": "Lena Kowalski",
+ "close_date": "Jul 1, 2026",
+ },
+ {
+ "company": "Summit Analytics",
+ "amount": "$176,000",
+ "stage": "Closed Won",
+ "rep": "Marcus Rivera",
+ "close_date": "Jun 5, 2026",
+ },
+ {
+ "company": "Brightpath Education",
+ "amount": "$142,000",
+ "stage": "Proposal Sent",
+ "rep": "Aisha Patel",
+ "close_date": "Jul 12, 2026",
+ },
+ {
+ "company": "Vantage Media",
+ "amount": "$128,000",
+ "stage": "Closed Won",
+ "rep": "Lena Kowalski",
+ "close_date": "Jun 3, 2026",
+ },
+ {
+ "company": "Redwood Hospitality",
+ "amount": "$97,000",
+ "stage": "Discovery",
+ "rep": "James O'Brien",
+ "close_date": "Jul 20, 2026",
+ },
+]
+
+
+@mcp.tool(app=True)
+def sales_dashboard() -> Column:
+ """Company sales dashboard with KPIs, revenue trends, segment breakdown, and recent deals."""
+ total_revenue = sum(
+ row["new_business"] + row["expansion"] + row["renewal"]
+ for row in MONTHLY_REVENUE
+ )
+ current_quarter = sum(
+ row["new_business"] + row["expansion"] + row["renewal"]
+ for row in MONTHLY_REVENUE[-3:]
+ )
+ prior_quarter = sum(
+ row["new_business"] + row["expansion"] + row["renewal"]
+ for row in MONTHLY_REVENUE[-6:-3]
+ )
+ growth_pct = (current_quarter - prior_quarter) / prior_quarter * 100
+
+ with Column(gap=6, css_class="p-6") as view:
+ with Row(gap=2, align="center"):
+ Heading("Sales Dashboard")
+ Muted("FY2026 | Last updated Jun 15, 2026")
+
+ with Grid(columns=4, gap=4):
+ with Card():
+ with CardContent():
+ Metric(
+ label="Total Revenue",
+ value=f"${total_revenue / 1_000_000:.1f}M",
+ delta="+18.2% YoY",
+ trend="up",
+ )
+
+ with Card():
+ with CardContent():
+ Metric(
+ label="Quarterly Growth",
+ value=f"{growth_pct:.1f}%",
+ delta="+3.8pp vs prior",
+ trend="up",
+ )
+
+ with Card():
+ with CardContent():
+ Metric(
+ label="Active Customers",
+ value="1,847",
+ delta="+124 this quarter",
+ trend="up",
+ )
+
+ with Card():
+ with CardContent():
+ Metric(
+ label="Avg Deal Size",
+ value="$236K",
+ delta="+12% vs H1",
+ trend="up",
+ )
+
+ with Grid(columns=3, gap=6):
+ with Card(css_class="col-span-2"):
+ with CardContent():
+ Text(
+ "Monthly Revenue",
+ css_class="text-sm font-medium text-muted-foreground mb-2",
+ )
+ AreaChart(
+ data=MONTHLY_REVENUE,
+ series=[
+ ChartSeries(data_key="new_business", label="New Business"),
+ ChartSeries(data_key="expansion", label="Expansion"),
+ ChartSeries(data_key="renewal", label="Renewal"),
+ ],
+ x_axis="month",
+ stacked=True,
+ curve="smooth",
+ show_legend=True,
+ height=280,
+ y_axis_format="compact",
+ )
+
+ with Card():
+ with CardContent():
+ Text(
+ "Revenue by Segment",
+ css_class="text-sm font-medium text-muted-foreground mb-2",
+ )
+ PieChart(
+ data=REVENUE_BY_SEGMENT,
+ data_key="revenue",
+ name_key="segment",
+ show_legend=True,
+ inner_radius=50,
+ height=280,
+ )
+
+ Separator()
+
+ Text("Recent Deals", css_class="text-lg font-semibold")
+
+ DataTable(
+ columns=[
+ DataTableColumn(key="company", header="Company", sortable=True),
+ DataTableColumn(key="amount", header="Amount", sortable=True),
+ DataTableColumn(key="stage", header="Stage", sortable=True),
+ DataTableColumn(key="rep", header="Sales Rep", sortable=True),
+ DataTableColumn(key="close_date", header="Close Date", sortable=True),
+ ],
+ rows=RECENT_DEALS,
+ search=True,
+ paginated=True,
+ )
+
+ return view
+
+
+if __name__ == "__main__":
+ mcp.run()
diff --git a/examples/apps/showcase_server.py b/examples/apps/showcase_server.py
new file mode 100644
index 000000000..4368a7f9c
--- /dev/null
+++ b/examples/apps/showcase_server.py
@@ -0,0 +1,345 @@
+# ruff: noqa: F405
+"""Component showcase — demonstrates the breadth of Prefab UI components.
+
+Usage:
+ uv run python showcase_server.py
+"""
+
+from prefab_ui.actions import SetState, ShowToast
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import * # noqa: F403, F405
+from prefab_ui.components.charts import * # noqa: F403, F405
+from prefab_ui.components.control_flow import Else, If
+
+from fastmcp import FastMCP
+
+mcp = FastMCP("Showcase")
+
+
+@mcp.tool(app=True)
+def showcase() -> PrefabApp:
+ """Prefab UI component showcase."""
+ with Grid(columns={"default": 1, "md": 2, "lg": 4}, gap=4, css_class="p-4") as view:
+ # ── 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):
+ owner_input = Input(placeholder="Owner name...", name="owner")
+ 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")
+ with If("{{ owner }}"):
+ Text(
+ f"Thanks, {owner_input.rx}. Don't forget to bring it."
+ )
+ with Else():
+ Text("Anonymous, I see? Don't forget to bring it.")
+ Button("Cancel", variant="outline")
+ with Card():
+ with CardContent():
+ with Row(gap=2, align="center"):
+ Loader(variant="dots", size="sm")
+ Muted("Marvin is thinking...")
+
+ 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 Alert(variant="success", icon="circle-check"):
+ AlertTitle("Don't Panic")
+ AlertDescription("Normality achieved.")
+ 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 3–4 ──────────────────────────────────────────────────
+ 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("45% used")
+ Muted("90k / 200k tokens")
+ with Tooltip("Auto-compact buffer: 12%", delay=0):
+ Progress(value=45, max=100)
+ 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 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("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 Alert(variant="destructive", icon="triangle-alert"):
+ AlertTitle("Beware of the Leopard")
+ 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,
+ )
+
+ return PrefabApp(view=view, state={"pressed": False, "improbability": 42})
+
+
+if __name__ == "__main__":
+ mcp.run()
diff --git a/examples/apps/system_monitor/system_monitor_server.py b/examples/apps/system_monitor/system_monitor_server.py
new file mode 100644
index 000000000..7105a3697
--- /dev/null
+++ b/examples/apps/system_monitor/system_monitor_server.py
@@ -0,0 +1,195 @@
+"""System monitor — live CPU, memory, and disk stats from the host machine.
+
+Auto-refreshes every 3 seconds via SetInterval + CallTool.
+
+Requires psutil: pip install psutil
+
+Usage:
+ fastmcp dev apps system_monitor_server.py
+"""
+
+import platform
+import time
+from datetime import datetime
+
+import psutil
+from prefab_ui.actions import SetInterval, SetState
+from prefab_ui.actions.mcp import CallTool
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import (
+ Badge,
+ Card,
+ CardContent,
+ CardHeader,
+ Column,
+ Grid,
+ Heading,
+ Metric,
+ Muted,
+ Progress,
+ Row,
+ Select,
+ SelectOption,
+ Small,
+ Text,
+)
+from prefab_ui.components.charts import AreaChart, ChartSeries
+from prefab_ui.components.control_flow import ForEach
+from prefab_ui.rx import RESULT, STATE, Rx
+
+from fastmcp import FastMCP
+from fastmcp.apps.app import FastMCPApp
+
+app = FastMCPApp("Monitor")
+
+_history: list[dict] = []
+
+
+def _collect_stats() -> dict:
+ """Collect a full snapshot of system stats."""
+ cpu = psutil.cpu_percent(interval=0.1)
+ mem = psutil.virtual_memory()
+ disk = psutil.disk_usage("/")
+
+ now = datetime.now().strftime("%H:%M:%S")
+ _history.append({"time": now, "cpu": cpu, "memory": mem.percent})
+ if len(_history) > 100:
+ del _history[: len(_history) - 100]
+
+ top_procs = []
+ for p in sorted(
+ psutil.process_iter(["pid", "name", "cpu_percent", "memory_percent"]),
+ key=lambda p: p.info.get("cpu_percent") or 0,
+ reverse=True,
+ )[:6]:
+ info = p.info
+ top_procs.append(
+ {
+ "pid": info.get("pid") or 0,
+ "name": info.get("name") or "unknown",
+ "cpu": f"{(info.get('cpu_percent') or 0):.1f}%",
+ "memory": f"{(info.get('memory_percent') or 0):.1f}%",
+ }
+ )
+
+ return {
+ "cpu": cpu,
+ "mem_pct": mem.percent,
+ "mem_used": mem.used // (1024**3),
+ "mem_total": mem.total // (1024**3),
+ "disk_pct": disk.percent,
+ "disk_used": disk.used // (1024**3),
+ "disk_total": disk.total // (1024**3),
+ "uptime": _format_uptime(),
+ "cores": psutil.cpu_count(),
+ "platform": f"{platform.system()} {platform.machine()}",
+ "hostname": platform.node(),
+ "healthy": cpu < 80 and mem.percent < 90,
+ "history": list(_history),
+ "top_procs": top_procs,
+ }
+
+
+def _format_uptime() -> str:
+ elapsed = int(time.time() - psutil.boot_time())
+ days, remainder = divmod(elapsed, 86400)
+ hours, remainder = divmod(remainder, 3600)
+ minutes, _ = divmod(remainder, 60)
+ if days > 0:
+ return f"{days}d {hours}h {minutes}m"
+ return f"{hours}h {minutes}m"
+
+
+@app.tool()
+def refresh() -> dict:
+ """Collect fresh system stats."""
+ return _collect_stats()
+
+
+@app.ui()
+def system_dashboard() -> PrefabApp:
+ """Live system dashboard with auto-refresh."""
+ initial = _collect_stats()
+
+ with PrefabApp(state={"stats": initial, "interval": "500"}) as ui:
+ with Column(
+ gap=6,
+ css_class="p-6",
+ on_mount=SetInterval(
+ duration=Rx("interval"),
+ on_tick=CallTool(
+ "refresh",
+ on_success=SetState("stats", RESULT),
+ ),
+ ),
+ ):
+ with Row(gap=3, align="center"):
+ Heading("System Monitor")
+ Badge(STATE.stats.hostname, variant="outline")
+ with Select(name="interval", css_class="w-32"):
+ SelectOption("0.5s", value="500")
+ SelectOption("1s", value="1000")
+ SelectOption("5s", value="5000")
+
+ with Grid(columns=4, gap=4):
+ with Card():
+ with CardContent():
+ Metric(label="CPU", value=f"{STATE.stats.cpu}%")
+ Progress(value=STATE.stats.cpu)
+
+ with Card():
+ with CardContent():
+ Metric(label="Memory", value=f"{STATE.stats.mem_pct}%")
+ Progress(value=STATE.stats.mem_pct)
+ Muted(f"{STATE.stats.mem_used}GB / {STATE.stats.mem_total}GB")
+
+ with Card():
+ with CardContent():
+ Metric(label="Disk", value=f"{STATE.stats.disk_pct}%")
+ Progress(value=STATE.stats.disk_pct)
+ Muted(f"{STATE.stats.disk_used}GB / {STATE.stats.disk_total}GB")
+
+ with Card():
+ with CardContent():
+ Metric(label="Uptime", value=STATE.stats.uptime)
+ Muted(f"{STATE.stats.cores} cores")
+
+ with Grid(columns=[2, 1], gap=4):
+ with Card():
+ with CardHeader():
+ Text("CPU & Memory", css_class="text-sm font-medium")
+ with CardContent():
+ AreaChart(
+ data=STATE.stats.history,
+ series=[
+ ChartSeries(data_key="cpu", label="CPU %"),
+ ChartSeries(data_key="memory", label="Memory %"),
+ ],
+ x_axis="time",
+ curve="smooth",
+ show_legend=True,
+ height=220,
+ animate=False,
+ )
+
+ with Card():
+ with CardHeader():
+ Text("Top Processes", css_class="text-sm font-medium")
+ with CardContent():
+ with Column(gap=2):
+ with ForEach("stats.top_procs") as proc:
+ with Row(justify="between", align="center"):
+ with Column(gap=0):
+ Small(proc.name)
+ Muted(proc.pid)
+ with Row(gap=2):
+ Badge(proc.cpu, variant="outline")
+ Badge(proc.memory, variant="outline")
+
+ return ui
+
+
+mcp = FastMCP("System Monitor", providers=[app])
+
+if __name__ == "__main__":
+ mcp.run()
diff --git a/examples/atproto_mcp/src/atproto_mcp/_atproto/_posts.py b/examples/atproto_mcp/src/atproto_mcp/_atproto/_posts.py
index daf8c340a..58cce2901 100644
--- a/examples/atproto_mcp/src/atproto_mcp/_atproto/_posts.py
+++ b/examples/atproto_mcp/src/atproto_mcp/_atproto/_posts.py
@@ -224,7 +224,7 @@ def _build_quote_with_images_embed(
quote_uri: str, image_urls: list[str], image_alts: list[str] | None, client
):
"""Build quote embed with images."""
- import httpx
+ import httpx2
# Get the quoted post
quoted_post = client.app.bsky.feed.get_posts(params={"uris": [quote_uri]})
@@ -239,7 +239,7 @@ def _build_quote_with_images_embed(
alts = image_alts or [""] * len(image_urls)
for i, url in enumerate(image_urls[:4]):
- response = httpx.get(url, follow_redirects=True)
+ response = httpx2.get(url, follow_redirects=True)
response.raise_for_status()
# Upload to blob storage
@@ -267,7 +267,7 @@ def _send_images(
client,
):
"""Send post with images using the client's send_images method."""
- import httpx
+ import httpx2
# Ensure alt_texts has same length as images
if image_alts is None:
@@ -279,7 +279,7 @@ def _send_images(
alts = []
for i, url in enumerate(image_urls[:4]): # Max 4 images
# Download image (follow redirects)
- response = httpx.get(url, follow_redirects=True)
+ response = httpx2.get(url, follow_redirects=True)
response.raise_for_status()
image_data.append(response.content)
diff --git a/examples/auth/auth0_mcp/README.md b/examples/auth/auth0_mcp/README.md
new file mode 100644
index 000000000..e8e0e6bd6
--- /dev/null
+++ b/examples/auth/auth0_mcp/README.md
@@ -0,0 +1,28 @@
+# Auth0 Auth for MCP Example
+
+Protects a FastMCP server with Auth0 [Auth for MCP](https://auth0.com/ai/docs/mcp/intro/overview). Auth0 handles OAuth and client registration; FastMCP validates access tokens.
+
+## Auth0 setup
+
+1. Enable **Resource Parameter Compatibility Profile** (Settings → Advanced).
+2. Create an API whose identifier is `http://127.0.0.1:8000/mcp` (must match the URL logged at server startup).
+3. Promote your login connections to domain-level (required for third-party DCR clients).
+
+See Auth0's [authorization quickstart](https://auth0.com/ai/docs/mcp/get-started/authorization-for-your-mcp-server) for details.
+
+## Running
+
+```bash
+export AUTH0_CONFIG_URL="https://YOUR_TENANT.auth0.com/.well-known/openid-configuration"
+python server.py
+```
+
+In another terminal:
+
+```bash
+python client.py
+```
+
+Use `127.0.0.1` consistently — mixing `localhost` and `127.0.0.1` breaks audience validation.
+
+For troubleshooting (DCR grants, token exchange errors, MCP Inspector), see the [Auth0 integration guide](https://gofastmcp.com/integrations/auth0).
diff --git a/examples/auth/auth0_mcp/client.py b/examples/auth/auth0_mcp/client.py
new file mode 100644
index 000000000..24985ae9c
--- /dev/null
+++ b/examples/auth/auth0_mcp/client.py
@@ -0,0 +1,21 @@
+"""Auth0 Auth for MCP client example."""
+
+import asyncio
+
+from fastmcp import Client
+from fastmcp.client.auth import OAuth
+
+auth = OAuth(
+ additional_client_metadata={"token_endpoint_auth_method": "none"},
+ callback_host="127.0.0.1",
+)
+
+
+async def main() -> None:
+ async with Client("http://127.0.0.1:8000/mcp", auth=auth) as client:
+ result = await client.call_tool("echo", {"message": "hello"})
+ print(result)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/examples/auth/auth0_mcp/server.py b/examples/auth/auth0_mcp/server.py
new file mode 100644
index 000000000..ac08c1c43
--- /dev/null
+++ b/examples/auth/auth0_mcp/server.py
@@ -0,0 +1,39 @@
+"""Auth0 Auth for MCP server example.
+
+Required environment variables:
+- AUTH0_CONFIG_URL: OIDC discovery URL for your Auth0 tenant
+
+To run:
+ export AUTH0_CONFIG_URL="https://YOUR_TENANT.auth0.com/.well-known/openid-configuration"
+ python server.py
+"""
+
+import os
+import sys
+
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.auth0 import Auth0MCPProvider
+
+config_url = os.getenv("AUTH0_CONFIG_URL")
+if not config_url:
+ sys.exit(
+ "AUTH0_CONFIG_URL must be set to your Auth0 OIDC discovery URL, "
+ 'e.g. "https://YOUR_TENANT.auth0.com/.well-known/openid-configuration"'
+ )
+
+auth = Auth0MCPProvider(
+ config_url=config_url,
+ base_url="http://127.0.0.1:8000",
+)
+
+mcp = FastMCP("Auth0 MCP Example Server", auth=auth)
+
+
+@mcp.tool
+def echo(message: str) -> str:
+ """Echo the provided message."""
+ return message
+
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
diff --git a/examples/auth/authkit/README.md b/examples/auth/authkit/README.md
new file mode 100644
index 000000000..8c4b8a6aa
--- /dev/null
+++ b/examples/auth/authkit/README.md
@@ -0,0 +1,36 @@
+# AuthKit Example
+
+Protects a FastMCP server with WorkOS AuthKit. The server binds the JWT
+`aud` claim to its own resource URL automatically — you just paste that same
+URL into the WorkOS Dashboard as a resource indicator.
+
+## WorkOS Dashboard setup
+
+In the WorkOS Dashboard for your project, go to **Connect → Configuration** and:
+
+1. Under **MCP Auth**, enable **Dynamic Client Registration** (or **Client ID
+ Metadata Document** if your MCP client supports it).
+2. Under **MCP resource indicators**, add `http://127.0.0.1:8000/mcp` as a
+ valid resource indicator.
+
+## Running
+
+1. Set your AuthKit domain:
+
+ ```bash
+ export AUTHKIT_DOMAIN="https://your-app.authkit.app"
+ ```
+
+2. Start the server. It logs the resource URL it's validating against —
+ that's the URL that must match your dashboard resource indicator:
+
+ ```bash
+ python server.py
+ ```
+
+3. In another terminal, run the client. Your browser will open for AuthKit
+ authentication:
+
+ ```bash
+ python client.py
+ ```
diff --git a/examples/auth/authkit_dcr/client.py b/examples/auth/authkit/client.py
similarity index 100%
rename from examples/auth/authkit_dcr/client.py
rename to examples/auth/authkit/client.py
diff --git a/examples/auth/authkit_dcr/server.py b/examples/auth/authkit/server.py
similarity index 50%
rename from examples/auth/authkit_dcr/server.py
rename to examples/auth/authkit/server.py
index 8974376d2..7611ccddf 100644
--- a/examples/auth/authkit_dcr/server.py
+++ b/examples/auth/authkit/server.py
@@ -1,9 +1,11 @@
-"""AuthKit DCR server example for FastMCP.
+"""AuthKit server example for FastMCP.
-This example demonstrates how to protect a FastMCP server with AuthKit DCR.
+Demonstrates an MCP server secured by WorkOS AuthKit. FastMCP binds the JWT
+audience to this server's resource URL automatically; you configure the same
+URL as an MCP resource indicator in the WorkOS Dashboard.
Required environment variables:
-- FASTMCP_SERVER_AUTH_AUTHKITPROVIDER_AUTHKIT_DOMAIN: Your AuthKit domain (e.g., "https://your-app.authkit.app")
+- AUTHKIT_DOMAIN: Your AuthKit domain (e.g., "https://your-app.authkit.app")
To run:
python server.py
@@ -16,10 +18,10 @@ from fastmcp.server.auth.providers.workos import AuthKitProvider
auth = AuthKitProvider(
authkit_domain=os.getenv("AUTHKIT_DOMAIN") or "",
- base_url="http://localhost:8000",
+ base_url="http://127.0.0.1:8000",
)
-mcp = FastMCP("AuthKit DCR Example Server", auth=auth)
+mcp = FastMCP("AuthKit Example Server", auth=auth)
@mcp.tool
diff --git a/examples/auth/authkit_dcr/README.md b/examples/auth/authkit_dcr/README.md
deleted file mode 100644
index 808246199..000000000
--- a/examples/auth/authkit_dcr/README.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# AuthKit DCR Example
-
-Demonstrates FastMCP server protection with AuthKit Dynamic Client Registration.
-
-## Setup
-
-1. Set your AuthKit domain:
-
- ```bash
- export AUTHKIT_DOMAIN="https://your-app.authkit.app"
- ```
-
-2. Run the server:
-
- ```bash
- python server.py
- ```
-
-3. In another terminal, run the client:
-
- ```bash
- python client.py
- ```
-
-The client will open your browser for AuthKit authentication.
diff --git a/examples/auth/aws_oauth/README.md b/examples/auth/aws_oauth/README.md
index 9abff838c..c4e25b1f8 100644
--- a/examples/auth/aws_oauth/README.md
+++ b/examples/auth/aws_oauth/README.md
@@ -10,7 +10,7 @@ Demonstrates FastMCP server protection with AWS Cognito OAuth.
- Create an App Client in your User Pool
- Configure the App Client settings:
- Enable "Authorization code grant" flow
- - Add Callback URL: `http://localhost:8000/auth/callback`
+ - Add Callback URL: `http://127.0.0.1:8000/auth/callback`
- Configure OAuth scopes (at minimum: `openid`)
- Note your User Pool ID, App Client ID, Client Secret, and Cognito Domain Prefix
diff --git a/examples/auth/aws_oauth/client.py b/examples/auth/aws_oauth/client.py
index 4043e6d4f..afcf54fd1 100644
--- a/examples/auth/aws_oauth/client.py
+++ b/examples/auth/aws_oauth/client.py
@@ -10,7 +10,7 @@ import asyncio
from fastmcp.client import Client
-SERVER_URL = "http://localhost:8000/mcp"
+SERVER_URL = "http://127.0.0.1:8000/mcp"
async def main():
diff --git a/examples/auth/aws_oauth/requirements.txt b/examples/auth/aws_oauth/requirements.txt
index 9c7f15cd1..044c95a70 100644
--- a/examples/auth/aws_oauth/requirements.txt
+++ b/examples/auth/aws_oauth/requirements.txt
@@ -1,2 +1,2 @@
fastmcp
-python-dotenv
\ No newline at end of file
+python-dotenv
diff --git a/examples/auth/aws_oauth/server.py b/examples/auth/aws_oauth/server.py
index dfe596a83..a854c790c 100644
--- a/examples/auth/aws_oauth/server.py
+++ b/examples/auth/aws_oauth/server.py
@@ -31,7 +31,7 @@ auth = AWSCognitoProvider(
or "eu-central-1",
client_id=os.getenv("FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID") or "",
client_secret=os.getenv("FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET") or "",
- base_url="http://localhost:8000",
+ base_url="http://127.0.0.1:8000",
# redirect_path="/custom/callback"
)
@@ -48,6 +48,8 @@ def echo(message: str) -> str:
async def get_access_token_claims() -> dict:
"""Get the authenticated user's access token claims."""
token = get_access_token()
+ if token is None:
+ return {"error": "Not authenticated"}
return {
"sub": token.claims.get("sub"),
"username": token.claims.get("username"),
diff --git a/examples/auth/azure_oauth/README.md b/examples/auth/azure_oauth/README.md
index ba0757ca7..98d9ae756 100644
--- a/examples/auth/azure_oauth/README.md
+++ b/examples/auth/azure_oauth/README.md
@@ -10,7 +10,7 @@ This example demonstrates how to use the Azure OAuth provider with FastMCP serve
2. Click "New registration" and configure:
- Name: Your app name
- Supported account types: Choose based on your needs
- - Redirect URI: `http://localhost:8000/auth/callback` (Web platform)
+ - Redirect URI: `http://127.0.0.1:8000/auth/callback` (Web platform)
3. After creation, go to "Certificates & secrets" → "New client secret"
4. Note these values from the Overview page:
- Application (client) ID
diff --git a/examples/auth/azure_oauth/server.py b/examples/auth/azure_oauth/server.py
index d214389aa..e0c9e799e 100644
--- a/examples/auth/azure_oauth/server.py
+++ b/examples/auth/azure_oauth/server.py
@@ -24,7 +24,7 @@ auth = AzureProvider(
client_secret=os.getenv("FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET") or "",
tenant_id=os.getenv("FASTMCP_SERVER_AUTH_AZURE_TENANT_ID")
or "", # Required for single-tenant apps - get from Azure Portal
- base_url="http://localhost:8000",
+ base_url="http://127.0.0.1:8000",
required_scopes=["read"],
# required_scopes is automatically loaded from FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES
# At least one scope is required - use unprefixed scope names from your Azure App (e.g., ["read", "write"])
diff --git a/examples/auth/clerk_oauth/README.md b/examples/auth/clerk_oauth/README.md
new file mode 100644
index 000000000..9a79ff566
--- /dev/null
+++ b/examples/auth/clerk_oauth/README.md
@@ -0,0 +1,36 @@
+# Clerk OAuth Example
+
+Demonstrates FastMCP server protection with Clerk OAuth.
+
+## Setup
+
+1. Create a Clerk OAuth Application:
+ - Go to [Clerk Dashboard](https://dashboard.clerk.com/)
+ - Create or select an application
+ - Go to Developers > OAuth Applications
+ - Create an OAuth application
+ - Add Authorized redirect URI: `http://127.0.0.1:8000/auth/callback`
+ - Copy the Client ID and Client Secret
+ - Note your instance domain (e.g., `saving-primate-16.clerk.accounts.dev`)
+
+2. Set environment variables:
+
+ ```bash
+ export FASTMCP_SERVER_AUTH_CLERK_DOMAIN="your-instance.clerk.accounts.dev"
+ export FASTMCP_SERVER_AUTH_CLERK_CLIENT_ID="your-clerk-client-id"
+ export FASTMCP_SERVER_AUTH_CLERK_CLIENT_SECRET="your-clerk-client-secret"
+ ```
+
+3. Run the server:
+
+ ```bash
+ python server.py
+ ```
+
+4. In another terminal, run the client:
+
+ ```bash
+ python client.py
+ ```
+
+The client will open your browser for Clerk authentication.
diff --git a/examples/auth/clerk_oauth/client.py b/examples/auth/clerk_oauth/client.py
new file mode 100644
index 000000000..d9d44c9d6
--- /dev/null
+++ b/examples/auth/clerk_oauth/client.py
@@ -0,0 +1,33 @@
+"""OAuth client example for connecting to a Clerk-protected FastMCP server.
+
+This example demonstrates how to connect to an OAuth-protected FastMCP server
+using Clerk as the identity provider.
+
+To run:
+ python client.py
+"""
+
+import asyncio
+
+from fastmcp.client import Client
+
+SERVER_URL = "http://127.0.0.1:8000/mcp"
+
+
+async def main():
+ try:
+ async with Client(SERVER_URL, auth="oauth") as client:
+ assert await client.ping()
+ print("✅ Successfully authenticated!")
+
+ tools = await client.list_tools()
+ print(f"🔧 Available tools ({len(tools)}):")
+ for tool in tools:
+ print(f" - {tool.name}: {tool.description}")
+ except Exception as e:
+ print(f"❌ Authentication failed: {e}")
+ raise
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/examples/auth/clerk_oauth/server.py b/examples/auth/clerk_oauth/server.py
new file mode 100644
index 000000000..e7d080734
--- /dev/null
+++ b/examples/auth/clerk_oauth/server.py
@@ -0,0 +1,40 @@
+"""Clerk OAuth server example for FastMCP.
+
+This example demonstrates how to protect a FastMCP server with Clerk OAuth.
+
+Required environment variables:
+- FASTMCP_SERVER_AUTH_CLERK_DOMAIN: Your Clerk instance domain
+ (e.g., "saving-primate-16.clerk.accounts.dev")
+- FASTMCP_SERVER_AUTH_CLERK_CLIENT_ID: Your Clerk OAuth client ID
+- FASTMCP_SERVER_AUTH_CLERK_CLIENT_SECRET: Your Clerk OAuth client secret
+
+To run:
+ python server.py
+"""
+
+import os
+
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.clerk import ClerkProvider
+
+auth = ClerkProvider(
+ domain=os.getenv("FASTMCP_SERVER_AUTH_CLERK_DOMAIN") or "",
+ client_id=os.getenv("FASTMCP_SERVER_AUTH_CLERK_CLIENT_ID") or "",
+ client_secret=os.getenv("FASTMCP_SERVER_AUTH_CLERK_CLIENT_SECRET") or "",
+ base_url="http://127.0.0.1:8000",
+ # redirect_path="/auth/callback", # Default path - change if using a different callback URL
+ # Optional: specify required scopes (defaults to ["openid", "email", "profile"])
+ # required_scopes=["openid", "email", "profile", "public_metadata"],
+)
+
+mcp = FastMCP("Clerk OAuth Example Server", auth=auth)
+
+
+@mcp.tool
+def echo(message: str) -> str:
+ """Echo the provided message."""
+ return message
+
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
diff --git a/examples/auth/discord_oauth/README.md b/examples/auth/discord_oauth/README.md
index 74217f833..e757ad84b 100644
--- a/examples/auth/discord_oauth/README.md
+++ b/examples/auth/discord_oauth/README.md
@@ -8,7 +8,7 @@ Demonstrates FastMCP server protection with Discord OAuth.
- Go to https://discord.com/developers/applications
- Click "New Application" and give it a name
- Go to OAuth2 in the left sidebar
- - Add a Redirect URL: `http://localhost:8000/auth/callback`
+ - Add a Redirect URL: `http://127.0.0.1:8000/auth/callback`
- Copy the Client ID and Client Secret
2. Set environment variables:
diff --git a/examples/auth/discord_oauth/server.py b/examples/auth/discord_oauth/server.py
index 424c97bdb..1e109b76a 100644
--- a/examples/auth/discord_oauth/server.py
+++ b/examples/auth/discord_oauth/server.py
@@ -18,7 +18,7 @@ from fastmcp.server.auth.providers.discord import DiscordProvider
auth = DiscordProvider(
client_id=os.getenv("FASTMCP_SERVER_AUTH_DISCORD_CLIENT_ID") or "",
client_secret=os.getenv("FASTMCP_SERVER_AUTH_DISCORD_CLIENT_SECRET") or "",
- base_url="http://localhost:8000",
+ base_url="http://127.0.0.1:8000",
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
)
diff --git a/examples/auth/github_oauth/README.md b/examples/auth/github_oauth/README.md
index 557ba7774..dcd5c2205 100644
--- a/examples/auth/github_oauth/README.md
+++ b/examples/auth/github_oauth/README.md
@@ -6,7 +6,7 @@ Demonstrates FastMCP server protection with GitHub OAuth.
1. Create a GitHub OAuth App:
- Go to GitHub Settings > Developer settings > OAuth Apps
- - Set Authorization callback URL to: `http://localhost:8000/auth/callback`
+ - Set Authorization callback URL to: `http://127.0.0.1:8000/auth/callback`
- Copy the Client ID and Client Secret
2. Set environment variables:
diff --git a/examples/auth/github_oauth/client.py b/examples/auth/github_oauth/client.py
index 7158583bc..8722a547c 100644
--- a/examples/auth/github_oauth/client.py
+++ b/examples/auth/github_oauth/client.py
@@ -10,7 +10,7 @@ import asyncio
from fastmcp.client import Client, OAuth
-SERVER_URL = "http://localhost:8000/mcp"
+SERVER_URL = "http://127.0.0.1:8000/mcp"
async def main():
diff --git a/examples/auth/github_oauth/server.py b/examples/auth/github_oauth/server.py
index 1f88c6977..e93d6f01a 100644
--- a/examples/auth/github_oauth/server.py
+++ b/examples/auth/github_oauth/server.py
@@ -18,7 +18,7 @@ from fastmcp.server.auth.providers.github import GitHubProvider
auth = GitHubProvider(
client_id=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID") or "",
client_secret=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET") or "",
- base_url="http://localhost:8000",
+ base_url="http://127.0.0.1:8000",
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
)
diff --git a/examples/auth/google_oauth/README.md b/examples/auth/google_oauth/README.md
index 869718344..82bcd8696 100644
--- a/examples/auth/google_oauth/README.md
+++ b/examples/auth/google_oauth/README.md
@@ -9,7 +9,7 @@ Demonstrates FastMCP server protection with Google OAuth.
- Create or select a project
- Go to APIs & Services > Credentials
- Create OAuth 2.0 Client ID (Web application)
- - Add Authorized redirect URI: `http://localhost:8000/auth/callback`
+ - Add Authorized redirect URI: `http://127.0.0.1:8000/auth/callback`
- Copy the Client ID and Client Secret
2. Set environment variables:
diff --git a/examples/auth/google_oauth/server.py b/examples/auth/google_oauth/server.py
index 2a5b1c7df..2043ed6c3 100644
--- a/examples/auth/google_oauth/server.py
+++ b/examples/auth/google_oauth/server.py
@@ -18,7 +18,7 @@ from fastmcp.server.auth.providers.google import GoogleProvider
auth = GoogleProvider(
client_id=os.getenv("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID") or "",
client_secret=os.getenv("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET") or "",
- base_url="http://localhost:8000",
+ base_url="http://127.0.0.1:8000",
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
# Optional: specify required scopes
# required_scopes=["openid", "https://www.googleapis.com/auth/userinfo.email"],
diff --git a/examples/auth/huggingface_oauth/README.md b/examples/auth/huggingface_oauth/README.md
new file mode 100644
index 000000000..3b84c70e1
--- /dev/null
+++ b/examples/auth/huggingface_oauth/README.md
@@ -0,0 +1,31 @@
+# Hugging FAce OAuth Example
+
+Demonstrates FastMCP server protection with Hugging Face OAuth.
+
+## Setup
+
+1. Create a Hugging Face OAuth App:
+ - Go to Hugging Face Settings > Connected Apps > Create App (`https://huggingface.co/settings/applications/new`)
+ - Set Authorization callback URL to: `http://localhost:8000/auth/callback`
+ - Copy the Client ID and Client Secret
+
+2. Set environment variables:
+
+ ```bash
+ export FASTMCP_SERVER_AUTH_HF_CLIENT_ID="your-client-id"
+ export FASTMCP_SERVER_AUTH_HF_CLIENT_SECRET="your-client-secret"
+ ```
+
+3. Run the server:
+
+ ```bash
+ python server.py
+ ```
+
+4. In another terminal, run the client:
+
+ ```bash
+ python client.py
+ ```
+
+The client will open your browser for Hugging Face authentication.
diff --git a/examples/auth/huggingface_oauth/client.py b/examples/auth/huggingface_oauth/client.py
new file mode 100644
index 000000000..d7f2b760a
--- /dev/null
+++ b/examples/auth/huggingface_oauth/client.py
@@ -0,0 +1,32 @@
+"""OAuth client example for connecting to FastMCP servers.
+
+This example demonstrates how to connect to an OAuth-protected FastMCP server.
+
+To run:
+ python client.py
+"""
+
+import asyncio
+
+from fastmcp.client import Client
+
+SERVER_URL = "http://localhost:8000/mcp"
+
+
+async def main():
+ try:
+ async with Client(SERVER_URL, auth="oauth") as client:
+ assert await client.ping()
+ print("✅ Successfully authenticated!")
+
+ tools = await client.list_tools()
+ print(f"🔧 Available tools ({len(tools)}):")
+ for tool in tools:
+ print(f" - {tool.name}: {tool.description}")
+ except Exception as e:
+ print(f"❌ Authentication failed: {e}")
+ raise
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/examples/auth/huggingface_oauth/server.py b/examples/auth/huggingface_oauth/server.py
new file mode 100644
index 000000000..5a819aa88
--- /dev/null
+++ b/examples/auth/huggingface_oauth/server.py
@@ -0,0 +1,36 @@
+import os
+
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.huggingface import HuggingFaceProvider
+from fastmcp.server.dependencies import get_access_token
+
+auth_provider = HuggingFaceProvider(
+ # Your Hugging Face OAuth app client ID
+ client_id=os.getenv("FASTMCP_SERVER_AUTH_HF_CLIENT_ID") or "",
+ # Your Hugging Face OAuth app client secret
+ client_secret=os.getenv("FASTMCP_SERVER_AUTH_HF_CLIENT_SECRET") or "",
+ # Must match your OAuth configuration
+ base_url="http://localhost:8000",
+ # Supply jwt_signing_key instead of client_secret for public applications
+ # jwt_signing_key="replace-with-a-secure-secret"
+)
+
+mcp = FastMCP(name="Hugging Face Secured App", auth=auth_provider)
+
+
+# Add a tool to test authentication
+@mcp.tool
+async def get_user_info() -> dict:
+ """Returns information about the authenticated Hugging Face user."""
+ token = get_access_token()
+ if token is None:
+ return {"error": "Not authenticated"}
+ return {
+ "subject": token.claims.get("sub"),
+ "username": token.claims.get("preferred_username"),
+ "profile": token.claims.get("profile"),
+ }
+
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
diff --git a/examples/auth/keycloak_oauth/README.md b/examples/auth/keycloak_oauth/README.md
new file mode 100644
index 000000000..ba6b95bf4
--- /dev/null
+++ b/examples/auth/keycloak_oauth/README.md
@@ -0,0 +1,29 @@
+# Keycloak OAuth Example
+
+Demonstrates FastMCP server protection with Keycloak OAuth.
+
+**Requires Keycloak 26.6.0 or later** with Dynamic Client Registration enabled.
+
+## Setup
+
+1. Configure a Keycloak realm with Dynamic Client Registration enabled and a trusted host policy for your server URL (e.g. `http://127.0.0.1:8000/*`).
+
+2. Set environment variables:
+
+ ```bash
+ export KEYCLOAK_REALM_URL="http://localhost:8080/realms/your-realm"
+ ```
+
+3. Run the server:
+
+ ```bash
+ python server.py
+ ```
+
+4. In another terminal, run the client:
+
+ ```bash
+ python client.py
+ ```
+
+The client will open your browser for Keycloak authentication.
diff --git a/examples/auth/keycloak_oauth/client.py b/examples/auth/keycloak_oauth/client.py
new file mode 100644
index 000000000..4992abbab
--- /dev/null
+++ b/examples/auth/keycloak_oauth/client.py
@@ -0,0 +1,33 @@
+"""OAuth client example for connecting to a Keycloak-protected FastMCP server.
+
+To run:
+ python client.py
+"""
+
+import asyncio
+
+from fastmcp import Client
+
+SERVER_URL = "http://127.0.0.1:8000/mcp"
+
+
+async def main():
+ async with Client(SERVER_URL, auth="oauth") as client:
+ assert await client.ping()
+ print("Successfully authenticated!")
+
+ tools = await client.list_tools()
+ print(f"Available tools ({len(tools)}):")
+ for tool in tools:
+ print(f" - {tool.name}: {tool.description}")
+
+ print("Calling protected tool: get_access_token_claims")
+ result = await client.call_tool("get_access_token_claims")
+ claims = result.data
+ print(f" sub: {claims.get('sub', 'N/A')}")
+ print(f" scope: {claims.get('scope', 'N/A')}")
+ print(f" azp: {claims.get('azp', 'N/A')}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/examples/auth/keycloak_oauth/server.py b/examples/auth/keycloak_oauth/server.py
new file mode 100644
index 000000000..5bf50b9b1
--- /dev/null
+++ b/examples/auth/keycloak_oauth/server.py
@@ -0,0 +1,46 @@
+"""Keycloak OAuth server example for FastMCP.
+
+This example demonstrates how to protect a FastMCP server with Keycloak OAuth.
+
+Required: Keycloak 26.6.0 or later with Dynamic Client Registration enabled.
+
+To run:
+ KEYCLOAK_REALM_URL=https://your-keycloak.com/realms/myrealm python server.py
+"""
+
+import os
+
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
+from fastmcp.server.dependencies import get_access_token
+
+auth = KeycloakAuthProvider(
+ realm_url=os.getenv("KEYCLOAK_REALM_URL") or "http://localhost:8080/realms/fastmcp",
+ base_url="http://127.0.0.1:8000",
+ # audience="http://127.0.0.1:8000", # Recommended for production
+)
+
+mcp = FastMCP("Keycloak Example Server", auth=auth)
+
+
+@mcp.tool
+def echo(message: str) -> str:
+ """Echo the provided message."""
+ return message
+
+
+@mcp.tool
+async def get_access_token_claims() -> dict:
+ """Get the authenticated user's access token claims."""
+ token = get_access_token()
+ if token is None:
+ return {"error": "Not authenticated"}
+ return {
+ "sub": token.claims.get("sub"),
+ "scope": token.claims.get("scope"),
+ "azp": token.claims.get("azp"),
+ }
+
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
diff --git a/examples/auth/mounted/README.md b/examples/auth/mounted/README.md
index 5810dab4c..2bf213094 100644
--- a/examples/auth/mounted/README.md
+++ b/examples/auth/mounted/README.md
@@ -4,12 +4,12 @@ This example demonstrates mounting multiple OAuth-protected MCP servers in a sin
## URL Structure
-- **GitHub MCP**: `http://localhost:8000/api/mcp/github/mcp`
-- **Google MCP**: `http://localhost:8000/api/mcp/google/mcp`
+- **GitHub MCP**: `http://127.0.0.1:8000/api/mcp/github/mcp`
+- **Google MCP**: `http://127.0.0.1:8000/api/mcp/google/mcp`
Discovery endpoints (RFC 8414 path-aware):
-- **GitHub**: `http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/github`
-- **Google**: `http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/google`
+- **GitHub**: `http://127.0.0.1:8000/.well-known/oauth-authorization-server/api/mcp/github`
+- **Google**: `http://127.0.0.1:8000/.well-known/oauth-authorization-server/api/mcp/google`
## Setup
@@ -23,8 +23,8 @@ export FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET="your-google-client-secret"
```
Configure redirect URIs in each provider's developer console (note the `/api/mcp/{provider}` prefix since the servers are mounted):
-- GitHub: `http://localhost:8000/api/mcp/github/auth/callback/github`
-- Google: `http://localhost:8000/api/mcp/google/auth/callback/google`
+- GitHub: `http://127.0.0.1:8000/api/mcp/github/auth/callback/github`
+- Google: `http://127.0.0.1:8000/api/mcp/google/auth/callback/google`
## Running
diff --git a/examples/auth/mounted/server.py b/examples/auth/mounted/server.py
index 24aefdd59..c5b3af593 100644
--- a/examples/auth/mounted/server.py
+++ b/examples/auth/mounted/server.py
@@ -5,10 +5,10 @@ application, each with its own provider. It showcases RFC 8414 path-aware discov
where each server has its own authorization server metadata endpoint.
URL structure:
-- GitHub MCP: http://localhost:8000/api/mcp/github/mcp
-- Google MCP: http://localhost:8000/api/mcp/google/mcp
-- GitHub discovery: http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/github
-- Google discovery: http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/google
+- GitHub MCP: http://127.0.0.1:8000/api/mcp/github/mcp
+- Google MCP: http://127.0.0.1:8000/api/mcp/google/mcp
+- GitHub discovery: http://127.0.0.1:8000/.well-known/oauth-authorization-server/api/mcp/github
+- Google discovery: http://127.0.0.1:8000/.well-known/oauth-authorization-server/api/mcp/google
Required environment variables:
- FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID: Your GitHub OAuth app client ID
@@ -31,7 +31,7 @@ from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.auth.providers.google import GoogleProvider
# Configuration
-ROOT_URL = "http://localhost:8000"
+ROOT_URL = "http://127.0.0.1:8000"
API_PREFIX = "/api/mcp"
# --- GitHub OAuth Server ---
diff --git a/examples/auth/oci_oauth/README.md b/examples/auth/oci_oauth/README.md
new file mode 100644
index 000000000..5b831770d
--- /dev/null
+++ b/examples/auth/oci_oauth/README.md
@@ -0,0 +1,51 @@
+# Oracle (OCI IAM (Identity Domain)) OAuth Example
+
+This example demonstrates how to use the OCI IAM OAuth provider with FastMCP servers.
+
+## Setup
+
+### 1. OCI App Registration
+
+1. Login to OCI console (https://cloud.oracle.com for OCI commercial cloud).
+2. From "Identity & Security" menu, open Domains page.
+3. On the Domains list page, select the domain in which you want to create MCP server OAuth client. If you need help finding the list page for the domain, see [Listing Identity Domains.](https://docs.oracle.com/en-us/iaas/Content/Identity/domains/to-view-identity-domains.htm#view-identity-domains).
+4. On the details page, select Integrated applications. A list of applications in the domain is displayed.
+5. Select Add application.
+6. In the Add application window, select Confidential Application.
+7. Select Launch workflow.
+8. In the Add application details page, Enter name and description and create the application.
+9. Once the Integrated Application is created, Click on "OAuth configuration" tab.
+10. Click on "Edit OAuth configuration" button.
+11. Configure the application as OAuth client by selecting "Configure this application as a client now" radio button.
+12. Select "Authorization code" grant type. If you are planning to use the same OAuth client application for token exchange, select "Client credentials" grant type as well. In the sample, we will use the same client.
+13. For Authorization grant type, select redirect URL. In most cases, this will be the MCP server URL followed by "/auth/callback". For example http://localhost:8000/auth/callback
+14. Click on "Submit" button to update OAuth configuration for the client application.
+15. Make sure to Activate the client application.
+16. Note down client ID and client secret for the application. You'll use these values when configuring the OCIProvider in the MCP server.
+
+For details instructions with screenshots, please refer to [FastMCP OCI Provider Documentation](https://gofastmcp.com/integrations/oci).
+
+### 2. Set Environment Variables
+
+```bash
+# Required
+FASTMCP_SERVER_AUTH_IDCS_CLIENT_ID=your-application-client-id
+FASTMCP_SERVER_AUTH_IDCS_CLIENT_SECRET=your-client-secret-value
+FASTMCP_SERVER_AUTH_IDCS_DOMAIN=your-iam-domain-url # IDCS domain URL for example idcs-abscasdwdac3432rdwsda.identity.oraclecloud.com
+```
+
+### 3. Run the Example
+
+Start the server:
+
+```bash
+python server.py
+```
+
+Test with client:
+
+```bash
+python client.py
+```
+
+When you run the client, it will open a browser on your machine to login to OCI IAM domain.
diff --git a/examples/auth/oci_oauth/client.py b/examples/auth/oci_oauth/client.py
new file mode 100644
index 000000000..d7f2b760a
--- /dev/null
+++ b/examples/auth/oci_oauth/client.py
@@ -0,0 +1,32 @@
+"""OAuth client example for connecting to FastMCP servers.
+
+This example demonstrates how to connect to an OAuth-protected FastMCP server.
+
+To run:
+ python client.py
+"""
+
+import asyncio
+
+from fastmcp.client import Client
+
+SERVER_URL = "http://localhost:8000/mcp"
+
+
+async def main():
+ try:
+ async with Client(SERVER_URL, auth="oauth") as client:
+ assert await client.ping()
+ print("✅ Successfully authenticated!")
+
+ tools = await client.list_tools()
+ print(f"🔧 Available tools ({len(tools)}):")
+ for tool in tools:
+ print(f" - {tool.name}: {tool.description}")
+ except Exception as e:
+ print(f"❌ Authentication failed: {e}")
+ raise
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/examples/auth/oci_oauth/server.py b/examples/auth/oci_oauth/server.py
new file mode 100644
index 000000000..e1b09f827
--- /dev/null
+++ b/examples/auth/oci_oauth/server.py
@@ -0,0 +1,38 @@
+"""Oracle OCI IAM OAuth server example for FastMCP.
+
+This example demonstrates how to protect a FastMCP server with Oracle OCI IAM OAuth.
+
+Required environment variables:
+- FASTMCP_SERVER_AUTH_IDCS_CLIENT_ID: Your IDCS OAuth Application clientID
+- FASTMCP_SERVER_AUTH_IDCS_CLIENT_SECRET: Your IDCS client secret
+- FASTMCP_SERVER_AUTH_IDCS_DOMAIN: IDCS domain URL for example idcs-abscasdwdac3432rdwsda.identity.oraclecloud.com
+
+To run:
+ python server.py
+"""
+
+import os
+
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.oci import OCIProvider
+
+auth = OCIProvider(
+ client_id=os.getenv("FASTMCP_SERVER_AUTH_IDCS_CLIENT_ID") or "",
+ client_secret=os.getenv("FASTMCP_SERVER_AUTH_IDCS_CLIENT_SECRET") or "",
+ config_url=f"https://{os.getenv('FASTMCP_SERVER_AUTH_IDCS_DOMAIN')}/.well-known/openid-configuration"
+ or "",
+ base_url="http://localhost:8000",
+ # redirect_path="/auth/callback", # Default path - change if using a different callback URL
+)
+
+mcp = FastMCP("OCI OAuth Example Server", auth=auth)
+
+
+@mcp.tool
+def echo(message: str) -> str:
+ """Echo the provided message."""
+ return message
+
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000, host="localhost")
diff --git a/examples/auth/propelauth_oauth/README.md b/examples/auth/propelauth_oauth/README.md
index 575ea7314..aa5b10ecf 100644
--- a/examples/auth/propelauth_oauth/README.md
+++ b/examples/auth/propelauth_oauth/README.md
@@ -36,7 +36,7 @@ Create a `.env` file:
PROPELAUTH_AUTH_URL=https://auth.yourdomain.com
PROPELAUTH_INTROSPECTION_CLIENT_ID=your-client-id
PROPELAUTH_INTROSPECTION_CLIENT_SECRET=your-client-secret
-BASE_URL=http://localhost:8000/
+BASE_URL=http://127.0.0.1:8000/
# Optional: additional scopes tokens must include (comma-separated)
# PROPELAUTH_REQUIRED_SCOPES=read:user_data
```
@@ -50,7 +50,7 @@ Start the server:
uv run python server.py
```
-The server will start on `http://localhost:8000/mcp` with PropelAuth OAuth authentication enabled.
+The server will start on `http://127.0.0.1:8000/mcp` with PropelAuth OAuth authentication enabled.
Test with client:
diff --git a/examples/auth/propelauth_oauth/server.py b/examples/auth/propelauth_oauth/server.py
index 8401882aa..ab1661d22 100644
--- a/examples/auth/propelauth_oauth/server.py
+++ b/examples/auth/propelauth_oauth/server.py
@@ -9,7 +9,7 @@ Required environment variables:
Optional:
- PROPELAUTH_REQUIRED_SCOPES: Comma-separated scopes tokens must include
-- BASE_URL: Public URL where the FastMCP server is exposed (defaults to `http://localhost:8000/`)
+- BASE_URL: Public URL where the FastMCP server is exposed (defaults to `http://127.0.0.1:8000/`)
To run:
python server.py
@@ -29,7 +29,7 @@ auth = PropelAuthProvider(
auth_url=os.environ["PROPELAUTH_AUTH_URL"],
introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
- base_url=os.getenv("BASE_URL", "http://localhost:8000/"),
+ base_url=os.getenv("BASE_URL", "http://127.0.0.1:8000/"),
)
mcp = FastMCP("PropelAuth OAuth Example Server", auth=auth)
diff --git a/examples/auth/scalekit_oauth/README.md b/examples/auth/scalekit_oauth/README.md
index c241d76f7..d16b81c37 100644
--- a/examples/auth/scalekit_oauth/README.md
+++ b/examples/auth/scalekit_oauth/README.md
@@ -24,7 +24,7 @@ Create a `.env` file:
# Required Scalekit credentials
SCALEKIT_ENVIRONMENT_URL=
SCALEKIT_RESOURCE_ID= # res_926EXAMPLE5878
-BASE_URL=http://localhost:8000/
+BASE_URL=http://127.0.0.1:8000/
# Optional: additional scopes tokens must include (comma-separated)
# SCALEKIT_REQUIRED_SCOPES=read,write
```
@@ -38,7 +38,7 @@ Start the server:
uv run python server.py
```
-The server will start on `http://localhost:8000/mcp` with Scalekit OAuth authentication enabled.
+The server will start on `http://127.0.0.1:8000/mcp` with Scalekit OAuth authentication enabled.
Test with client:
diff --git a/examples/auth/scalekit_oauth/server.py b/examples/auth/scalekit_oauth/server.py
index 68cef23b5..09d4f5959 100644
--- a/examples/auth/scalekit_oauth/server.py
+++ b/examples/auth/scalekit_oauth/server.py
@@ -8,7 +8,7 @@ Required environment variables:
Optional:
- SCALEKIT_REQUIRED_SCOPES: Comma-separated scopes tokens must include
-- BASE_URL: Public URL where the FastMCP server is exposed (defaults to `http://localhost:8000/`)
+- BASE_URL: Public URL where the FastMCP server is exposed (defaults to `http://127.0.0.1:8000/`)
To run:
python server.py
@@ -30,7 +30,7 @@ auth = ScalekitProvider(
environment_url=os.getenv("SCALEKIT_ENVIRONMENT_URL")
or "https://your-env.scalekit.com",
resource_id=os.getenv("SCALEKIT_RESOURCE_ID") or "",
- base_url=os.getenv("BASE_URL", "http://localhost:8000/"),
+ base_url=os.getenv("BASE_URL", "http://127.0.0.1:8000/"),
required_scopes=required_scopes,
)
diff --git a/examples/auth/workos_oauth/server.py b/examples/auth/workos_oauth/server.py
index 08c1db62b..4dba970a8 100644
--- a/examples/auth/workos_oauth/server.py
+++ b/examples/auth/workos_oauth/server.py
@@ -20,7 +20,7 @@ auth = WorkOSProvider(
client_id=os.getenv("WORKOS_CLIENT_ID") or "",
client_secret=os.getenv("WORKOS_CLIENT_SECRET") or "",
authkit_domain=os.getenv("WORKOS_AUTHKIT_DOMAIN") or "https://your-app.authkit.app",
- base_url="http://localhost:8000",
+ base_url="http://127.0.0.1:8000",
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
)
diff --git a/examples/custom_tool_serializer_decorator.py b/examples/custom_tool_serializer_decorator.py
index 7075b7238..4993d7356 100644
--- a/examples/custom_tool_serializer_decorator.py
+++ b/examples/custom_tool_serializer_decorator.py
@@ -11,9 +11,10 @@ from functools import wraps
from typing import Any
import yaml
+from mcp_types import TextContent
-from fastmcp import FastMCP
-from fastmcp.tools.tool import ToolResult
+from fastmcp import Client, FastMCP
+from fastmcp.tools import ToolResult
def with_serializer(serializer: Callable[[Any], str]):
@@ -55,18 +56,19 @@ def get_json_data() -> dict:
async def example_usage():
- # YAML serialized tool
- yaml_result = await server._call_tool_mcp("get_example_data", {})
- print("YAML Tool Result:")
- print(yaml_result)
- print()
+ async with Client(server) as client:
+ # YAML serialized tool
+ yaml_result = await client.call_tool("get_example_data", {})
+ print("YAML Tool Result:")
+ if yaml_result.content and isinstance(yaml_result.content[0], TextContent):
+ print(yaml_result.content[0].text)
+ print()
- # Default JSON serialized tool
- json_result = await server._call_tool_mcp("get_json_data", {})
- print("JSON Tool Result:")
- print(json_result)
+ # Default JSON serialized tool
+ json_result = await client.call_tool("get_json_data", {})
+ print("JSON Tool Result:")
+ print(json_result.data)
if __name__ == "__main__":
asyncio.run(example_usage())
- server.run()
diff --git a/examples/diagnostics/server.py b/examples/diagnostics/server.py
index 211c5d893..354ba08bb 100644
--- a/examples/diagnostics/server.py
+++ b/examples/diagnostics/server.py
@@ -7,7 +7,7 @@ from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
-import httpx
+import httpx2
from fastmcp import FastMCP
from fastmcp.server import create_proxy
@@ -44,7 +44,7 @@ async def lifespan(server: FastMCP) -> AsyncIterator[None]:
)
# Wait for server to be ready (async to avoid blocking event loop)
- async with httpx.AsyncClient() as client:
+ async with httpx2.AsyncClient() as client:
for _ in range(50):
try:
await client.get(
diff --git a/examples/filesystem-provider/mcp/prompts/assistant.py b/examples/filesystem-provider/components/prompts/assistant.py
similarity index 100%
rename from examples/filesystem-provider/mcp/prompts/assistant.py
rename to examples/filesystem-provider/components/prompts/assistant.py
diff --git a/examples/filesystem-provider/mcp/resources/config.py b/examples/filesystem-provider/components/resources/config.py
similarity index 100%
rename from examples/filesystem-provider/mcp/resources/config.py
rename to examples/filesystem-provider/components/resources/config.py
diff --git a/examples/filesystem-provider/mcp/tools/calculator.py b/examples/filesystem-provider/components/tools/calculator.py
similarity index 100%
rename from examples/filesystem-provider/mcp/tools/calculator.py
rename to examples/filesystem-provider/components/tools/calculator.py
diff --git a/examples/filesystem-provider/mcp/tools/greeting.py b/examples/filesystem-provider/components/tools/greeting.py
similarity index 100%
rename from examples/filesystem-provider/mcp/tools/greeting.py
rename to examples/filesystem-provider/components/tools/greeting.py
diff --git a/examples/filesystem-provider/server.py b/examples/filesystem-provider/server.py
index 2f3cf47a6..11bbf7f81 100644
--- a/examples/filesystem-provider/server.py
+++ b/examples/filesystem-provider/server.py
@@ -22,7 +22,7 @@ from fastmcp.server.providers import FileSystemProvider
# Functions decorated with @tool, @resource, or @prompt are registered.
# Directory structure is purely organizational - decorators determine type.
provider = FileSystemProvider(
- root=Path(__file__).parent / "mcp",
+ root=Path(__file__).parent / "components",
reload=True, # Set True for dev mode (re-scan on every request)
)
diff --git a/examples/in_memory_proxy_example.py b/examples/in_memory_proxy_example.py
index 45e7a10b2..116e693e6 100644
--- a/examples/in_memory_proxy_example.py
+++ b/examples/in_memory_proxy_example.py
@@ -3,16 +3,17 @@ This example demonstrates how to set up and use an in-memory FastMCP proxy.
It illustrates the pattern:
1. Create an original FastMCP server with some tools.
-2. Create a proxy FastMCP server using ``FastMCP.as_proxy(original_server)``.
+2. Create a proxy FastMCP server using ``create_proxy(original_server)``.
3. Use another Client to connect to the proxy server (in-memory) and interact with the original server's tools through the proxy.
"""
import asyncio
-from mcp.types import TextContent
+from mcp_types import TextContent
from fastmcp import FastMCP
from fastmcp.client import Client
+from fastmcp.server import create_proxy
class EchoService:
@@ -37,10 +38,8 @@ async def main():
# 2. Proxy Server Creation
print("\nStep 2: Creating the Proxy Server (InMemoryProxy)...")
- print(
- f" (Using FastMCP.as_proxy to wrap '{original_server.name}' directly)"
- )
- proxy_server = FastMCP.as_proxy(original_server, name="InMemoryProxy")
+ print(f" (Using create_proxy to wrap '{original_server.name}' directly)")
+ proxy_server = create_proxy(original_server, name="InMemoryProxy")
print(
f" -> Proxy Server '{proxy_server.name}' created, proxying '{original_server.name}'."
)
@@ -65,8 +64,10 @@ async def main():
print(f"\n Calling 'echo' tool via proxy with message: '{message_to_echo}'")
try:
result = await final_client.call_tool("echo", {"message": message_to_echo})
- if result and isinstance(result[0], TextContent):
- print(f" Result from proxied 'echo' call: '{result[0].text}'")
+ if result.content and isinstance(result.content[0], TextContent):
+ print(
+ f" Result from proxied 'echo' call: '{result.content[0].text}'"
+ )
else:
print(
f" Error: Unexpected result format from proxied 'echo' call: {result}"
diff --git a/examples/mount_example.py b/examples/mount_example.py
index 9e8c0fb42..5844ebe4c 100644
--- a/examples/mount_example.py
+++ b/examples/mount_example.py
@@ -63,9 +63,9 @@ def check_app_status() -> dict[str, str]:
# Mount sub-applications
-app.mount(server=weather_app, prefix="weather")
+app.mount(server=weather_app, namespace="weather")
-app.mount(server=news_app, prefix="news")
+app.mount(server=news_app, namespace="news")
async def get_server_details():
diff --git a/examples/providers/sqlite/server.py b/examples/providers/sqlite/server.py
index dd9f19ff6..694a2906e 100644
--- a/examples/providers/sqlite/server.py
+++ b/examples/providers/sqlite/server.py
@@ -23,7 +23,7 @@ from rich import print
from fastmcp import Client, FastMCP
from fastmcp.server.providers import Provider
-from fastmcp.tools.tool import Tool, ToolResult
+from fastmcp.tools import Tool, ToolResult
DB_PATH = Path(__file__).parent / "tools.db"
diff --git a/examples/sampling/README.md b/examples/sampling/README.md
deleted file mode 100644
index 3f9225b19..000000000
--- a/examples/sampling/README.md
+++ /dev/null
@@ -1,62 +0,0 @@
-# Sampling Examples
-
-These examples demonstrate FastMCP's sampling API, which allows server tools to request LLM completions from the client.
-
-## Prerequisites
-
-```bash
-pip install fastmcp[anthropic]
-export ANTHROPIC_API_KEY=your-key
-```
-
-Or run directly with `uv`:
-
-```bash
-uv run examples/sampling/text.py
-```
-
-## Examples
-
-### Simple Text Sampling (`text.py`)
-
-Basic sampling flow where a server tool requests an LLM completion:
-
-```bash
-uv run examples/sampling/text.py
-```
-
-### Structured Output (`structured_output.py`)
-
-Uses `result_type` to get validated Pydantic models from the LLM:
-
-```bash
-uv run examples/sampling/structured_output.py
-```
-
-### Tool Use (`tool_use.py`)
-
-Gives the LLM tools to use during sampling (calculator, time, dice):
-
-```bash
-uv run examples/sampling/tool_use.py
-```
-
-### Server Fallback (`server_fallback.py`)
-
-Configures a fallback sampling handler on the server, enabling sampling even when clients don't support it:
-
-```bash
-uv run examples/sampling/server_fallback.py
-```
-
-## Using OpenAI Instead
-
-To use OpenAI instead of Anthropic, change the handler:
-
-```python
-from fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler
-
-handler = OpenAISamplingHandler(default_model="gpt-4o-mini")
-```
-
-And install with `pip install fastmcp[openai]`.
diff --git a/examples/sampling/server_fallback.py b/examples/sampling/server_fallback.py
deleted file mode 100644
index 1c3fb10af..000000000
--- a/examples/sampling/server_fallback.py
+++ /dev/null
@@ -1,88 +0,0 @@
-# /// script
-# dependencies = ["anthropic", "fastmcp", "rich"]
-# ///
-"""
-Server-Side Fallback Handler
-
-Demonstrates configuring a sampling handler on the server. This ensures
-sampling works even when the client doesn't provide a handler.
-
-The server runs as an HTTP server that can be connected to by any MCP client.
-
-Run:
- uv run examples/sampling/server_fallback.py
-
-Then connect with any MCP client (e.g., Claude Desktop) or test with:
- curl http://localhost:8000/mcp/
-"""
-
-import asyncio
-
-from rich.console import Console
-from rich.panel import Panel
-
-from fastmcp import FastMCP
-from fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler
-from fastmcp.server.context import Context
-
-console = Console()
-
-
-# Create server with a fallback sampling handler
-# This handler is used when the client doesn't support sampling
-mcp = FastMCP(
- "Server with Fallback Handler",
- sampling_handler=AnthropicSamplingHandler(default_model="claude-sonnet-4-5"),
- sampling_handler_behavior="fallback", # Use only if client lacks sampling
-)
-
-
-@mcp.tool
-async def summarize(text: str, ctx: Context) -> str:
- """Summarize the given text."""
- console.print(f"[bold cyan]SERVER[/] Summarizing text ({len(text)} chars)...")
-
- result = await ctx.sample(
- messages=f"Summarize this text in 1-2 sentences:\n\n{text}",
- system_prompt="You are a concise summarizer.",
- max_tokens=150,
- )
-
- console.print("[bold cyan]SERVER[/] Summary complete")
- return result.text or ""
-
-
-@mcp.tool
-async def translate(text: str, target_language: str, ctx: Context) -> str:
- """Translate text to the target language."""
- console.print(f"[bold cyan]SERVER[/] Translating to {target_language}...")
-
- result = await ctx.sample(
- messages=f"Translate to {target_language}:\n\n{text}",
- system_prompt=f"You are a translator. Output only the {target_language} translation.",
- max_tokens=500,
- )
-
- console.print("[bold cyan]SERVER[/] Translation complete")
- return result.text or ""
-
-
-async def main():
- console.print(
- Panel.fit(
- "[bold]Server-Side Fallback Handler Demo[/]\n\n"
- "This server has a built-in Anthropic handler that activates\n"
- "when clients don't provide their own sampling support.",
- subtitle="server_fallback.py",
- )
- )
- console.print()
- console.print("[bold yellow]Starting HTTP server on http://localhost:8000[/]")
- console.print("Connect with an MCP client or press Ctrl+C to stop")
- console.print()
-
- await mcp.run_http_async(host="localhost", port=8000)
-
-
-if __name__ == "__main__":
- asyncio.run(main())
diff --git a/examples/sampling/structured_output.py b/examples/sampling/structured_output.py
deleted file mode 100644
index fa7afe7ad..000000000
--- a/examples/sampling/structured_output.py
+++ /dev/null
@@ -1,110 +0,0 @@
-# /// script
-# dependencies = ["anthropic", "fastmcp", "rich"]
-# ///
-"""
-Structured Output Sampling
-
-Demonstrates using `result_type` to get validated Pydantic models from an LLM.
-The server exposes a sentiment analysis tool that returns structured data.
-
-Run:
- uv run examples/sampling/structured_output.py
-"""
-
-import asyncio
-
-from pydantic import BaseModel
-from rich.console import Console
-from rich.panel import Panel
-from rich.table import Table
-
-from fastmcp import Client, Context, FastMCP
-from fastmcp.client.sampling import SamplingMessage, SamplingParams
-from fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler
-
-console = Console()
-
-
-class LoggingAnthropicHandler(AnthropicSamplingHandler):
- async def __call__(
- self, messages: list[SamplingMessage], params: SamplingParams, context
- ): # type: ignore[override]
- console.print(" [bold blue]SAMPLING[/] Calling Claude API...")
- result = await super().__call__(messages, params, context)
- console.print(" [bold blue]SAMPLING[/] Response received")
- return result
-
-
-# Define a structured output model
-class SentimentAnalysis(BaseModel):
- sentiment: str # "positive", "negative", or "neutral"
- confidence: float # 0.0 to 1.0
- keywords: list[str] # Keywords that influenced the analysis
- explanation: str # Brief explanation of the analysis
-
-
-# Create the MCP server
-mcp = FastMCP("Sentiment Analyzer")
-
-
-@mcp.tool
-async def analyze_sentiment(text: str, ctx: Context) -> dict:
- """Analyze the sentiment of the given text."""
- console.print(" [bold cyan]SERVER[/] Analyzing sentiment...")
-
- result = await ctx.sample(
- messages=f"Analyze the sentiment of this text:\n\n{text}",
- system_prompt="You are a sentiment analysis expert. Analyze text carefully.",
- result_type=SentimentAnalysis,
- )
-
- console.print(" [bold cyan]SERVER[/] Analysis complete")
- return result.result.model_dump() # type: ignore[attr-defined]
-
-
-async def main():
- console.print(
- Panel.fit("[bold]MCP Sampling Flow Demo[/]", subtitle="structured_output.py")
- )
- console.print()
-
- handler = LoggingAnthropicHandler(default_model="claude-sonnet-4-5")
-
- async with Client(mcp, sampling_handler=handler) as client:
- texts = [
- "I absolutely love this product! It exceeded all my expectations.",
- "The service was okay, nothing special but got the job done.",
- "This is the worst experience I've ever had. Never again.",
- ]
-
- for text in texts:
- console.print(f"[bold green]CLIENT[/] Analyzing: [italic]{text[:50]}...[/]")
- console.print()
-
- result = await client.call_tool("analyze_sentiment", {"text": text})
- data = result.data
-
- # Display results in a table
- table = Table(show_header=False, box=None, padding=(0, 2))
- table.add_column(style="bold")
- table.add_column()
-
- sentiment_color = {
- "positive": "green",
- "negative": "red",
- "neutral": "yellow",
- }.get(
- data["sentiment"],
- "white", # type: ignore[union-attr]
- )
- table.add_row("Sentiment", f"[{sentiment_color}]{data['sentiment']}[/]") # type: ignore[index]
- table.add_row("Confidence", f"{data['confidence']:.0%}") # type: ignore[index]
- table.add_row("Keywords", ", ".join(data["keywords"])) # type: ignore[index]
- table.add_row("Explanation", data["explanation"]) # type: ignore[index]
-
- console.print(Panel(table, border_style=sentiment_color))
- console.print()
-
-
-if __name__ == "__main__":
- asyncio.run(main())
diff --git a/examples/sampling/text.py b/examples/sampling/text.py
deleted file mode 100644
index 6d354e7c2..000000000
--- a/examples/sampling/text.py
+++ /dev/null
@@ -1,78 +0,0 @@
-# /// script
-# dependencies = ["anthropic", "fastmcp", "rich"]
-# ///
-"""
-Simple Text Sampling
-
-Demonstrates the basic MCP sampling flow where a server tool requests
-an LLM completion from the client.
-
-Run:
- uv run examples/sampling/text.py
-"""
-
-import asyncio
-
-from rich.console import Console
-from rich.panel import Panel
-
-from fastmcp import Client, Context, FastMCP
-from fastmcp.client.sampling import SamplingMessage, SamplingParams
-from fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler
-
-console = Console()
-
-
-# Create a wrapper handler that logs when the LLM is called
-class LoggingAnthropicHandler(AnthropicSamplingHandler):
- async def __call__(
- self, messages: list[SamplingMessage], params: SamplingParams, context
- ): # type: ignore[override]
- console.print(" [bold blue]SAMPLING[/] Calling Claude API...")
- result = await super().__call__(messages, params, context)
- console.print(" [bold blue]SAMPLING[/] Response received")
- return result
-
-
-# Create the MCP server
-mcp = FastMCP("Haiku Generator")
-
-
-@mcp.tool
-async def write_haiku(topic: str, ctx: Context) -> str:
- """Write a haiku about any topic."""
- console.print(
- f" [bold cyan]SERVER[/] Tool 'write_haiku' called with topic: {topic}"
- )
-
- result = await ctx.sample(
- messages=f"Write a haiku about: {topic}",
- system_prompt="You are a poet. Write only the haiku, nothing else.",
- max_tokens=100,
- )
-
- console.print(" [bold cyan]SERVER[/] Returning haiku to client")
- return result.text or ""
-
-
-async def main():
- console.print(Panel.fit("[bold]MCP Sampling Flow Demo[/]", subtitle="text.py"))
- console.print()
-
- # Create the sampling handler
- handler = LoggingAnthropicHandler(default_model="claude-sonnet-4-5")
-
- # Connect client to server with the sampling handler
- async with Client(mcp, sampling_handler=handler) as client:
- console.print("[bold green]CLIENT[/] Calling tool 'write_haiku'...")
- console.print()
-
- result = await client.call_tool("write_haiku", {"topic": "Python programming"})
-
- console.print()
- console.print("[bold green]CLIENT[/] Received result:")
- console.print(Panel(result.data, title="Haiku", border_style="green")) # type: ignore[arg-type]
-
-
-if __name__ == "__main__":
- asyncio.run(main())
diff --git a/examples/sampling/tool_use.py b/examples/sampling/tool_use.py
deleted file mode 100644
index e7869a16c..000000000
--- a/examples/sampling/tool_use.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# /// script
-# dependencies = ["anthropic", "fastmcp", "rich"]
-# ///
-"""
-Sampling with Tools
-
-Demonstrates giving an LLM tools to use during sampling. The LLM can call
-helper functions to gather information before responding.
-
-Run:
- uv run examples/sampling/tool_use.py
-"""
-
-import asyncio
-import random
-from datetime import datetime
-
-from pydantic import BaseModel, Field
-from rich.console import Console
-from rich.panel import Panel
-
-from fastmcp import Client, Context, FastMCP
-from fastmcp.client.sampling import SamplingMessage, SamplingParams
-from fastmcp.client.sampling.handlers.anthropic import AnthropicSamplingHandler
-
-console = Console()
-
-
-class LoggingAnthropicHandler(AnthropicSamplingHandler):
- async def __call__(
- self, messages: list[SamplingMessage], params: SamplingParams, context
- ): # type: ignore[override]
- console.print(" [bold blue]SAMPLING[/] Calling Claude API...")
- result = await super().__call__(messages, params, context)
- console.print(" [bold blue]SAMPLING[/] Response received")
- return result
-
-
-# Define tools available to the LLM during sampling
-def add(a: float, b: float) -> str:
- """Add two numbers together."""
- result = a + b
- console.print(f" [bold magenta]TOOL[/] add({a}, {b}) = {result}")
- return str(result)
-
-
-def multiply(a: float, b: float) -> str:
- """Multiply two numbers together."""
- result = a * b
- console.print(f" [bold magenta]TOOL[/] multiply({a}, {b}) = {result}")
- return str(result)
-
-
-def get_current_time() -> str:
- """Get the current date and time."""
- console.print(" [bold magenta]TOOL[/] get_current_time()")
- return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-
-
-def roll_dice(sides: int = 6) -> str:
- """Roll a die with the specified number of sides."""
- result = random.randint(1, sides)
- console.print(f" [bold magenta]TOOL[/] roll_dice({sides}) = {result}")
- return str(result)
-
-
-# Structured output for the response
-class AssistantResponse(BaseModel):
- answer: str = Field(description="The answer to the user's question")
- tools_used: list[str] = Field(description="List of tools that were used")
- reasoning: str = Field(
- description="Brief explanation of how the answer was determined"
- )
-
-
-# Create the MCP server
-mcp = FastMCP("Smart Assistant")
-
-
-@mcp.tool
-async def ask_assistant(question: str, ctx: Context) -> dict:
- """Ask the assistant a question. It can use tools to help answer."""
- console.print(" [bold cyan]SERVER[/] Processing question...")
-
- result = await ctx.sample(
- messages=question,
- system_prompt="You are a helpful assistant with access to tools. Use them when needed to answer questions accurately.",
- tools=[add, multiply, get_current_time, roll_dice],
- result_type=AssistantResponse,
- )
-
- console.print(" [bold cyan]SERVER[/] Response ready")
- return result.result.model_dump() # type: ignore[attr-defined]
-
-
-async def main():
- console.print(Panel.fit("[bold]MCP Sampling Flow Demo[/]", subtitle="tool_use.py"))
- console.print()
-
- handler = LoggingAnthropicHandler(default_model="claude-sonnet-4-5")
-
- async with Client(mcp, sampling_handler=handler) as client:
- questions = [
- "What is 15 times 7, plus 23?",
- "Roll a 20-sided dice for me",
- "What time is it right now?",
- ]
-
- for question in questions:
- console.print(f"[bold green]CLIENT[/] Question: {question}")
- console.print()
-
- result = await client.call_tool("ask_assistant", {"question": question})
- data = result.data
-
- console.print(f"[bold green]CLIENT[/] Answer: {data['answer']}") # type: ignore[index]
- console.print(
- f" Tools used: {', '.join(data['tools_used']) or 'none'}"
- ) # type: ignore[index]
- console.print(f" Reasoning: {data['reasoning']}") # type: ignore[index]
- console.print()
-
-
-if __name__ == "__main__":
- asyncio.run(main())
diff --git a/examples/skills/client.py b/examples/skills/client.py
index a376fe235..d5005ac23 100644
--- a/examples/skills/client.py
+++ b/examples/skills/client.py
@@ -41,7 +41,7 @@ async def main():
print("=== Resource Templates ===")
templates = await client.list_resource_templates()
for t in templates:
- print(f" {t.uriTemplate}")
+ print(f" {t.uri_template}")
print()
# Read a skill's main file
diff --git a/examples/skills/download_skills.py b/examples/skills/download_skills.py
index 69b8d0373..c6e571c57 100644
--- a/examples/skills/download_skills.py
+++ b/examples/skills/download_skills.py
@@ -1,7 +1,7 @@
"""Example: Downloading skills from an MCP server.
This example shows how to use the skills client utilities to discover
-and download skills from any MCP server that exposes them via SkillsProvider.
+and download skills from any MCP server that exposes them via a skills provider.
Run this script:
uv run python examples/skills/download_skills.py
diff --git a/examples/smart_home/src/smart_home/hub.py b/examples/smart_home/src/smart_home/hub.py
index 827eae159..be2efc2f1 100644
--- a/examples/smart_home/src/smart_home/hub.py
+++ b/examples/smart_home/src/smart_home/hub.py
@@ -1,4 +1,4 @@
-from mcp.types import ToolAnnotations
+from mcp_types import ToolAnnotations
from phue import Bridge
from fastmcp import FastMCP
diff --git a/examples/smart_home/src/smart_home/lights/server.py b/examples/smart_home/src/smart_home/lights/server.py
index 7af3953d0..6fffcb298 100644
--- a/examples/smart_home/src/smart_home/lights/server.py
+++ b/examples/smart_home/src/smart_home/lights/server.py
@@ -7,7 +7,7 @@
from typing import Annotated, Any, Literal, TypedDict
-from mcp.types import ToolAnnotations
+from mcp_types import ToolAnnotations
from phue.exceptions import PhueException
from pydantic import Field
from typing_extensions import NotRequired
diff --git a/examples/tags_example.py b/examples/tags_example.py
index c5ff6ae0e..f714c9236 100644
--- a/examples/tags_example.py
+++ b/examples/tags_example.py
@@ -10,7 +10,7 @@ import asyncio
from fastapi import FastAPI
from fastmcp import FastMCP
-from fastmcp.server.openapi import MCPType, RouteMap
+from fastmcp.server.providers.openapi import MCPType, RouteMap
# Create a FastAPI app with tagged endpoints
app = FastAPI(title="Tagged API Example")
diff --git a/examples/task_elicitation.py b/examples/task_elicitation.py
index 51f7b046a..18d56f2e7 100644
--- a/examples/task_elicitation.py
+++ b/examples/task_elicitation.py
@@ -1,8 +1,16 @@
"""
-Background task elicitation demo.
+Background task input demo (SEP-2663 guard pattern).
-A background task (Docket) that pauses mid-execution to ask the user a
-question, waits for the answer, then resumes and finishes.
+A background task that pauses to ask the user a question, waits for the answer,
+then resumes and finishes. Under SEP-2663 a task gathers input by the *guard
+pattern*: instead of awaiting `ctx.elicit()` (which would block a worker), the
+tool *returns* an `InputRequiredResult`. That ends the leg; the client answers
+via the tasks protocol; the framework re-runs the tool with the answer on
+`ctx.input_responses`. No worker is ever blocked.
+
+The client side is transparent: `client.call_tool(...)` drives the whole
+round-trip — poll, answer via the `elicitation_handler`, poll again — and returns
+the finished result.
Works with both in-memory and Redis backends:
@@ -22,13 +30,15 @@ Requires the `docket` extra (included in dev dependencies).
import asyncio
from dataclasses import dataclass
-from mcp.types import TextContent
+import mcp_types
+from mcp_types import TextContent
from fastmcp import Context, FastMCP
from fastmcp.client import Client
-from fastmcp.server.elicitation import AcceptedElicitation
+from fastmcp_tasks import TasksExtension
mcp = FastMCP("Task Elicitation Demo")
+mcp.add_extension(TasksExtension())
@dataclass
@@ -37,43 +47,60 @@ class DinnerPrefs:
vegetarian: bool
-@mcp.tool(task=True)
-async def plan_dinner(ctx: Context) -> str:
- """Plan a dinner menu, asking the user what they're in the mood for."""
-
- await ctx.report_progress(0, 2, "Asking what you'd like...")
-
- result = await ctx.elicit(
- "What kind of dinner are you in the mood for?",
- response_type=DinnerPrefs,
+def _ask_dinner_prefs() -> mcp_types.InputRequiredResult:
+ """Return the input request that pauses the task until the client answers."""
+ request = mcp_types.ElicitRequest(
+ params=mcp_types.ElicitRequestFormParams(
+ message="What kind of dinner are you in the mood for?",
+ requested_schema={
+ "type": "object",
+ "properties": {
+ "cuisine": {"type": "string"},
+ "vegetarian": {"type": "boolean"},
+ },
+ "required": ["cuisine", "vegetarian"],
+ },
+ )
+ )
+ return mcp_types.InputRequiredResult(
+ result_type="input_required",
+ input_requests={"prefs": request},
)
- if not isinstance(result, AcceptedElicitation):
+
+@mcp.tool(task=True)
+async def plan_dinner(ctx: Context) -> str | mcp_types.InputRequiredResult:
+ """Plan a dinner menu, asking the user what they're in the mood for."""
+ responses = ctx.input_responses
+ if responses is None:
+ # First leg: ask for preferences and end the leg.
+ return _ask_dinner_prefs()
+
+ # Re-entered leg: the client's answer is on ctx.input_responses.
+ answer = responses["prefs"]
+ assert isinstance(answer, mcp_types.ElicitResult)
+ if answer.action != "accept" or answer.content is None:
return "Dinner cancelled!"
- prefs = result.data
- await ctx.report_progress(1, 2, "Planning your menu...")
- await asyncio.sleep(1)
- await ctx.report_progress(2, 2, "Done!")
-
- veg = "vegetarian " if prefs.vegetarian else ""
- return f"Tonight's menu: a lovely {veg}{prefs.cuisine} dinner!"
+ await asyncio.sleep(1) # "planning the menu"
+ veg = "vegetarian " if answer.content["vegetarian"] else ""
+ return f"Tonight's menu: a lovely {veg}{answer.content['cuisine']} dinner!"
async def handle_elicitation(message, response_type, params, context):
- """Handle elicitation requests from background tasks."""
+ """Answer elicitation requests raised by the background task."""
print(f" Server asks: {message}")
print(" Responding with: cuisine=Thai, vegetarian=True")
return DinnerPrefs(cuisine="Thai", vegetarian=True)
async def main():
- async with Client(mcp, elicitation_handler=handle_elicitation) as client:
- print("Starting background task...")
- task = await client.call_tool("plan_dinner", {}, task=True)
- print(f" task_id = {task.task_id}\n")
-
- result = await task.result()
+ client = Client(mcp, mode="auto", elicitation_handler=handle_elicitation)
+ async with client:
+ print("Calling plan_dinner (runs as a background task)...")
+ # call_tool drives the whole round-trip transparently: it polls, answers
+ # the task's input request via handle_elicitation, and returns the result.
+ result = await client.call_tool("plan_dinner", {})
assert isinstance(result.content[0], TextContent)
print(f"\nResult: {result.content[0].text}")
diff --git a/examples/tasks/.envrc b/examples/tasks/.envrc
index 87a7dfef9..7c90adf43 100644
--- a/examples/tasks/.envrc
+++ b/examples/tasks/.envrc
@@ -1,10 +1,11 @@
# FastMCP Tasks Example Environment Configuration
-# This file is loaded by direnv (https://direnv.net/) when you cd into this directory
-# Run `direnv allow` to enable automatic environment loading
+# Loaded by direnv (https://direnv.net/) when you cd into this directory.
+# Run `direnv allow` to enable automatic loading — or just `source .envrc`.
-# Configure Docket backend URL
-# Use Redis backend (requires docker-compose up)
-export FASTMCP_DOCKET_URL=redis://localhost:24242/0
+# In-process worker on an in-memory backend: no Redis, nothing to start.
+# This is the default the example runs on.
+export FASTMCP_DOCKET_URL=memory://
-# Or uncomment to use memory:// for single-process testing
-# export FASTMCP_DOCKET_URL=memory://
+# For distributed workers across separate processes (the `fastmcp tasks worker`
+# CLI), point at Redis instead and run `docker compose up -d` first:
+# export FASTMCP_DOCKET_URL=redis://localhost:24242/0
diff --git a/examples/tasks/README.md b/examples/tasks/README.md
index 8013968d5..d9f2dab5a 100644
--- a/examples/tasks/README.md
+++ b/examples/tasks/README.md
@@ -1,60 +1,75 @@
-# FastMCP Tasks Example
+# FastMCP Background Tasks Example
-Demonstrates background task execution with Docket, including progress tracking, distributed backends, and CLI worker management.
+A runnable client/server pair for SEP-2663 background tasks. The server exposes
+one `task=True` tool that reports progress as it works; the client drives it
+three ways — transparently, through an explicit handle, and several at once in
+parallel.
-## Setup
+This runs on the in-memory backend by default, so there's nothing to install or
+start beyond the two processes.
+
+## Run it
+
+In one terminal, start the server:
```bash
-# From the fastmcp root directory
-uv sync
+uv sync # from the fastmcp root, once
+python examples/tasks/server.py # listens on http://127.0.0.1:8000/mcp
+```
-# Start Redis
+In another terminal, drive it from the client:
+
+```bash
+# Transparent — call_tool runs the background task and returns its result
+python examples/tasks/client.py --duration 8
+
+# Explicit handle — returns immediately, poll it yourself, then collect
+python examples/tasks/client.py handle --duration 6
+
+# Parallel — fire several tasks at once and watch them overlap
+python examples/tasks/client.py parallel
+python examples/tasks/client.py parallel 8 6 4 2
+```
+
+The `parallel` run is the one to watch: four tasks of decreasing duration all
+start at once and total wall-clock tracks the *longest* task rather than the
+sum, because the worker runs them concurrently.
+
+## How it works
+
+The server enables tasks with one line:
+
+```python
+mcp = FastMCP("Tasks Example")
+mcp.add_extension(TasksExtension())
+```
+
+The client opts in by importing `fastmcp_tasks` (which it does to use
+`call_tool_task`). That single import enables task support for every `Client`
+in the process — without it, a `Client` never advertises the tasks capability,
+so the server would run the calls synchronously.
+
+## Distributed workers (optional)
+
+The default `memory://` backend runs the worker in the server process. To run
+workers as separate processes, point Docket at Redis and start it first:
+
+```bash
cd examples/tasks
docker compose up -d
+export FASTMCP_DOCKET_URL=redis://localhost:24242/0 # or: direnv allow
-# Load environment (or source .envrc manually)
-direnv allow
-
-# Run the server
-fastmcp run server.py
+python server.py # in one terminal
+python -m fastmcp_tasks.worker_cli worker server.py # extra worker(s) in others
```
-For single-process mode without Redis, set `FASTMCP_DOCKET_URL=memory://` (note: CLI workers won't work).
+| Backend | Workers |
+| ------------ | ------------------------------- |
+| `memory://` | in-process only (default) |
+| `redis://…` | distributed across processes |
-## Running the Client
+## Learn more
-```bash
-# Background execution with progress callbacks
-python examples/tasks/client.py --duration 10
-
-# Immediate execution (blocks)
-python examples/tasks/client.py immediate --duration 5
-```
-
-## Starting Additional Workers
-
-With Redis, you can run additional workers to process tasks in parallel:
-
-```bash
-fastmcp tasks worker server.py
-
-# Configure via environment:
-export FASTMCP_DOCKET_CONCURRENCY=20
-fastmcp tasks worker server.py
-```
-
-**Backend options:**
-- `memory://` - Single-process only (default)
-- `redis://` - Distributed, multi-process (Redis or Valkey)
-
-## Environment Variables
-
-| Variable | Default | Description |
-|----------|---------|-------------|
-| `FASTMCP_DOCKET_URL` | `memory://` | Docket backend URL |
-
-## Learn More
-
-- [FastMCP Tasks Documentation](https://gofastmcp.com/docs/tasks)
-- [Docket Documentation](https://github.com/PrefectHQ/docket)
-- [MCP Task Protocol (SEP-1686)](https://spec.modelcontextprotocol.io/specification/architecture/tasks/)
+- [Server background tasks](https://gofastmcp.com/servers/tasks)
+- [Client background tasks](https://gofastmcp.com/clients/tasks)
+- [Docket](https://github.com/chrisguidry/docket)
diff --git a/examples/tasks/client.py b/examples/tasks/client.py
index f72814889..fe93ea23b 100644
--- a/examples/tasks/client.py
+++ b/examples/tasks/client.py
@@ -1,159 +1,136 @@
-"""
-FastMCP Tasks Example Client
+"""FastMCP background-tasks example client (SEP-2663).
-Demonstrates calling tools both immediately and as background tasks,
-with real-time progress updates via status callbacks.
+Start the server first (`python examples/tasks/server.py`), then run any of the
+commands below against it over HTTP.
-Usage:
- # Make sure environment is configured (source .envrc or use direnv)
- source .envrc
+ # Transparent: call_tool drives the background task and returns its result
+ python examples/tasks/client.py --duration 8
- # Background task execution with progress callbacks (default)
- python client.py --duration 10
+ # Explicit handle: return immediately, poll it yourself, then collect
+ python examples/tasks/client.py handle --duration 6
- # Immediate execution (blocks until complete)
- python client.py immediate --duration 5
+ # Parallel: fire several tasks at once and watch them overlap
+ python examples/tasks/client.py parallel
+
+Importing `fastmcp_tasks` (below) enables client task support for every Client
+in the process — without it, a Client never advertises the tasks capability and
+the server runs its calls synchronously.
"""
import asyncio
-import sys
-from pathlib import Path
+import time
from typing import Annotated
import cyclopts
-from mcp.types import GetTaskResult, TextContent
+from mcp_types import TextContent
from rich.console import Console
from fastmcp.client import Client
+from fastmcp_tasks import call_tool_task # importing enables client task support
+
+SERVER_URL = "http://127.0.0.1:8000/mcp"
console = Console()
-app = cyclopts.App(name="tasks-client", help="FastMCP Tasks Example Client")
+app = cyclopts.App(name="tasks-client", help="FastMCP background-tasks example client")
-def load_server():
- """Load the example server."""
- examples_dir = Path(__file__).parent.parent.parent
- if str(examples_dir) not in sys.path:
- sys.path.insert(0, str(examples_dir))
-
- import examples.tasks.server as server_module
-
- return server_module.mcp
-
-
-# Track last message to deduplicate consecutive identical notifications
-# Note: Docket fires separate events for progress.increment() and progress.set_message(),
-# but MCP's statusMessage field only carries the text message (no numerical progress).
-# This means we often get duplicate notifications with identical messages.
-_last_notification_message = None
-
-
-def print_notification(status: GetTaskResult) -> None:
- """Callback function for push notifications from server.
-
- This is called automatically when the server sends notifications/tasks/status.
- Deduplicates identical consecutive messages to keep output clean.
- """
- global _last_notification_message
-
- # Skip if this is the same message we just printed
- if status.statusMessage == _last_notification_message:
- return
-
- _last_notification_message = status.statusMessage
-
- color = {
- "working": "yellow",
- "completed": "green",
- "failed": "red",
- }.get(status.status, "yellow")
-
- icon = {
- "working": "🚀",
- "completed": "✅",
- "failed": "❌",
- }.get(status.status, "⚠️")
-
- console.print(
- f"[{color}]📢 Notification: {status.status} {icon} - {status.statusMessage}[/{color}]"
- )
+def _text(result) -> str:
+ assert isinstance(result.content[0], TextContent)
+ return result.content[0].text
@app.default
-async def task(
- duration: Annotated[
- int,
- cyclopts.Parameter(help="Duration of computation in seconds (1-60)"),
- ] = 10,
+async def transparent(
+ duration: Annotated[int, cyclopts.Parameter(help="Seconds (1-60)")] = 8,
):
- """Execute as background task with real-time progress callbacks."""
- if duration < 1 or duration > 60:
- console.print("[red]Error: Duration must be between 1 and 60 seconds[/red]")
- sys.exit(1)
+ """Call the tool transparently: the client drives the task to completion.
- server = load_server()
-
- console.print(f"\n[bold]Calling slow_computation(duration={duration})[/bold]")
- console.print("Mode: [cyan]Background task[/cyan]\n")
-
- async with Client(server) as client:
- task_obj = await client.call_tool(
+ The server runs `slow_computation` as a background task, but `call_tool`
+ polls it under the hood and returns the tool's real result — the calling
+ code looks exactly like an ordinary synchronous tool call.
+ """
+ async with Client(SERVER_URL, mode="auto") as client:
+ console.print(f"\n[bold]Transparent call[/bold] (duration={duration})\n")
+ started = time.perf_counter()
+ result = await client.call_tool(
"slow_computation",
- arguments={"duration": duration},
- task=True,
+ {"label": "transparent", "duration": duration},
)
-
- console.print(f"Task started: [cyan]{task_obj.task_id}[/cyan]\n")
-
- # Register callback for real-time push notifications
- task_obj.on_status_change(print_notification)
-
- console.print(
- "[dim]Notifications will appear as the server sends them...[/dim]\n"
- )
-
- # Do other work while task runs in background
- for i in range(3):
- await asyncio.sleep(0.5)
- console.print(f"[dim]Client doing other work... ({i + 1}/3)[/dim]")
-
- console.print()
-
- # Wait for task to complete
- console.print("[dim]Waiting for final result...[/dim]")
- result = await task_obj.result()
-
- console.print("\n[bold]Result:[/bold]")
- assert isinstance(result.content[0], TextContent)
- console.print(f" {result.content[0].text}")
+ console.print(f"[green]{_text(result)}[/green]")
+ console.print(f"[dim]elapsed {time.perf_counter() - started:.1f}s[/dim]")
@app.command
-async def immediate(
- duration: Annotated[
- int,
- cyclopts.Parameter(help="Duration of computation in seconds (1-60)"),
- ] = 5,
+async def handle(
+ duration: Annotated[int, cyclopts.Parameter(help="Seconds (1-60)")] = 6,
):
- """Execute the tool immediately (blocks until complete)."""
- if duration < 1 or duration > 60:
- console.print("[red]Error: Duration must be between 1 and 60 seconds[/red]")
- sys.exit(1)
-
- server = load_server()
-
- console.print(f"\n[bold]Calling slow_computation(duration={duration})[/bold]")
- console.print("Mode: [cyan]Immediate execution[/cyan]\n")
-
- async with Client(server) as client:
- result = await client.call_tool(
- "slow_computation",
- arguments={"duration": duration},
+ """Use the explicit handle: return immediately, then drive the task yourself."""
+ async with Client(SERVER_URL, mode="auto") as client:
+ console.print(f"\n[bold]Explicit handle[/bold] (duration={duration})\n")
+ task = await call_tool_task(
+ client, "slow_computation", {"label": "handle", "duration": duration}
)
+ console.print(f"Task started: [cyan]{task.task_id}[/cyan]\n")
- console.print("\n[bold]Result:[/bold]")
- assert isinstance(result.content[0], TextContent)
- console.print(f" {result.content[0].text}")
+ # Do other work while the task runs, checking its status as you go.
+ while True:
+ status = await task.status()
+ if status.status in ("completed", "failed", "cancelled"):
+ break
+ console.print(f"[dim]still {status.status}: {status.status_message}[/dim]")
+ await asyncio.sleep(1)
+
+ result = await task.result()
+ console.print(f"\n[green]{_text(result)}[/green]")
+
+
+@app.command
+async def parallel(
+ durations: Annotated[
+ list[int] | None,
+ cyclopts.Parameter(help="One task per duration (default: 5 4 3 2)"),
+ ] = None,
+):
+ """Fire several background tasks at once and drive them concurrently.
+
+ Each `call_tool_task` returns immediately, so we start every task before
+ awaiting any of them. The worker runs them in parallel, so total wall-clock
+ tracks the *longest* task, not the sum — proof the work actually overlaps.
+ """
+ durations = durations or [5, 4, 3, 2]
+
+ async with Client(SERVER_URL, mode="auto") as client:
+ console.print(f"\n[bold]Parallel tasks[/bold]: durations={durations}\n")
+ started = time.perf_counter()
+
+ # Start every task up front — none of these await completion.
+ tasks = [
+ await call_tool_task(
+ client,
+ "slow_computation",
+ {"label": f"task-{i}({d}s)", "duration": d},
+ )
+ for i, d in enumerate(durations)
+ ]
+ for task in tasks:
+ console.print(f" started [cyan]{task.task_id}[/cyan]")
+
+ # Await them together; results print as each task finishes.
+ async def collect(task):
+ result = await task.result()
+ console.print(
+ f"[green]✓[/green] {_text(result)} "
+ f"[dim](+{time.perf_counter() - started:.1f}s)[/dim]"
+ )
+
+ console.print()
+ await asyncio.gather(*(collect(task) for task in tasks))
+
+ total = time.perf_counter() - started
+ console.print(
+ f"\n[bold]All {len(tasks)} tasks done in {total:.1f}s[/bold] "
+ f"[dim](longest single task: {max(durations)}s)[/dim]"
+ )
if __name__ == "__main__":
diff --git a/examples/tasks/server.py b/examples/tasks/server.py
index 77b3cde82..745009ef7 100644
--- a/examples/tasks/server.py
+++ b/examples/tasks/server.py
@@ -1,75 +1,65 @@
-"""
-FastMCP Tasks Example Server
+"""FastMCP background-tasks example server (SEP-2663).
-Demonstrates background task execution with progress tracking using Docket.
+Run this in one terminal, then drive it from `client.py` in another. It exposes
+one `task=True` tool that reports progress as it works, so you can watch the
+client poll a real background task over HTTP.
-Setup:
- 1. Start Redis: docker compose up -d
- 2. Load environment: source .envrc
- 3. Run server: fastmcp run server.py
+ # From the fastmcp root (memory:// backend, no Redis needed):
+ python examples/tasks/server.py
-The example uses Redis by default to demonstrate distributed task execution
-and the fastmcp tasks CLI commands.
+The server listens on http://localhost:8000/mcp. The tasks extension runs its
+Docket worker in-process on the default `memory://` backend, so several tasks
+submitted at once execute concurrently (worker concurrency defaults to 10).
+Point `FASTMCP_DOCKET_URL` at Redis to distribute work across separate worker
+processes instead — see README.md.
"""
import asyncio
import logging
+from datetime import timedelta
from typing import Annotated
-from docket import Logged
-
from fastmcp import FastMCP
from fastmcp.dependencies import Progress
+from fastmcp.utilities.tasks import TaskConfig
+from fastmcp_tasks import TasksExtension
-# Configure logging
-logging.basicConfig(level=logging.INFO)
-logger = logging.getLogger(__name__)
+logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
+logger = logging.getLogger("tasks-example")
-# Create server
+# Enable SEP-2663 background tasks. With no arguments the extension reads the
+# FASTMCP_DOCKET_* environment and falls back to an in-process memory:// worker.
mcp = FastMCP("Tasks Example")
+mcp.add_extension(TasksExtension())
-@mcp.tool(task=True)
+# A short poll interval keeps the example snappy: the client observes each
+# task finishing within ~1s. The default is 5s, tuned for real workloads.
+@mcp.tool(task=TaskConfig(poll_interval=timedelta(seconds=1)))
async def slow_computation(
- duration: Annotated[int, Logged],
+ label: Annotated[str, "A name for this run, echoed back in progress logs"],
+ duration: Annotated[int, "How many seconds the computation should take (1-60)"],
progress: Progress = Progress(),
) -> str:
+ """Spend `duration` seconds working, reporting progress once per second.
+
+ Marked `task=True`, so a task-aware client runs it in the background and
+ polls for progress and the final result instead of blocking on the call.
"""
- Perform a slow computation that takes `duration` seconds.
+ if not 1 <= duration <= 60:
+ raise ValueError("duration must be between 1 and 60 seconds")
- This tool demonstrates progress tracking with background tasks.
- It logs progress every 1-2 seconds and reports progress via Docket.
-
- Args:
- duration: Number of seconds the computation should take (1-60)
-
- Returns:
- A completion message with the total duration
- """
- if duration < 1 or duration > 60:
- raise ValueError("Duration must be between 1 and 60 seconds")
-
- logger.info(f"Starting slow computation for {duration} seconds")
-
- # Set total progress units
+ logger.info("[%s] starting — %ds", label, duration)
await progress.set_total(duration)
- # Process each second
- for i in range(duration):
- # Sleep for 1 second
+ for elapsed in range(1, duration + 1):
await asyncio.sleep(1)
-
- # Update progress
- elapsed = i + 1
- remaining = duration - elapsed
await progress.increment()
- await progress.set_message(
- f"Working... {elapsed}/{duration}s ({remaining}s remaining)"
- )
+ await progress.set_message(f"{label}: {elapsed}/{duration}s")
- # Log every 1-2 seconds
- if elapsed % 2 == 0 or elapsed == duration:
- logger.info(f"Progress: {elapsed}/{duration}s")
+ logger.info("[%s] done", label)
+ return f"{label} finished in {duration}s"
- logger.info(f"Completed computation in {duration} seconds")
- return f"Computation completed successfully in {duration} seconds!"
+
+if __name__ == "__main__":
+ mcp.run(transport="http", host="127.0.0.1", port=8000)
diff --git a/examples/testing_demo/README.md b/examples/testing_demo/README.md
index e4e711111..346dc8cc8 100644
--- a/examples/testing_demo/README.md
+++ b/examples/testing_demo/README.md
@@ -81,4 +81,4 @@ uv run fastmcp inspect server.py
## Learning More
-For detailed information about testing FastMCP servers, see the [Testing Documentation](../../docs/patterns/testing.mdx).
+For detailed information about testing FastMCP servers, see the [Testing Documentation](../../docs/servers/testing.mdx).
diff --git a/examples/testing_demo/pyproject.toml b/examples/testing_demo/pyproject.toml
index dce14d85e..6130b9cf2 100644
--- a/examples/testing_demo/pyproject.toml
+++ b/examples/testing_demo/pyproject.toml
@@ -6,7 +6,7 @@ readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"fastmcp>=2.0.0",
- "pytest>=8.3.3",
+ "pytest>=9.0.3",
"pytest-asyncio>=1.2.0",
"dirty-equals>=0.9.0",
]
diff --git a/examples/testing_demo/uv.lock b/examples/testing_demo/uv.lock
index 8f07579f9..a9cb8f163 100644
--- a/examples/testing_demo/uv.lock
+++ b/examples/testing_demo/uv.lock
@@ -1,6 +1,39 @@
version = 1
revision = 3
requires-python = ">=3.10"
+resolution-markers = [
+ "python_full_version >= '3.14' and sys_platform == 'win32'",
+ "python_full_version >= '3.14' and sys_platform != 'win32'",
+ "python_full_version < '3.14' and sys_platform == 'win32'",
+ "python_full_version < '3.14' and sys_platform != 'win32'",
+]
+
+[options]
+exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values.
+exclude-newer-span = "P1W"
+
+[options.exclude-newer-package]
+mcp-types = false
+prefab-ui = false
+truststore = false
+fastmcp-slim = false
+fastmcp = false
+mcp = false
+httpcore2 = false
+fastmcp-remote = false
+httpx2 = false
+
+[[package]]
+name = "aiofile"
+version = "3.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "caio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" },
+]
[[package]]
name = "annotated-types"
@@ -13,26 +46,16 @@ wheels = [
[[package]]
name = "anyio"
-version = "4.11.0"
+version = "4.12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" },
- { name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
-]
-
-[[package]]
-name = "async-timeout"
-version = "5.0.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" },
+ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
]
[[package]]
@@ -46,14 +69,15 @@ wheels = [
[[package]]
name = "authlib"
-version = "1.6.6"
+version = "1.7.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
+ { name = "joserfc" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894, upload-time = "2025-12-12T08:01:41.464Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/36/98/7d93f30d029643c0275dbc0bd6d5a6f670661ee6c9a94d93af7ab4887600/authlib-1.7.2.tar.gz", hash = "sha256:2cea25fefcd4e7173bdf1372c0afc265c8034b23a8cd5dcb6a9164b826c64231", size = 176511, upload-time = "2026-05-06T08:10:23.116Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/95/adcb68e20c34162e9135f370d6e31737719c2b6f94bc953fe7ed1f10fe21/authlib-1.7.2-py2.py3-none-any.whl", hash = "sha256:3e1faedc9d87e7d56a164eca3ccb6ace0d61b94abe83e92242f8dc8bba9b4a9f", size = 259548, upload-time = "2026-05-06T08:10:21.436Z" },
]
[[package]]
@@ -76,29 +100,58 @@ wheels = [
[[package]]
name = "beartype"
-version = "0.22.5"
+version = "0.22.9"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a6/09/9003e5662691056e0e8b2e6f57c799e71875fac0be0e785d8cb11557cd2a/beartype-0.22.5.tar.gz", hash = "sha256:516a9096cc77103c96153474fa35c3ebcd9d36bd2ec8d0e3a43307ced0fa6341", size = 1586256, upload-time = "2025-11-01T05:49:20.771Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f7/f6/073d19f7b571c08327fbba3f8e011578da67ab62a11f98911274ff80653f/beartype-0.22.5-py3-none-any.whl", hash = "sha256:d9743dd7cd6d193696eaa1e025f8a70fb09761c154675679ff236e61952dfba0", size = 1321700, upload-time = "2025-11-01T05:49:18.436Z" },
+ { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" },
]
[[package]]
name = "cachetools"
-version = "6.2.1"
+version = "7.0.5"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325, upload-time = "2025-10-12T14:55:30.139Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/dd/57fe3fdb6e65b25a5987fd2cdc7e22db0aef508b91634d2e57d22928d41b/cachetools-7.0.5.tar.gz", hash = "sha256:0cd042c24377200c1dcd225f8b7b12b0ca53cc2c961b43757e774ebe190fd990", size = 37367, upload-time = "2026-03-09T20:51:29.451Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280, upload-time = "2025-10-12T14:55:28.382Z" },
+ { url = "https://files.pythonhosted.org/packages/06/f3/39cf3367b8107baa44f861dc802cbf16263c945b62d8265d36034fc07bea/cachetools-7.0.5-py3-none-any.whl", hash = "sha256:46bc8ebefbe485407621d0a4264b23c080cedd913921bad7ac3ed2f26c183114", size = 13918, upload-time = "2026-03-09T20:51:27.33Z" },
+]
+
+[[package]]
+name = "caio"
+version = "0.9.25"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6a/80/ea4ead0c5d52a9828692e7df20f0eafe8d26e671ce4883a0a146bb91049e/caio-0.9.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ca6c8ecda611478b6016cb94d23fd3eb7124852b985bdec7ecaad9f3116b9619", size = 36836, upload-time = "2025-12-26T15:22:04.662Z" },
+ { url = "https://files.pythonhosted.org/packages/17/b9/36715c97c873649d1029001578f901b50250916295e3dddf20c865438865/caio-0.9.25-cp310-cp310-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db9b5681e4af8176159f0d6598e73b2279bb661e718c7ac23342c550bd78c241", size = 79695, upload-time = "2025-12-26T15:22:18.818Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ab/07080ecb1adb55a02cbd8ec0126aa8e43af343ffabb6a71125b42670e9a1/caio-0.9.25-cp310-cp310-manylinux_2_34_aarch64.whl", hash = "sha256:bf61d7d0c4fd10ffdd98ca47f7e8db4d7408e74649ffaf4bef40b029ada3c21b", size = 79457, upload-time = "2026-03-04T22:08:16.024Z" },
+ { url = "https://files.pythonhosted.org/packages/88/95/dd55757bb671eb4c376e006c04e83beb413486821f517792ea603ef216e9/caio-0.9.25-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:ab52e5b643f8bbd64a0605d9412796cd3464cb8ca88593b13e95a0f0b10508ae", size = 77705, upload-time = "2026-03-04T22:08:17.202Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/90/543f556fcfcfa270713eef906b6352ab048e1e557afec12925c991dc93c2/caio-0.9.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d6956d9e4a27021c8bd6c9677f3a59eb1d820cc32d0343cea7961a03b1371965", size = 36839, upload-time = "2025-12-26T15:21:40.267Z" },
+ { url = "https://files.pythonhosted.org/packages/51/3b/36f3e8ec38dafe8de4831decd2e44c69303d2a3892d16ceda42afed44e1b/caio-0.9.25-cp311-cp311-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bf84bfa039f25ad91f4f52944452a5f6f405e8afab4d445450978cd6241d1478", size = 80255, upload-time = "2025-12-26T15:22:20.271Z" },
+ { url = "https://files.pythonhosted.org/packages/df/ce/65e64867d928e6aff1b4f0e12dba0ef6d5bf412c240dc1df9d421ac10573/caio-0.9.25-cp311-cp311-manylinux_2_34_aarch64.whl", hash = "sha256:ae3d62587332bce600f861a8de6256b1014d6485cfd25d68c15caf1611dd1f7c", size = 80052, upload-time = "2026-03-04T22:08:20.402Z" },
+ { url = "https://files.pythonhosted.org/packages/46/90/e278863c47e14ec58309aa2e38a45882fbe67b4cc29ec9bc8f65852d3e45/caio-0.9.25-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:fc220b8533dcf0f238a6b1a4a937f92024c71e7b10b5a2dfc1c73604a25709bc", size = 78273, upload-time = "2026-03-04T22:08:21.368Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" },
+ { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" },
+ { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" },
+ { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" },
+ { url = "https://files.pythonhosted.org/packages/69/ca/a08fdc7efdcc24e6a6131a93c85be1f204d41c58f474c42b0670af8c016b/caio-0.9.25-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fab6078b9348e883c80a5e14b382e6ad6aabbc4429ca034e76e730cf464269db", size = 36978, upload-time = "2025-12-26T15:21:41.055Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/6c/d4d24f65e690213c097174d26eda6831f45f4734d9d036d81790a27e7b78/caio-0.9.25-cp314-cp314-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:44a6b58e52d488c75cfaa5ecaa404b2b41cc965e6c417e03251e868ecd5b6d77", size = 81832, upload-time = "2025-12-26T15:22:22.757Z" },
+ { url = "https://files.pythonhosted.org/packages/87/a4/e534cf7d2d0e8d880e25dd61e8d921ffcfe15bd696734589826f5a2df727/caio-0.9.25-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:628a630eb7fb22381dd8e3c8ab7f59e854b9c806639811fc3f4310c6bd711d79", size = 81565, upload-time = "2026-03-04T22:08:27.483Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/ed/bf81aeac1d290017e5e5ac3e880fd56ee15e50a6d0353986799d1bc5cfd5/caio-0.9.25-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:0ba16aa605ccb174665357fc729cf500679c2d94d5f1458a6f0d5ca48f2060a7", size = 80071, upload-time = "2026-03-04T22:08:28.751Z" },
+ { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" },
]
[[package]]
name = "certifi"
-version = "2025.10.5"
+version = "2026.2.25"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/4c/5b/b6ce21586237c77ce67d01dc5507039d444b630dd76611bbca2d8e5dcd91/certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43", size = 164519, upload-time = "2025-10-05T04:12:15.808Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de", size = 163286, upload-time = "2025-10-05T04:12:14.03Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
]
[[package]]
@@ -183,114 +236,16 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
-[[package]]
-name = "charset-normalizer"
-version = "3.4.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709, upload-time = "2025-10-14T04:40:11.385Z" },
- { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814, upload-time = "2025-10-14T04:40:13.135Z" },
- { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467, upload-time = "2025-10-14T04:40:14.728Z" },
- { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280, upload-time = "2025-10-14T04:40:16.14Z" },
- { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454, upload-time = "2025-10-14T04:40:17.567Z" },
- { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609, upload-time = "2025-10-14T04:40:19.08Z" },
- { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849, upload-time = "2025-10-14T04:40:20.607Z" },
- { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586, upload-time = "2025-10-14T04:40:21.719Z" },
- { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290, upload-time = "2025-10-14T04:40:23.069Z" },
- { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663, upload-time = "2025-10-14T04:40:24.17Z" },
- { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964, upload-time = "2025-10-14T04:40:25.368Z" },
- { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064, upload-time = "2025-10-14T04:40:26.806Z" },
- { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015, upload-time = "2025-10-14T04:40:28.284Z" },
- { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792, upload-time = "2025-10-14T04:40:29.613Z" },
- { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198, upload-time = "2025-10-14T04:40:30.644Z" },
- { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262, upload-time = "2025-10-14T04:40:32.108Z" },
- { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
- { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
- { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
- { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
- { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
- { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
- { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
- { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
- { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
- { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
- { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
- { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
- { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
- { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
- { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
- { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
- { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
- { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
- { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
- { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
- { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
- { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
- { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
- { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
- { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
- { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
- { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
- { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
- { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
- { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
- { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
- { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
- { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
- { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
- { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
- { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" },
- { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" },
- { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" },
- { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" },
- { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" },
- { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" },
- { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" },
- { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" },
- { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" },
- { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" },
- { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" },
- { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" },
- { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" },
- { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" },
- { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" },
- { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" },
- { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" },
- { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" },
- { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" },
- { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" },
- { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" },
- { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" },
- { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" },
- { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" },
- { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" },
- { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" },
- { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" },
- { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" },
- { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" },
- { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
-]
-
[[package]]
name = "click"
-version = "8.3.0"
+version = "8.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" },
-]
-
-[[package]]
-name = "cloudpickle"
-version = "3.1.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" },
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
]
[[package]]
@@ -304,67 +259,64 @@ wheels = [
[[package]]
name = "cryptography"
-version = "46.0.5"
+version = "49.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
- { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
- { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
- { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
- { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
- { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
- { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
- { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
- { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
- { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
- { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
- { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
- { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
- { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
- { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" },
- { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" },
- { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" },
- { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" },
- { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" },
- { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" },
- { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" },
- { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" },
- { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" },
- { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" },
- { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" },
- { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" },
- { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" },
- { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" },
- { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
- { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
- { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
- { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
- { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
- { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
- { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
- { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
- { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
- { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
- { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
- { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
- { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
- { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
- { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" },
- { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" },
- { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" },
- { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" },
- { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" },
- { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" },
+ { url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" },
+ { url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" },
+ { url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" },
+ { url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" },
+ { url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" },
+ { url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" },
+ { url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" },
+ { url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" },
+ { url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" },
+ { url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" },
+ { url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" },
+ { url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" },
+ { url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" },
+ { url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" },
+ { url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" },
+ { url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" },
]
[[package]]
name = "cyclopts"
-version = "4.2.1"
+version = "4.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
@@ -374,27 +326,18 @@ dependencies = [
{ name = "tomli", marker = "python_full_version < '3.11'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/8a/51/a67b17fac2530d22216a335bd10f48631412dd824013ea559ec236668f76/cyclopts-4.2.1.tar.gz", hash = "sha256:49bb4c35644e7a9658f706ade4cf1a9958834b2dca4425e2fafecf8a0537fac7", size = 148693, upload-time = "2025-10-31T14:30:58.681Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/2c/e7/3e26855c046ac527cf94d890f6698e703980337f22ea7097e02b35b910f9/cyclopts-4.10.0.tar.gz", hash = "sha256:0ae04a53274e200ef3477c8b54de63b019bc6cd0162d75c718bf40c9c3fb5268", size = 166394, upload-time = "2026-03-14T14:09:31.043Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/4d/1d/2b313e157c9c7bba319e42f464d15073d32a81ac4827bdc5b7de38832b3e/cyclopts-4.2.1-py3-none-any.whl", hash = "sha256:17a801faa814988b0307385ef8aaeb6b14b4d64473015a2d66bde9ea13f14d9c", size = 184333, upload-time = "2025-10-31T14:30:57.581Z" },
+ { url = "https://files.pythonhosted.org/packages/06/06/d68a5d5d292c2ad2bc6a02e5ca2cb1bb9c15e941ab02f004a06a342d7f0f/cyclopts-4.10.0-py3-none-any.whl", hash = "sha256:50f333382a60df8d40ec14aa2e627316b361c4f478598ada1f4169d959bf9ea7", size = 204097, upload-time = "2026-03-14T14:09:32.504Z" },
]
[[package]]
name = "dirty-equals"
-version = "0.10.0"
+version = "0.11"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/30/69/f8a63f97166565dbf01e6a3fdf4665313719a6781125f105e4ffde82c5cd/dirty_equals-0.10.0.tar.gz", hash = "sha256:623d7a07c5ba437f1a834c6246d1e3eb97238ca70331c61a499d9aabd757b899", size = 125778, upload-time = "2025-09-19T16:05:31.371Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/30/1d/c5913ac9d6615515a00f4bdc71356d302437cb74ff2e9aaccd3c14493b78/dirty_equals-0.11.tar.gz", hash = "sha256:f4ac74ee88f2d11e2fa0f65eb30ee4f07105c5f86f4dc92b09eb1138775027c3", size = 128067, upload-time = "2025-11-17T01:51:24.451Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e9/87/0fc6e51f9db3a3b3de88fb0c9cf6414d4572d565f4ba4d166023cbd4354d/dirty_equals-0.10.0-py3-none-any.whl", hash = "sha256:bbf4a4eaafd56e371dafe2edf2265315ebd71a441b142ed801511aa33e4c3438", size = 28014, upload-time = "2025-09-19T16:05:29.953Z" },
-]
-
-[[package]]
-name = "diskcache"
-version = "5.6.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/3f/21/1c1ffc1a039ddcc459db43cc108658f32c57d271d7289a2794e401d0fdb6/diskcache-5.6.3.tar.gz", hash = "sha256:2c3a3fa2743d8535d832ec61c2054a1641f41775aa7c556758a109941e33e4fc", size = 67916, upload-time = "2023-08-31T06:12:00.316Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3f/27/4570e78fc0bf5ea0ca45eb1de3818a23787af9b390c0b0a0033a1b8236f9/diskcache-5.6.3-py3-none-any.whl", hash = "sha256:5e31b2d5fbad117cc363ebaf6b689474db18a1f6438bc82358b024abd4c2ca19", size = 45550, upload-time = "2023-08-31T06:11:58.822Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/8d/dbff05239043271dbeace563a7686212a3dd517864a35623fe4d4a64ca19/dirty_equals-0.11-py3-none-any.whl", hash = "sha256:b1d7093273fc2f9be12f443a8ead954ef6daaf6746fd42ef3a5616433ee85286", size = 28051, upload-time = "2025-11-17T01:51:22.849Z" },
]
[[package]]
@@ -417,11 +360,11 @@ wheels = [
[[package]]
name = "docutils"
-version = "0.22.3"
+version = "0.22.4"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d9/02/111134bfeb6e6c7ac4c74594e39a59f6c0195dc4846afbeac3cba60f1927/docutils-0.22.3.tar.gz", hash = "sha256:21486ae730e4ca9f622677b1412b879af1791efcfba517e4c6f60be543fc8cdd", size = 2290153, upload-time = "2025-11-06T02:35:55.655Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/11/a8/c6a4b901d17399c77cd81fb001ce8961e9f5e04d3daf27e8925cb012e163/docutils-0.22.3-py3-none-any.whl", hash = "sha256:bd772e4aca73aff037958d44f2be5229ded4c09927fcf8690c577b66234d6ceb", size = 633032, upload-time = "2025-11-06T02:35:52.391Z" },
+ { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" },
]
[[package]]
@@ -439,60 +382,46 @@ wheels = [
[[package]]
name = "exceptiongroup"
-version = "1.3.0"
+version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
-]
-
-[[package]]
-name = "fakeredis"
-version = "2.33.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "redis" },
- { name = "sortedcontainers" },
- { name = "typing-extensions", marker = "python_full_version < '3.11'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/5f/f9/57464119936414d60697fcbd32f38909bb5688b616ae13de6e98384433e0/fakeredis-2.33.0.tar.gz", hash = "sha256:d7bc9a69d21df108a6451bbffee23b3eba432c21a654afc7ff2d295428ec5770", size = 175187, upload-time = "2025-12-16T19:45:52.269Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/6e/78/a850fed8aeef96d4a99043c90b818b2ed5419cd5b24a4049fd7cfb9f1471/fakeredis-2.33.0-py3-none-any.whl", hash = "sha256:de535f3f9ccde1c56672ab2fdd6a8efbc4f2619fc2f1acc87b8737177d71c965", size = 119605, upload-time = "2025-12-16T19:45:51.08Z" },
-]
-
-[package.optional-dependencies]
-lua = [
- { name = "lupa" },
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
]
[[package]]
name = "fastmcp"
-version = "2.14.0"
+version = "3.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "authlib" },
{ name = "cyclopts" },
{ name = "exceptiongroup" },
{ name = "httpx" },
+ { name = "jsonref" },
{ name = "jsonschema-path" },
{ name = "mcp" },
{ name = "openapi-pydantic" },
+ { name = "opentelemetry-api" },
+ { name = "packaging" },
{ name = "platformdirs" },
- { name = "py-key-value-aio", extra = ["disk", "keyring", "memory"] },
+ { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] },
{ name = "pydantic", extra = ["email"] },
- { name = "pydocket" },
{ name = "pyperclip" },
{ name = "python-dotenv" },
+ { name = "pyyaml" },
{ name = "rich" },
+ { name = "uncalled-for" },
{ name = "uvicorn" },
+ { name = "watchfiles" },
{ name = "websockets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/35/50/9bb042a2d290ccadb35db3580ac507f192e1a39c489eb8faa167cd5e3b57/fastmcp-2.14.0.tar.gz", hash = "sha256:c1f487b36a3e4b043dbf3330e588830047df2e06f8ef0920d62dfb34d0905727", size = 8232562, upload-time = "2025-12-11T23:04:27.134Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/32/4f1b2cfd7b50db89114949f90158b1dcc2c92a1917b9f57c0ff24e47a2f4/fastmcp-3.2.0.tar.gz", hash = "sha256:d4830b8ffc3592d3d9c76dc0f398904cf41f04910e41a0de38cc1004e0903bef", size = 26318581, upload-time = "2026-03-30T20:25:37.692Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/54/73/b5656172a6beb2eacec95f04403ddea1928e4b22066700fd14780f8f45d1/fastmcp-2.14.0-py3-none-any.whl", hash = "sha256:7b374c0bcaf1ef1ef46b9255ea84c607f354291eaf647ff56a47c69f5ec0c204", size = 398965, upload-time = "2025-12-11T23:04:25.587Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/67/684fa2d2de1e7504549d4ca457b4f854ccec3cd3be03bd86b33b599fbf58/fastmcp-3.2.0-py3-none-any.whl", hash = "sha256:e71aba3df16f86f546a4a9e513261d3233bcc92bef0dfa647bac3fa33623f681", size = 705550, upload-time = "2026-03-30T20:25:35.499Z" },
]
[[package]]
@@ -543,23 +472,23 @@ wheels = [
[[package]]
name = "idna"
-version = "3.11"
+version = "3.18"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
]
[[package]]
name = "importlib-metadata"
-version = "8.7.0"
+version = "8.7.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "zipp" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/49/3b30cad09e7771a4982d9975a8cbf64f00d4a1ececb53297f1d9a7be1b10/importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb", size = 57107, upload-time = "2025-12-21T10:00:19.278Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/5e/f8e9a1d23b9c20a551a8a02ea3637b4642e22c2626e3a13a9a29cdea99eb/importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151", size = 27865, upload-time = "2025-12-21T10:00:18.329Z" },
]
[[package]]
@@ -585,26 +514,26 @@ wheels = [
[[package]]
name = "jaraco-context"
-version = "6.0.1"
+version = "6.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-tarfile", marker = "python_full_version < '3.12'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/27/7b/c3081ff1af947915503121c649f26a778e1a2101fd525f74aef997d75b7e/jaraco_context-6.1.1.tar.gz", hash = "sha256:bc046b2dc94f1e5532bd02402684414575cc11f565d929b6563125deb0a6e581", size = 15832, upload-time = "2026-03-07T15:46:04.63Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/49/c152890d49102b280ecf86ba5f80a8c111c3a155dafa3bd24aeb64fde9e1/jaraco_context-6.1.1-py3-none-any.whl", hash = "sha256:0df6a0287258f3e364072c3e40d5411b20cafa30cb28c4839d24319cecf9f808", size = 7005, upload-time = "2026-03-07T15:46:03.515Z" },
]
[[package]]
name = "jaraco-functools"
-version = "4.3.0"
+version = "4.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "more-itertools" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/0f/27/056e0638a86749374d6f57d0b0db39f29509cce9313cf91bdc0ac4d91084/jaraco_functools-4.4.0.tar.gz", hash = "sha256:da21933b0417b89515562656547a77b4931f98176eb173644c0d35032a33d6bb", size = 19943, upload-time = "2025-12-21T09:29:43.6Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/c4/813bb09f0985cb21e959f21f2464169eca882656849adf727ac7bb7e1767/jaraco_functools-4.4.0-py3-none-any.whl", hash = "sha256:9eec1e36f45c818d9bf307c8948eb03b2b56cd44087b3cdc989abca1f20b9176", size = 10481, upload-time = "2025-12-21T09:29:42.27Z" },
]
[[package]]
@@ -616,9 +545,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" },
]
+[[package]]
+name = "joserfc"
+version = "1.7.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cryptography" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/44/90/25cb27518750218e4f850be63d8bbb2343efaad1c01c3571aaa4b3c33bd7/joserfc-1.7.1.tar.gz", hash = "sha256:77d0b76514879c68c6f433bc5b7357a4ab72008ff1e33d8379fd11d72bd8ca81", size = 233181, upload-time = "2026-06-08T07:21:33.412Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b3/00/fa62404c3e347f946faa13aa21085205f9cc06ad17671e37f81a51662ae8/joserfc-1.7.1-py3-none-any.whl", hash = "sha256:b3e3d655612e2e1ef67b2600f2f420e12e537b020208fab1761fad647319c164", size = 70423, upload-time = "2026-06-08T07:21:32.001Z" },
+]
+
+[[package]]
+name = "jsonref"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" },
+]
+
[[package]]
name = "jsonschema"
-version = "4.25.1"
+version = "4.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
@@ -626,24 +576,23 @@ dependencies = [
{ name = "referencing" },
{ name = "rpds-py" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/74/69/f7185de793a29082a9f3c7728268ffb31cb5095131a9c139a74078e27336/jsonschema-4.25.1.tar.gz", hash = "sha256:e4a9655ce0da0c0b67a085847e00a3a51449e1157f4f75e9fb5aa545e122eb85", size = 357342, upload-time = "2025-08-18T17:03:50.038Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/bf/9c/8c95d856233c1f82500c2450b8c68576b4cf1c871db3afac5c34ff84e6fd/jsonschema-4.25.1-py3-none-any.whl", hash = "sha256:3fba0169e345c7175110351d456342c364814cfcf3b964ba4587f22915230a63", size = 90040, upload-time = "2025-08-18T17:03:48.373Z" },
+ { url = "https://files.pythonhosted.org/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" },
]
[[package]]
name = "jsonschema-path"
-version = "0.3.4"
+version = "0.4.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pathable" },
{ name = "pyyaml" },
{ name = "referencing" },
- { name = "requests" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/6e/45/41ebc679c2a4fced6a722f624c18d658dee42612b83ea24c1caf7c0eb3a8/jsonschema_path-0.3.4.tar.gz", hash = "sha256:8365356039f16cc65fddffafda5f58766e34bebab7d6d105616ab52bc4297001", size = 11159, upload-time = "2025-01-24T14:33:16.547Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5b/8a/7e6102f2b8bdc6705a9eb5294f8f6f9ccd3a8420e8e8e19671d1dd773251/jsonschema_path-0.4.5.tar.gz", hash = "sha256:c6cd7d577ae290c7defd4f4029e86fdb248ca1bd41a07557795b3c95e5144918", size = 15113, upload-time = "2026-03-03T09:56:46.87Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/cb/58/3485da8cb93d2f393bce453adeef16896751f14ba3e2024bc21dc9597646/jsonschema_path-0.3.4-py3-none-any.whl", hash = "sha256:f502191fdc2b22050f9a81c9237be9d27145b9001c55842bece5e94e382e52f8", size = 14810, upload-time = "2025-01-24T14:33:14.652Z" },
+ { url = "https://files.pythonhosted.org/packages/04/d5/4e96c44f6c1ea3d812cf5391d81a4f5abaa540abf8d04ecd7f66e0ed11df/jsonschema_path-0.4.5-py3-none-any.whl", hash = "sha256:7d77a2c3f3ec569a40efe5c5f942c44c1af2a6f96fe0866794c9ef5b8f87fd65", size = 19368, upload-time = "2026-03-03T09:56:45.39Z" },
]
[[package]]
@@ -660,7 +609,7 @@ wheels = [
[[package]]
name = "keyring"
-version = "25.6.0"
+version = "25.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata", marker = "python_full_version < '3.12'" },
@@ -671,83 +620,9 @@ dependencies = [
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
{ name = "secretstorage", marker = "sys_platform == 'linux'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/4b/674af6ef2f97d56f0ab5153bf0bfa28ccb6c3ed4d1babf4305449668807b/keyring-25.7.0.tar.gz", hash = "sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b", size = 63516, upload-time = "2025-11-16T16:26:09.482Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" },
-]
-
-[[package]]
-name = "lupa"
-version = "2.6"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a1/15/713cab5d0dfa4858f83b99b3e0329072df33dc14fc3ebbaa017e0f9755c4/lupa-2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b3dabda836317e63c5ad052826e156610f356a04b3003dfa0dbe66b5d54d671", size = 954828, upload-time = "2025-10-24T07:17:15.726Z" },
- { url = "https://files.pythonhosted.org/packages/2e/71/704740cbc6e587dd6cc8dabf2f04820ac6a671784e57cc3c29db795476db/lupa-2.6-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8726d1c123bbe9fbb974ce29825e94121824e66003038ff4532c14cc2ed0c51c", size = 1919259, upload-time = "2025-10-24T07:17:18.586Z" },
- { url = "https://files.pythonhosted.org/packages/eb/18/f248341c423c5d48837e35584c6c3eb4acab7e722b6057d7b3e28e42dae8/lupa-2.6-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:f4e159e7d814171199b246f9235ca8961f6461ea8c1165ab428afa13c9289a94", size = 984998, upload-time = "2025-10-24T07:17:20.428Z" },
- { url = "https://files.pythonhosted.org/packages/44/1e/8a4bd471e018aad76bcb9455d298c2c96d82eced20f2ae8fcec8cd800948/lupa-2.6-cp310-cp310-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:202160e80dbfddfb79316692a563d843b767e0f6787bbd1c455f9d54052efa6c", size = 1174871, upload-time = "2025-10-24T07:17:22.755Z" },
- { url = "https://files.pythonhosted.org/packages/2a/5c/3a3f23fd6a91b0986eea1ceaf82ad3f9b958fe3515a9981fb9c4eb046c8b/lupa-2.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5deede7c5b36ab64f869dae4831720428b67955b0bb186c8349cf6ea121c852b", size = 1057471, upload-time = "2025-10-24T07:17:24.908Z" },
- { url = "https://files.pythonhosted.org/packages/45/ac/01be1fed778fb0c8f46ee8cbe344e4d782f6806fac12717f08af87aa4355/lupa-2.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86f04901f920bbf7c0cac56807dc9597e42347123e6f1f3ca920f15f54188ce5", size = 2100592, upload-time = "2025-10-24T07:17:27.089Z" },
- { url = "https://files.pythonhosted.org/packages/3f/6c/1a05bb873e30830f8574e10cd0b4cdbc72e9dbad2a09e25810b5e3b1f75d/lupa-2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6deef8f851d6afb965c84849aa5b8c38856942df54597a811ce0369ced678610", size = 1081396, upload-time = "2025-10-24T07:17:29.064Z" },
- { url = "https://files.pythonhosted.org/packages/a2/c2/a19dd80d6dc98b39bbf8135b8198e38aa7ca3360b720eac68d1d7e9286b5/lupa-2.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:21f2b5549681c2a13b1170a26159d30875d367d28f0247b81ca347222c755038", size = 1192007, upload-time = "2025-10-24T07:17:31.362Z" },
- { url = "https://files.pythonhosted.org/packages/4f/43/e1b297225c827f55752e46fdbfb021c8982081b0f24490e42776ea69ae3b/lupa-2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:66eea57630eab5e6f49fdc5d7811c0a2a41f2011be4ea56a087ea76112011eb7", size = 2196661, upload-time = "2025-10-24T07:17:33.484Z" },
- { url = "https://files.pythonhosted.org/packages/2e/8f/2272d429a7fa9dc8dbd6e9c5c9073a03af6007eb22a4c78829fec6a34b80/lupa-2.6-cp310-cp310-win32.whl", hash = "sha256:60a403de8cab262a4fe813085dd77010effa6e2eb1886db2181df803140533b1", size = 1412738, upload-time = "2025-10-24T07:17:35.11Z" },
- { url = "https://files.pythonhosted.org/packages/35/2a/1708911271dd49ad87b4b373b5a4b0e0a0516d3d2af7b76355946c7ee171/lupa-2.6-cp310-cp310-win_amd64.whl", hash = "sha256:e4656a39d93dfa947cf3db56dc16c7916cb0cc8024acd3a952071263f675df64", size = 1656898, upload-time = "2025-10-24T07:17:36.949Z" },
- { url = "https://files.pythonhosted.org/packages/ca/29/1f66907c1ebf1881735afa695e646762c674f00738ebf66d795d59fc0665/lupa-2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6d988c0f9331b9f2a5a55186701a25444ab10a1432a1021ee58011499ecbbdd5", size = 962875, upload-time = "2025-10-24T07:17:39.107Z" },
- { url = "https://files.pythonhosted.org/packages/e6/67/4a748604be360eb9c1c215f6a0da921cd1a2b44b2c5951aae6fb83019d3a/lupa-2.6-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:ebe1bbf48259382c72a6fe363dea61a0fd6fe19eab95e2ae881e20f3654587bf", size = 1935390, upload-time = "2025-10-24T07:17:41.427Z" },
- { url = "https://files.pythonhosted.org/packages/ac/0c/8ef9ee933a350428b7bdb8335a37ef170ab0bb008bbf9ca8f4f4310116b6/lupa-2.6-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:a8fcee258487cf77cdd41560046843bb38c2e18989cd19671dd1e2596f798306", size = 992193, upload-time = "2025-10-24T07:17:43.231Z" },
- { url = "https://files.pythonhosted.org/packages/65/46/e6c7facebdb438db8a65ed247e56908818389c1a5abbf6a36aab14f1057d/lupa-2.6-cp311-cp311-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:561a8e3be800827884e767a694727ed8482d066e0d6edfcbf423b05e63b05535", size = 1165844, upload-time = "2025-10-24T07:17:45.437Z" },
- { url = "https://files.pythonhosted.org/packages/1c/26/9f1154c6c95f175ccbf96aa96c8f569c87f64f463b32473e839137601a8b/lupa-2.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af880a62d47991cae78b8e9905c008cbfdc4a3a9723a66310c2634fc7644578c", size = 1048069, upload-time = "2025-10-24T07:17:47.181Z" },
- { url = "https://files.pythonhosted.org/packages/68/67/2cc52ab73d6af81612b2ea24c870d3fa398443af8e2875e5befe142398b1/lupa-2.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80b22923aa4023c86c0097b235615f89d469a0c4eee0489699c494d3367c4c85", size = 2079079, upload-time = "2025-10-24T07:17:49.755Z" },
- { url = "https://files.pythonhosted.org/packages/2e/dc/f843f09bbf325f6e5ee61730cf6c3409fc78c010d968c7c78acba3019ca7/lupa-2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:153d2cc6b643f7efb9cfc0c6bb55ec784d5bac1a3660cfc5b958a7b8f38f4a75", size = 1071428, upload-time = "2025-10-24T07:17:51.991Z" },
- { url = "https://files.pythonhosted.org/packages/2e/60/37533a8d85bf004697449acb97ecdacea851acad28f2ad3803662487dd2a/lupa-2.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3fa8777e16f3ded50b72967dc17e23f5a08e4f1e2c9456aff2ebdb57f5b2869f", size = 1181756, upload-time = "2025-10-24T07:17:53.752Z" },
- { url = "https://files.pythonhosted.org/packages/e4/f2/cf29b20dbb4927b6a3d27c339ac5d73e74306ecc28c8e2c900b2794142ba/lupa-2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8dbdcbe818c02a2f56f5ab5ce2de374dab03e84b25266cfbaef237829bc09b3f", size = 2175687, upload-time = "2025-10-24T07:17:56.228Z" },
- { url = "https://files.pythonhosted.org/packages/94/7c/050e02f80c7131b63db1474bff511e63c545b5a8636a24cbef3fc4da20b6/lupa-2.6-cp311-cp311-win32.whl", hash = "sha256:defaf188fde8f7a1e5ce3a5e6d945e533b8b8d547c11e43b96c9b7fe527f56dc", size = 1412592, upload-time = "2025-10-24T07:17:59.062Z" },
- { url = "https://files.pythonhosted.org/packages/6f/9a/6f2af98aa5d771cea661f66c8eb8f53772ec1ab1dfbce24126cfcd189436/lupa-2.6-cp311-cp311-win_amd64.whl", hash = "sha256:9505ae600b5c14f3e17e70f87f88d333717f60411faca1ddc6f3e61dce85fa9e", size = 1669194, upload-time = "2025-10-24T07:18:01.647Z" },
- { url = "https://files.pythonhosted.org/packages/94/86/ce243390535c39d53ea17ccf0240815e6e457e413e40428a658ea4ee4b8d/lupa-2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47ce718817ef1cc0c40d87c3d5ae56a800d61af00fbc0fad1ca9be12df2f3b56", size = 951707, upload-time = "2025-10-24T07:18:03.884Z" },
- { url = "https://files.pythonhosted.org/packages/86/85/cedea5e6cbeb54396fdcc55f6b741696f3f036d23cfaf986d50d680446da/lupa-2.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7aba985b15b101495aa4b07112cdc08baa0c545390d560ad5cfde2e9e34f4d58", size = 1916703, upload-time = "2025-10-24T07:18:05.6Z" },
- { url = "https://files.pythonhosted.org/packages/24/be/3d6b5f9a8588c01a4d88129284c726017b2089f3a3fd3ba8bd977292fea0/lupa-2.6-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b766f62f95b2739f2248977d29b0722e589dcf4f0ccfa827ccbd29f0148bd2e5", size = 985152, upload-time = "2025-10-24T07:18:08.561Z" },
- { url = "https://files.pythonhosted.org/packages/eb/23/9f9a05beee5d5dce9deca4cb07c91c40a90541fc0a8e09db4ee670da550f/lupa-2.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:00a934c23331f94cb51760097ebfab14b005d55a6b30a2b480e3c53dd2fa290d", size = 1159599, upload-time = "2025-10-24T07:18:10.346Z" },
- { url = "https://files.pythonhosted.org/packages/40/4e/e7c0583083db9d7f1fd023800a9767d8e4391e8330d56c2373d890ac971b/lupa-2.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21de9f38bd475303e34a042b7081aabdf50bd9bafd36ce4faea2f90fd9f15c31", size = 1038686, upload-time = "2025-10-24T07:18:12.112Z" },
- { url = "https://files.pythonhosted.org/packages/1c/9f/5a4f7d959d4feba5e203ff0c31889e74d1ca3153122be4a46dca7d92bf7c/lupa-2.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf3bda96d3fc41237e964a69c23647d50d4e28421111360274d4799832c560e9", size = 2071956, upload-time = "2025-10-24T07:18:14.572Z" },
- { url = "https://files.pythonhosted.org/packages/92/34/2f4f13ca65d01169b1720176aedc4af17bc19ee834598c7292db232cb6dc/lupa-2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a76ead245da54801a81053794aa3975f213221f6542d14ec4b859ee2e7e0323", size = 1057199, upload-time = "2025-10-24T07:18:16.379Z" },
- { url = "https://files.pythonhosted.org/packages/35/2a/5f7d2eebec6993b0dcd428e0184ad71afb06a45ba13e717f6501bfed1da3/lupa-2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8dd0861741caa20886ddbda0a121d8e52fb9b5bb153d82fa9bba796962bf30e8", size = 1173693, upload-time = "2025-10-24T07:18:18.153Z" },
- { url = "https://files.pythonhosted.org/packages/e4/29/089b4d2f8e34417349af3904bb40bec40b65c8731f45e3fd8d497ca573e5/lupa-2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:239e63948b0b23023f81d9a19a395e768ed3da6a299f84e7963b8f813f6e3f9c", size = 2164394, upload-time = "2025-10-24T07:18:20.403Z" },
- { url = "https://files.pythonhosted.org/packages/f3/1b/79c17b23c921f81468a111cad843b076a17ef4b684c4a8dff32a7969c3f0/lupa-2.6-cp312-cp312-win32.whl", hash = "sha256:325894e1099499e7a6f9c351147661a2011887603c71086d36fe0f964d52d1ce", size = 1420647, upload-time = "2025-10-24T07:18:23.368Z" },
- { url = "https://files.pythonhosted.org/packages/b8/15/5121e68aad3584e26e1425a5c9a79cd898f8a152292059e128c206ee817c/lupa-2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c735a1ce8ee60edb0fe71d665f1e6b7c55c6021f1d340eb8c865952c602cd36f", size = 1688529, upload-time = "2025-10-24T07:18:25.523Z" },
- { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232, upload-time = "2025-10-24T07:18:27.878Z" },
- { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625, upload-time = "2025-10-24T07:18:29.944Z" },
- { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057, upload-time = "2025-10-24T07:18:31.553Z" },
- { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227, upload-time = "2025-10-24T07:18:33.981Z" },
- { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752, upload-time = "2025-10-24T07:18:36.32Z" },
- { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009, upload-time = "2025-10-24T07:18:38.072Z" },
- { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301, upload-time = "2025-10-24T07:18:40.165Z" },
- { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673, upload-time = "2025-10-24T07:18:42.426Z" },
- { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227, upload-time = "2025-10-24T07:18:46.112Z" },
- { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558, upload-time = "2025-10-24T07:18:48.371Z" },
- { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424, upload-time = "2025-10-24T07:18:50.976Z" },
- { url = "https://files.pythonhosted.org/packages/66/9d/d9427394e54d22a35d1139ef12e845fd700d4872a67a34db32516170b746/lupa-2.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:dcb6d0a3264873e1653bc188499f48c1fb4b41a779e315eba45256cfe7bc33c1", size = 953818, upload-time = "2025-10-24T07:18:53.378Z" },
- { url = "https://files.pythonhosted.org/packages/10/41/27bbe81953fb2f9ecfced5d9c99f85b37964cfaf6aa8453bb11283983721/lupa-2.6-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:a37e01f2128f8c36106726cb9d360bac087d58c54b4522b033cc5691c584db18", size = 1915850, upload-time = "2025-10-24T07:18:55.259Z" },
- { url = "https://files.pythonhosted.org/packages/a3/98/f9ff60db84a75ba8725506bbf448fb085bc77868a021998ed2a66d920568/lupa-2.6-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:458bd7e9ff3c150b245b0fcfbb9bd2593d1152ea7f0a7b91c1d185846da033fe", size = 982344, upload-time = "2025-10-24T07:18:57.05Z" },
- { url = "https://files.pythonhosted.org/packages/41/f7/f39e0f1c055c3b887d86b404aaf0ca197b5edfd235a8b81b45b25bac7fc3/lupa-2.6-cp314-cp314-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:052ee82cac5206a02df77119c325339acbc09f5ce66967f66a2e12a0f3211cad", size = 1156543, upload-time = "2025-10-24T07:18:59.251Z" },
- { url = "https://files.pythonhosted.org/packages/9e/9c/59e6cffa0d672d662ae17bd7ac8ecd2c89c9449dee499e3eb13ca9cd10d9/lupa-2.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96594eca3c87dd07938009e95e591e43d554c1dbd0385be03c100367141db5a8", size = 1047974, upload-time = "2025-10-24T07:19:01.449Z" },
- { url = "https://files.pythonhosted.org/packages/23/c6/a04e9cef7c052717fcb28fb63b3824802488f688391895b618e39be0f684/lupa-2.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8faddd9d198688c8884091173a088a8e920ecc96cda2ffed576a23574c4b3f6", size = 2073458, upload-time = "2025-10-24T07:19:03.369Z" },
- { url = "https://files.pythonhosted.org/packages/e6/10/824173d10f38b51fc77785228f01411b6ca28826ce27404c7c912e0e442c/lupa-2.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:daebb3a6b58095c917e76ba727ab37b27477fb926957c825205fbda431552134", size = 1067683, upload-time = "2025-10-24T07:19:06.2Z" },
- { url = "https://files.pythonhosted.org/packages/b6/dc/9692fbcf3c924d9c4ece2d8d2f724451ac2e09af0bd2a782db1cef34e799/lupa-2.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f3154e68972befe0f81564e37d8142b5d5d79931a18309226a04ec92487d4ea3", size = 1171892, upload-time = "2025-10-24T07:19:08.544Z" },
- { url = "https://files.pythonhosted.org/packages/84/ff/e318b628d4643c278c96ab3ddea07fc36b075a57383c837f5b11e537ba9d/lupa-2.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e4dadf77b9fedc0bfa53417cc28dc2278a26d4cbd95c29f8927ad4d8fe0a7ef9", size = 2166641, upload-time = "2025-10-24T07:19:10.485Z" },
- { url = "https://files.pythonhosted.org/packages/12/f7/a6f9ec2806cf2d50826980cdb4b3cffc7691dc6f95e13cc728846d5cb793/lupa-2.6-cp314-cp314-win32.whl", hash = "sha256:cb34169c6fa3bab3e8ac58ca21b8a7102f6a94b6a5d08d3636312f3f02fafd8f", size = 1456857, upload-time = "2025-10-24T07:19:37.989Z" },
- { url = "https://files.pythonhosted.org/packages/c5/de/df71896f25bdc18360fdfa3b802cd7d57d7fede41a0e9724a4625b412c85/lupa-2.6-cp314-cp314-win_amd64.whl", hash = "sha256:b74f944fe46c421e25d0f8692aef1e842192f6f7f68034201382ac440ef9ea67", size = 1731191, upload-time = "2025-10-24T07:19:40.281Z" },
- { url = "https://files.pythonhosted.org/packages/47/3c/a1f23b01c54669465f5f4c4083107d496fbe6fb45998771420e9aadcf145/lupa-2.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0e21b716408a21ab65723f8841cf7f2f37a844b7a965eeabb785e27fca4099cf", size = 999343, upload-time = "2025-10-24T07:19:12.519Z" },
- { url = "https://files.pythonhosted.org/packages/c5/6d/501994291cb640bfa2ccf7f554be4e6914afa21c4026bd01bff9ca8aac57/lupa-2.6-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:589db872a141bfff828340079bbdf3e9a31f2689f4ca0d88f97d9e8c2eae6142", size = 2000730, upload-time = "2025-10-24T07:19:14.869Z" },
- { url = "https://files.pythonhosted.org/packages/53/a5/457ffb4f3f20469956c2d4c4842a7675e884efc895b2f23d126d23e126cc/lupa-2.6-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:cd852a91a4a9d4dcbb9a58100f820a75a425703ec3e3f049055f60b8533b7953", size = 1021553, upload-time = "2025-10-24T07:19:17.123Z" },
- { url = "https://files.pythonhosted.org/packages/51/6b/36bb5a5d0960f2a5c7c700e0819abb76fd9bf9c1d8a66e5106416d6e9b14/lupa-2.6-cp314-cp314t-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:0334753be028358922415ca97a64a3048e4ed155413fc4eaf87dd0a7e2752983", size = 1133275, upload-time = "2025-10-24T07:19:20.51Z" },
- { url = "https://files.pythonhosted.org/packages/19/86/202ff4429f663013f37d2229f6176ca9f83678a50257d70f61a0a97281bf/lupa-2.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:661d895cd38c87658a34780fac54a690ec036ead743e41b74c3fb81a9e65a6aa", size = 1038441, upload-time = "2025-10-24T07:19:22.509Z" },
- { url = "https://files.pythonhosted.org/packages/a7/42/d8125f8e420714e5b52e9c08d88b5329dfb02dcca731b4f21faaee6cc5b5/lupa-2.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aa58454ccc13878cc177c62529a2056be734da16369e451987ff92784994ca7", size = 2058324, upload-time = "2025-10-24T07:19:24.979Z" },
- { url = "https://files.pythonhosted.org/packages/2b/2c/47bf8b84059876e877a339717ddb595a4a7b0e8740bacae78ba527562e1c/lupa-2.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1425017264e470c98022bba8cff5bd46d054a827f5df6b80274f9cc71dafd24f", size = 1060250, upload-time = "2025-10-24T07:19:27.262Z" },
- { url = "https://files.pythonhosted.org/packages/c2/06/d88add2b6406ca1bdec99d11a429222837ca6d03bea42ca75afa169a78cb/lupa-2.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:224af0532d216e3105f0a127410f12320f7c5f1aa0300bdf9646b8d9afb0048c", size = 1151126, upload-time = "2025-10-24T07:19:29.522Z" },
- { url = "https://files.pythonhosted.org/packages/b4/a0/89e6a024c3b4485b89ef86881c9d55e097e7cb0bdb74efb746f2fa6a9a76/lupa-2.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9abb98d5a8fd27c8285302e82199f0e56e463066f88f619d6594a450bf269d80", size = 2153693, upload-time = "2025-10-24T07:19:31.379Z" },
- { url = "https://files.pythonhosted.org/packages/b6/36/a0f007dc58fc1bbf51fb85dcc82fcb1f21b8c4261361de7dab0e3d8521ef/lupa-2.6-cp314-cp314t-win32.whl", hash = "sha256:1849efeba7a8f6fb8aa2c13790bee988fd242ae404bd459509640eeea3d1e291", size = 1590104, upload-time = "2025-10-24T07:19:33.514Z" },
- { url = "https://files.pythonhosted.org/packages/7d/5e/db903ce9cf82c48d6b91bf6d63ae4c8d0d17958939a4e04ba6b9f38b8643/lupa-2.6-cp314-cp314t-win_amd64.whl", hash = "sha256:fc1498d1a4fc028bc521c26d0fad4ca00ed63b952e32fb95949bda76a04bad52", size = 1913818, upload-time = "2025-10-24T07:19:36.039Z" },
+ { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" },
]
[[package]]
@@ -764,7 +639,7 @@ wheels = [
[[package]]
name = "mcp"
-version = "1.25.0"
+version = "1.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -782,9 +657,9 @@ dependencies = [
{ name = "typing-inspection" },
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d5/2d/649d80a0ecf6a1f82632ca44bec21c0461a9d9fc8934d38cb5b319f2db5e/mcp-1.25.0.tar.gz", hash = "sha256:56310361ebf0364e2d438e5b45f7668cbb124e158bb358333cd06e49e83a6802", size = 605387, upload-time = "2025-12-19T10:19:56.985Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e2/fc/6dc7659c2ae5ddf280477011f4213a74f806862856b796ef08f028e664bf/mcp-1.25.0-py3-none-any.whl", hash = "sha256:b37c38144a666add0862614cc79ec276e97d72aa8ca26d622818d4e278b9721a", size = 233076, upload-time = "2025-12-19T10:19:55.416Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" },
]
[[package]]
@@ -819,107 +694,42 @@ wheels = [
[[package]]
name = "opentelemetry-api"
-version = "1.39.1"
+version = "1.40.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "importlib-metadata" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/97/b9/3161be15bb8e3ad01be8be5a968a9237c3027c5be504362ff800fca3e442/opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c", size = 65767, upload-time = "2025-12-11T13:32:39.182Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/2c/1d/4049a9e8698361cc1a1aa03a6c59e4fa4c71e0c0f94a30f988a6876a2ae6/opentelemetry_api-1.40.0.tar.gz", hash = "sha256:159be641c0b04d11e9ecd576906462773eb97ae1b657730f0ecf64d32071569f", size = 70851, upload-time = "2026-03-04T14:17:21.555Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/cf/df/d3f1ddf4bb4cb50ed9b1139cc7b1c54c34a1e7ce8fd1b9a37c0d1551a6bd/opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950", size = 66356, upload-time = "2025-12-11T13:32:17.304Z" },
-]
-
-[[package]]
-name = "opentelemetry-exporter-prometheus"
-version = "0.60b1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-api" },
- { name = "opentelemetry-sdk" },
- { name = "prometheus-client" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/14/39/7dafa6fff210737267bed35a8855b6ac7399b9e582b8cf1f25f842517012/opentelemetry_exporter_prometheus-0.60b1.tar.gz", hash = "sha256:a4011b46906323f71724649d301b4dc188aaa068852e814f4df38cc76eac616b", size = 14976, upload-time = "2025-12-11T13:32:42.944Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9b/0d/4be6bf5477a3eb3d917d2f17d3c0b6720cd6cb97898444a61d43cc983f5c/opentelemetry_exporter_prometheus-0.60b1-py3-none-any.whl", hash = "sha256:49f59178de4f4590e3cef0b8b95cf6e071aae70e1f060566df5546fad773b8fd", size = 13019, upload-time = "2025-12-11T13:32:23.974Z" },
-]
-
-[[package]]
-name = "opentelemetry-instrumentation"
-version = "0.60b1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-api" },
- { name = "opentelemetry-semantic-conventions" },
- { name = "packaging" },
- { name = "wrapt" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/41/0f/7e6b713ac117c1f5e4e3300748af699b9902a2e5e34c9cf443dde25a01fa/opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a", size = 31706, upload-time = "2025-12-11T13:36:42.515Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/77/d2/6788e83c5c86a2690101681aeef27eeb2a6bf22df52d3f263a22cee20915/opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d", size = 33096, upload-time = "2025-12-11T13:35:33.067Z" },
-]
-
-[[package]]
-name = "opentelemetry-sdk"
-version = "1.39.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-api" },
- { name = "opentelemetry-semantic-conventions" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/eb/fb/c76080c9ba07e1e8235d24cdcc4d125ef7aa3edf23eb4e497c2e50889adc/opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6", size = 171460, upload-time = "2025-12-11T13:32:49.369Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7c/98/e91cf858f203d86f4eccdf763dcf01cf03f1dae80c3750f7e635bfa206b6/opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c", size = 132565, upload-time = "2025-12-11T13:32:35.069Z" },
-]
-
-[[package]]
-name = "opentelemetry-semantic-conventions"
-version = "0.60b1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "opentelemetry-api" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/91/df/553f93ed38bf22f4b999d9be9c185adb558982214f33eae539d3b5cd0858/opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953", size = 137935, upload-time = "2025-12-11T13:32:50.487Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/7a/5e/5958555e09635d09b75de3c4f8b9cae7335ca545d77392ffe7331534c402/opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb", size = 219982, upload-time = "2025-12-11T13:32:36.955Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/bf/93795954016c522008da367da292adceed71cca6ee1717e1d64c83089099/opentelemetry_api-1.40.0-py3-none-any.whl", hash = "sha256:82dd69331ae74b06f6a874704be0cfaa49a1650e1537d4a813b86ecef7d0ecf9", size = 68676, upload-time = "2026-03-04T14:17:01.24Z" },
]
[[package]]
name = "packaging"
-version = "25.0"
+version = "26.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "pathable"
-version = "0.4.4"
+version = "0.5.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/67/93/8f2c2075b180c12c1e9f6a09d1a985bc2036906b13dff1d8917e395f2048/pathable-0.4.4.tar.gz", hash = "sha256:6905a3cd17804edfac7875b5f6c9142a218c7caef78693c2dbbbfbac186d88b2", size = 8124, upload-time = "2025-01-10T18:43:13.247Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/55/b748445cb4ea6b125626f15379be7c96d1035d4fa3e8fee362fa92298abf/pathable-0.5.0.tar.gz", hash = "sha256:d81938348a1cacb525e7c75166270644782c0fb9c8cecc16be033e71427e0ef1", size = 16655, upload-time = "2026-02-20T08:47:00.748Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/7d/eb/b6260b31b1a96386c0a880edebe26f89669098acea8e0318bff6adb378fd/pathable-0.4.4-py3-none-any.whl", hash = "sha256:5ae9e94793b6ef5a4cbe0a7ce9dbbefc1eec38df253763fd0aeeacf2762dbbc2", size = 9592, upload-time = "2025-01-10T18:43:11.88Z" },
-]
-
-[[package]]
-name = "pathvalidate"
-version = "3.3.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fa/2a/52a8da6fe965dea6192eb716b357558e103aea0a1e9a8352ad575a8406ca/pathvalidate-3.3.1.tar.gz", hash = "sha256:b18c07212bfead624345bb8e1d6141cdcf15a39736994ea0b94035ad2b1ba177", size = 63262, upload-time = "2025-06-15T09:07:20.736Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9a/70/875f4a23bfc4731703a5835487d0d2fb999031bd415e7d17c0ae615c18b7/pathvalidate-3.3.1-py3-none-any.whl", hash = "sha256:5263baab691f8e1af96092fa5137ee17df5bdfbd6cff1fcac4d6ef4bc2e1735f", size = 24305, upload-time = "2025-06-15T09:07:19.117Z" },
+ { url = "https://files.pythonhosted.org/packages/52/96/5a770e5c461462575474468e5af931cff9de036e7c2b4fea23c1c58d2cbe/pathable-0.5.0-py3-none-any.whl", hash = "sha256:646e3d09491a6351a0c82632a09c02cdf70a252e73196b36d8a15ba0a114f0a6", size = 16867, upload-time = "2026-02-20T08:46:59.536Z" },
]
[[package]]
name = "platformdirs"
-version = "4.5.0"
+version = "4.9.4"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
+ { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
]
[[package]]
@@ -931,32 +741,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
]
-[[package]]
-name = "prometheus-client"
-version = "0.24.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" },
-]
-
[[package]]
name = "py-key-value-aio"
-version = "0.3.0"
+version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "beartype" },
- { name = "py-key-value-shared" },
+ { name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/93/ce/3136b771dddf5ac905cc193b461eb67967cf3979688c6696e1f2cdcde7ea/py_key_value_aio-0.3.0.tar.gz", hash = "sha256:858e852fcf6d696d231266da66042d3355a7f9871650415feef9fca7a6cd4155", size = 50801, upload-time = "2025-11-17T16:50:04.711Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/99/10/72f6f213b8f0bce36eff21fda0a13271834e9eeff7f9609b01afdc253c79/py_key_value_aio-0.3.0-py3-none-any.whl", hash = "sha256:1c781915766078bfd608daa769fefb97e65d1d73746a3dfb640460e322071b64", size = 96342, upload-time = "2025-11-17T16:50:03.801Z" },
+ { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" },
]
[package.optional-dependencies]
-disk = [
- { name = "diskcache" },
- { name = "pathvalidate" },
+filetree = [
+ { name = "aiofile" },
+ { name = "anyio" },
]
keyring = [
{ name = "keyring" },
@@ -964,35 +765,19 @@ keyring = [
memory = [
{ name = "cachetools" },
]
-redis = [
- { name = "redis" },
-]
-
-[[package]]
-name = "py-key-value-shared"
-version = "0.3.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "beartype" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/7b/e4/1971dfc4620a3a15b4579fe99e024f5edd6e0967a71154771a059daff4db/py_key_value_shared-0.3.0.tar.gz", hash = "sha256:8fdd786cf96c3e900102945f92aa1473138ebe960ef49da1c833790160c28a4b", size = 11666, upload-time = "2025-11-17T16:50:06.849Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/51/e4/b8b0a03ece72f47dce2307d36e1c34725b7223d209fc679315ffe6a4e2c3/py_key_value_shared-0.3.0-py3-none-any.whl", hash = "sha256:5b0efba7ebca08bb158b1e93afc2f07d30b8f40c2fc12ce24a4c0d84f42f9298", size = 19560, upload-time = "2025-11-17T16:50:05.954Z" },
-]
[[package]]
name = "pycparser"
-version = "2.23"
+version = "3.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]]
name = "pydantic"
-version = "2.12.4"
+version = "2.12.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
@@ -1000,9 +785,9 @@ dependencies = [
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
]
[package.optional-dependencies]
@@ -1130,58 +915,37 @@ wheels = [
[[package]]
name = "pydantic-settings"
-version = "2.11.0"
+version = "2.14.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "python-dotenv" },
{ name = "typing-inspection" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/20/c5/dbbc27b814c71676593d1c3f718e6cd7d4f00652cefa24b75f7aa3efb25e/pydantic_settings-2.11.0.tar.gz", hash = "sha256:d0e87a1c7d33593beb7194adb8470fc426e95ba02af83a0f23474a04c9a08180", size = 188394, upload-time = "2025-09-24T14:19:11.764Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/83/d6/887a1ff844e64aa823fb4905978d882a633cfe295c32eacad582b78a7d8b/pydantic_settings-2.11.0-py3-none-any.whl", hash = "sha256:fe2cea3413b9530d10f3a5875adffb17ada5c1e1bab0b2885546d7310415207c", size = 48608, upload-time = "2025-09-24T14:19:10.015Z" },
-]
-
-[[package]]
-name = "pydocket"
-version = "0.16.6"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "cloudpickle" },
- { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
- { name = "fakeredis", extra = ["lua"] },
- { name = "opentelemetry-api" },
- { name = "opentelemetry-exporter-prometheus" },
- { name = "opentelemetry-instrumentation" },
- { name = "prometheus-client" },
- { name = "py-key-value-aio", extra = ["memory", "redis"] },
- { name = "python-json-logger" },
- { name = "redis" },
- { name = "rich" },
- { name = "typer" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/72/00/26befe5f58df7cd1aeda4a8d10bc7d1908ffd86b80fd995e57a2a7b3f7bd/pydocket-0.16.6.tar.gz", hash = "sha256:b96c96ad7692827214ed4ff25fcf941ec38371314db5dcc1ae792b3e9d3a0294", size = 299054, upload-time = "2026-01-09T22:09:15.405Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/0a/3f/7483e5a6dc6326b6e0c640619b5c5bd1d6e3c20e54d58f5fb86267cef00e/pydocket-0.16.6-py3-none-any.whl", hash = "sha256:683d21e2e846aa5106274e7d59210331b242d7fb0dce5b08d3b82065663ed183", size = 67697, upload-time = "2026-01-09T22:09:13.436Z" },
+ { url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" },
]
[[package]]
name = "pygments"
-version = "2.19.2"
+version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
name = "pyjwt"
-version = "2.10.1"
+version = "2.13.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" },
]
[package.optional-dependencies]
@@ -1200,7 +964,7 @@ wheels = [
[[package]]
name = "pytest"
-version = "8.4.2"
+version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -1211,50 +975,41 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/a3/5c/00a0e072241553e1a7496d638deababa67c5058571567b92a7eaa258397c/pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01", size = 1519618, upload-time = "2025-09-04T14:34:22.711Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
name = "pytest-asyncio"
-version = "1.2.0"
+version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
{ name = "pytest" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/04/93/2fa34714b7a4ae72f2f8dad66ba17dd9a2c793220719e736dda28b7aec27/pytest_asyncio-1.2.0-py3-none-any.whl", hash = "sha256:8e17ae5e46d8e7efe51ab6494dd2010f4ca8dae51652aa3c8d55acf50bfb2e99", size = 15095, upload-time = "2025-09-12T07:33:52.639Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
]
[[package]]
name = "python-dotenv"
-version = "1.2.1"
+version = "1.2.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
-]
-
-[[package]]
-name = "python-json-logger"
-version = "4.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/29/bf/eca6a3d43db1dae7070f70e160ab20b807627ba953663ba07928cdd3dc58/python_json_logger-4.0.0.tar.gz", hash = "sha256:f58e68eb46e1faed27e0f574a55a0455eecd7b8a5b88b85a784519ba3cff047f", size = 17683, upload-time = "2025-10-06T04:15:18.984Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/51/e5/fecf13f06e5e5f67e8837d777d1bc43fac0ed2b77a676804df5c34744727/python_json_logger-4.0.0-py3-none-any.whl", hash = "sha256:af09c9daf6a813aa4cc7180395f50f2a9e5fa056034c9953aec92e381c5ba1e2", size = 15548, upload-time = "2025-10-06T04:15:17.553Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
]
[[package]]
name = "python-multipart"
-version = "0.0.22"
+version = "0.0.32"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" },
]
[[package]]
@@ -1352,58 +1107,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
-[[package]]
-name = "redis"
-version = "7.1.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "async-timeout", marker = "python_full_version < '3.11.3'" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" },
-]
-
[[package]]
name = "referencing"
-version = "0.36.2"
+version = "0.37.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "rpds-py" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" },
-]
-
-[[package]]
-name = "requests"
-version = "2.32.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "certifi" },
- { name = "charset-normalizer" },
- { name = "idna" },
- { name = "urllib3" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
]
[[package]]
name = "rich"
-version = "14.2.0"
+version = "14.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown-it-py" },
{ name = "pygments" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" },
+ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" },
]
[[package]]
@@ -1421,189 +1149,163 @@ wheels = [
[[package]]
name = "rpds-py"
-version = "0.28.0"
+version = "0.30.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/48/dc/95f074d43452b3ef5d06276696ece4b3b5d696e7c9ad7173c54b1390cd70/rpds_py-0.28.0.tar.gz", hash = "sha256:abd4df20485a0983e2ca334a216249b6186d6e3c1627e106651943dbdb791aea", size = 27419, upload-time = "2025-10-22T22:24:29.327Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/af/3f2f423103f1113b36230496629986e0ef7e199d2aa8392452b484b38ced/rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84", size = 69469, upload-time = "2025-11-30T20:24:38.837Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/82/f8/13bb772dc7cbf2c3c5b816febc34fa0cb2c64a08e0569869585684ce6631/rpds_py-0.28.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7b6013db815417eeb56b2d9d7324e64fcd4fa289caeee6e7a78b2e11fc9b438a", size = 362820, upload-time = "2025-10-22T22:21:15.074Z" },
- { url = "https://files.pythonhosted.org/packages/84/91/6acce964aab32469c3dbe792cb041a752d64739c534e9c493c701ef0c032/rpds_py-0.28.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a4c6b05c685c0c03f80dabaeb73e74218c49deea965ca63f76a752807397207", size = 348499, upload-time = "2025-10-22T22:21:17.658Z" },
- { url = "https://files.pythonhosted.org/packages/f1/93/c05bb1f4f5e0234db7c4917cb8dd5e2e0a9a7b26dc74b1b7bee3c9cfd477/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4794c6c3fbe8f9ac87699b131a1f26e7b4abcf6d828da46a3a52648c7930eba", size = 379356, upload-time = "2025-10-22T22:21:19.847Z" },
- { url = "https://files.pythonhosted.org/packages/5c/37/e292da436f0773e319753c567263427cdf6c645d30b44f09463ff8216cda/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e8456b6ee5527112ff2354dd9087b030e3429e43a74f480d4a5ca79d269fd85", size = 390151, upload-time = "2025-10-22T22:21:21.569Z" },
- { url = "https://files.pythonhosted.org/packages/76/87/a4e3267131616e8faf10486dc00eaedf09bd61c87f01e5ef98e782ee06c9/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:beb880a9ca0a117415f241f66d56025c02037f7c4efc6fe59b5b8454f1eaa50d", size = 524831, upload-time = "2025-10-22T22:21:23.394Z" },
- { url = "https://files.pythonhosted.org/packages/e1/c8/4a4ca76f0befae9515da3fad11038f0fce44f6bb60b21fe9d9364dd51fb0/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6897bebb118c44b38c9cb62a178e09f1593c949391b9a1a6fe777ccab5934ee7", size = 404687, upload-time = "2025-10-22T22:21:25.201Z" },
- { url = "https://files.pythonhosted.org/packages/6a/65/118afe854424456beafbbebc6b34dcf6d72eae3a08b4632bc4220f8240d9/rpds_py-0.28.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1b553dd06e875249fd43efd727785efb57a53180e0fde321468222eabbeaafa", size = 382683, upload-time = "2025-10-22T22:21:26.536Z" },
- { url = "https://files.pythonhosted.org/packages/f7/bc/0625064041fb3a0c77ecc8878c0e8341b0ae27ad0f00cf8f2b57337a1e63/rpds_py-0.28.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:f0b2044fdddeea5b05df832e50d2a06fe61023acb44d76978e1b060206a8a476", size = 398927, upload-time = "2025-10-22T22:21:27.864Z" },
- { url = "https://files.pythonhosted.org/packages/5d/1a/fed7cf2f1ee8a5e4778f2054153f2cfcf517748875e2f5b21cf8907cd77d/rpds_py-0.28.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05cf1e74900e8da73fa08cc76c74a03345e5a3e37691d07cfe2092d7d8e27b04", size = 411590, upload-time = "2025-10-22T22:21:29.474Z" },
- { url = "https://files.pythonhosted.org/packages/c1/64/a8e0f67fa374a6c472dbb0afdaf1ef744724f165abb6899f20e2f1563137/rpds_py-0.28.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:efd489fec7c311dae25e94fe7eeda4b3d06be71c68f2cf2e8ef990ffcd2cd7e8", size = 559843, upload-time = "2025-10-22T22:21:30.917Z" },
- { url = "https://files.pythonhosted.org/packages/a9/ea/e10353f6d7c105be09b8135b72787a65919971ae0330ad97d87e4e199880/rpds_py-0.28.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ada7754a10faacd4f26067e62de52d6af93b6d9542f0df73c57b9771eb3ba9c4", size = 584188, upload-time = "2025-10-22T22:21:32.827Z" },
- { url = "https://files.pythonhosted.org/packages/18/b0/a19743e0763caf0c89f6fc6ba6fbd9a353b24ffb4256a492420c5517da5a/rpds_py-0.28.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c2a34fd26588949e1e7977cfcbb17a9a42c948c100cab890c6d8d823f0586457", size = 550052, upload-time = "2025-10-22T22:21:34.702Z" },
- { url = "https://files.pythonhosted.org/packages/de/bc/ec2c004f6c7d6ab1e25dae875cdb1aee087c3ebed5b73712ed3000e3851a/rpds_py-0.28.0-cp310-cp310-win32.whl", hash = "sha256:f9174471d6920cbc5e82a7822de8dfd4dcea86eb828b04fc8c6519a77b0ee51e", size = 215110, upload-time = "2025-10-22T22:21:36.645Z" },
- { url = "https://files.pythonhosted.org/packages/6c/de/4ce8abf59674e17187023933547d2018363e8fc76ada4f1d4d22871ccb6e/rpds_py-0.28.0-cp310-cp310-win_amd64.whl", hash = "sha256:6e32dd207e2c4f8475257a3540ab8a93eff997abfa0a3fdb287cae0d6cd874b8", size = 223850, upload-time = "2025-10-22T22:21:38.006Z" },
- { url = "https://files.pythonhosted.org/packages/a6/34/058d0db5471c6be7bef82487ad5021ff8d1d1d27794be8730aad938649cf/rpds_py-0.28.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:03065002fd2e287725d95fbc69688e0c6daf6c6314ba38bdbaa3895418e09296", size = 362344, upload-time = "2025-10-22T22:21:39.713Z" },
- { url = "https://files.pythonhosted.org/packages/5d/67/9503f0ec8c055a0782880f300c50a2b8e5e72eb1f94dfc2053da527444dd/rpds_py-0.28.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:28ea02215f262b6d078daec0b45344c89e161eab9526b0d898221d96fdda5f27", size = 348440, upload-time = "2025-10-22T22:21:41.056Z" },
- { url = "https://files.pythonhosted.org/packages/68/2e/94223ee9b32332a41d75b6f94b37b4ce3e93878a556fc5f152cbd856a81f/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25dbade8fbf30bcc551cb352376c0ad64b067e4fc56f90e22ba70c3ce205988c", size = 379068, upload-time = "2025-10-22T22:21:42.593Z" },
- { url = "https://files.pythonhosted.org/packages/b4/25/54fd48f9f680cfc44e6a7f39a5fadf1d4a4a1fd0848076af4a43e79f998c/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3c03002f54cc855860bfdc3442928ffdca9081e73b5b382ed0b9e8efe6e5e205", size = 390518, upload-time = "2025-10-22T22:21:43.998Z" },
- { url = "https://files.pythonhosted.org/packages/1b/85/ac258c9c27f2ccb1bd5d0697e53a82ebcf8088e3186d5d2bf8498ee7ed44/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9699fa7990368b22032baf2b2dce1f634388e4ffc03dfefaaac79f4695edc95", size = 525319, upload-time = "2025-10-22T22:21:45.645Z" },
- { url = "https://files.pythonhosted.org/packages/40/cb/c6734774789566d46775f193964b76627cd5f42ecf246d257ce84d1912ed/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9b06fe1a75e05e0713f06ea0c89ecb6452210fd60e2f1b6ddc1067b990e08d9", size = 404896, upload-time = "2025-10-22T22:21:47.544Z" },
- { url = "https://files.pythonhosted.org/packages/1f/53/14e37ce83202c632c89b0691185dca9532288ff9d390eacae3d2ff771bae/rpds_py-0.28.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9f83e7b326a3f9ec3ef84cda98fb0a74c7159f33e692032233046e7fd15da2", size = 382862, upload-time = "2025-10-22T22:21:49.176Z" },
- { url = "https://files.pythonhosted.org/packages/6a/83/f3642483ca971a54d60caa4449f9d6d4dbb56a53e0072d0deff51b38af74/rpds_py-0.28.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:0d3259ea9ad8743a75a43eb7819324cdab393263c91be86e2d1901ee65c314e0", size = 398848, upload-time = "2025-10-22T22:21:51.024Z" },
- { url = "https://files.pythonhosted.org/packages/44/09/2d9c8b2f88e399b4cfe86efdf2935feaf0394e4f14ab30c6c5945d60af7d/rpds_py-0.28.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a7548b345f66f6695943b4ef6afe33ccd3f1b638bd9afd0f730dd255c249c9e", size = 412030, upload-time = "2025-10-22T22:21:52.665Z" },
- { url = "https://files.pythonhosted.org/packages/dd/f5/e1cec473d4bde6df1fd3738be8e82d64dd0600868e76e92dfeaebbc2d18f/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9a40040aa388b037eb39416710fbcce9443498d2eaab0b9b45ae988b53f5c67", size = 559700, upload-time = "2025-10-22T22:21:54.123Z" },
- { url = "https://files.pythonhosted.org/packages/8d/be/73bb241c1649edbf14e98e9e78899c2c5e52bbe47cb64811f44d2cc11808/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f60c7ea34e78c199acd0d3cda37a99be2c861dd2b8cf67399784f70c9f8e57d", size = 584581, upload-time = "2025-10-22T22:21:56.102Z" },
- { url = "https://files.pythonhosted.org/packages/9c/9c/ffc6e9218cd1eb5c2c7dbd276c87cd10e8c2232c456b554169eb363381df/rpds_py-0.28.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1571ae4292649100d743b26d5f9c63503bb1fedf538a8f29a98dce2d5ba6b4e6", size = 549981, upload-time = "2025-10-22T22:21:58.253Z" },
- { url = "https://files.pythonhosted.org/packages/5f/50/da8b6d33803a94df0149345ee33e5d91ed4d25fc6517de6a25587eae4133/rpds_py-0.28.0-cp311-cp311-win32.whl", hash = "sha256:5cfa9af45e7c1140af7321fa0bef25b386ee9faa8928c80dc3a5360971a29e8c", size = 214729, upload-time = "2025-10-22T22:21:59.625Z" },
- { url = "https://files.pythonhosted.org/packages/12/fd/b0f48c4c320ee24c8c20df8b44acffb7353991ddf688af01eef5f93d7018/rpds_py-0.28.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd8d86b5d29d1b74100982424ba53e56033dc47720a6de9ba0259cf81d7cecaa", size = 223977, upload-time = "2025-10-22T22:22:01.092Z" },
- { url = "https://files.pythonhosted.org/packages/b4/21/c8e77a2ac66e2ec4e21f18a04b4e9a0417ecf8e61b5eaeaa9360a91713b4/rpds_py-0.28.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e27d3a5709cc2b3e013bf93679a849213c79ae0573f9b894b284b55e729e120", size = 217326, upload-time = "2025-10-22T22:22:02.944Z" },
- { url = "https://files.pythonhosted.org/packages/b8/5c/6c3936495003875fe7b14f90ea812841a08fca50ab26bd840e924097d9c8/rpds_py-0.28.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6b4f28583a4f247ff60cd7bdda83db8c3f5b05a7a82ff20dd4b078571747708f", size = 366439, upload-time = "2025-10-22T22:22:04.525Z" },
- { url = "https://files.pythonhosted.org/packages/56/f9/a0f1ca194c50aa29895b442771f036a25b6c41a35e4f35b1a0ea713bedae/rpds_py-0.28.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d678e91b610c29c4b3d52a2c148b641df2b4676ffe47c59f6388d58b99cdc424", size = 348170, upload-time = "2025-10-22T22:22:06.397Z" },
- { url = "https://files.pythonhosted.org/packages/18/ea/42d243d3a586beb72c77fa5def0487daf827210069a95f36328e869599ea/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e819e0e37a44a78e1383bf1970076e2ccc4dc8c2bbaa2f9bd1dc987e9afff628", size = 378838, upload-time = "2025-10-22T22:22:07.932Z" },
- { url = "https://files.pythonhosted.org/packages/e7/78/3de32e18a94791af8f33601402d9d4f39613136398658412a4e0b3047327/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5ee514e0f0523db5d3fb171f397c54875dbbd69760a414dccf9d4d7ad628b5bd", size = 393299, upload-time = "2025-10-22T22:22:09.435Z" },
- { url = "https://files.pythonhosted.org/packages/13/7e/4bdb435afb18acea2eb8a25ad56b956f28de7c59f8a1d32827effa0d4514/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3fa06d27fdcee47f07a39e02862da0100cb4982508f5ead53ec533cd5fe55e", size = 518000, upload-time = "2025-10-22T22:22:11.326Z" },
- { url = "https://files.pythonhosted.org/packages/31/d0/5f52a656875cdc60498ab035a7a0ac8f399890cc1ee73ebd567bac4e39ae/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46959ef2e64f9e4a41fc89aa20dbca2b85531f9a72c21099a3360f35d10b0d5a", size = 408746, upload-time = "2025-10-22T22:22:13.143Z" },
- { url = "https://files.pythonhosted.org/packages/3e/cd/49ce51767b879cde77e7ad9fae164ea15dce3616fe591d9ea1df51152706/rpds_py-0.28.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8455933b4bcd6e83fde3fefc987a023389c4b13f9a58c8d23e4b3f6d13f78c84", size = 386379, upload-time = "2025-10-22T22:22:14.602Z" },
- { url = "https://files.pythonhosted.org/packages/6a/99/e4e1e1ee93a98f72fc450e36c0e4d99c35370220e815288e3ecd2ec36a2a/rpds_py-0.28.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:ad50614a02c8c2962feebe6012b52f9802deec4263946cddea37aaf28dd25a66", size = 401280, upload-time = "2025-10-22T22:22:16.063Z" },
- { url = "https://files.pythonhosted.org/packages/61/35/e0c6a57488392a8b319d2200d03dad2b29c0db9996f5662c3b02d0b86c02/rpds_py-0.28.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e5deca01b271492553fdb6c7fd974659dce736a15bae5dad7ab8b93555bceb28", size = 412365, upload-time = "2025-10-22T22:22:17.504Z" },
- { url = "https://files.pythonhosted.org/packages/ff/6a/841337980ea253ec797eb084665436007a1aad0faac1ba097fb906c5f69c/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:735f8495a13159ce6a0d533f01e8674cec0c57038c920495f87dcb20b3ddb48a", size = 559573, upload-time = "2025-10-22T22:22:19.108Z" },
- { url = "https://files.pythonhosted.org/packages/e7/5e/64826ec58afd4c489731f8b00729c5f6afdb86f1df1df60bfede55d650bb/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:961ca621ff10d198bbe6ba4957decca61aa2a0c56695384c1d6b79bf61436df5", size = 583973, upload-time = "2025-10-22T22:22:20.768Z" },
- { url = "https://files.pythonhosted.org/packages/b6/ee/44d024b4843f8386a4eeaa4c171b3d31d55f7177c415545fd1a24c249b5d/rpds_py-0.28.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2374e16cc9131022e7d9a8f8d65d261d9ba55048c78f3b6e017971a4f5e6353c", size = 553800, upload-time = "2025-10-22T22:22:22.25Z" },
- { url = "https://files.pythonhosted.org/packages/7d/89/33e675dccff11a06d4d85dbb4d1865f878d5020cbb69b2c1e7b2d3f82562/rpds_py-0.28.0-cp312-cp312-win32.whl", hash = "sha256:d15431e334fba488b081d47f30f091e5d03c18527c325386091f31718952fe08", size = 216954, upload-time = "2025-10-22T22:22:24.105Z" },
- { url = "https://files.pythonhosted.org/packages/af/36/45f6ebb3210887e8ee6dbf1bc710ae8400bb417ce165aaf3024b8360d999/rpds_py-0.28.0-cp312-cp312-win_amd64.whl", hash = "sha256:a410542d61fc54710f750d3764380b53bf09e8c4edbf2f9141a82aa774a04f7c", size = 227844, upload-time = "2025-10-22T22:22:25.551Z" },
- { url = "https://files.pythonhosted.org/packages/57/91/f3fb250d7e73de71080f9a221d19bd6a1c1eb0d12a1ea26513f6c1052ad6/rpds_py-0.28.0-cp312-cp312-win_arm64.whl", hash = "sha256:1f0cfd1c69e2d14f8c892b893997fa9a60d890a0c8a603e88dca4955f26d1edd", size = 217624, upload-time = "2025-10-22T22:22:26.914Z" },
- { url = "https://files.pythonhosted.org/packages/d3/03/ce566d92611dfac0085c2f4b048cd53ed7c274a5c05974b882a908d540a2/rpds_py-0.28.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e9e184408a0297086f880556b6168fa927d677716f83d3472ea333b42171ee3b", size = 366235, upload-time = "2025-10-22T22:22:28.397Z" },
- { url = "https://files.pythonhosted.org/packages/00/34/1c61da1b25592b86fd285bd7bd8422f4c9d748a7373b46126f9ae792a004/rpds_py-0.28.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:edd267266a9b0448f33dc465a97cfc5d467594b600fe28e7fa2f36450e03053a", size = 348241, upload-time = "2025-10-22T22:22:30.171Z" },
- { url = "https://files.pythonhosted.org/packages/fc/00/ed1e28616848c61c493a067779633ebf4b569eccaacf9ccbdc0e7cba2b9d/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85beb8b3f45e4e32f6802fb6cd6b17f615ef6c6a52f265371fb916fae02814aa", size = 378079, upload-time = "2025-10-22T22:22:31.644Z" },
- { url = "https://files.pythonhosted.org/packages/11/b2/ccb30333a16a470091b6e50289adb4d3ec656fd9951ba8c5e3aaa0746a67/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2412be8d00a1b895f8ad827cc2116455196e20ed994bb704bf138fe91a42724", size = 393151, upload-time = "2025-10-22T22:22:33.453Z" },
- { url = "https://files.pythonhosted.org/packages/8c/d0/73e2217c3ee486d555cb84920597480627d8c0240ff3062005c6cc47773e/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf128350d384b777da0e68796afdcebc2e9f63f0e9f242217754e647f6d32491", size = 517520, upload-time = "2025-10-22T22:22:34.949Z" },
- { url = "https://files.pythonhosted.org/packages/c4/91/23efe81c700427d0841a4ae7ea23e305654381831e6029499fe80be8a071/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2036d09b363aa36695d1cc1a97b36865597f4478470b0697b5ee9403f4fe399", size = 408699, upload-time = "2025-10-22T22:22:36.584Z" },
- { url = "https://files.pythonhosted.org/packages/ca/ee/a324d3198da151820a326c1f988caaa4f37fc27955148a76fff7a2d787a9/rpds_py-0.28.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8e1e9be4fa6305a16be628959188e4fd5cd6f1b0e724d63c6d8b2a8adf74ea6", size = 385720, upload-time = "2025-10-22T22:22:38.014Z" },
- { url = "https://files.pythonhosted.org/packages/19/ad/e68120dc05af8b7cab4a789fccd8cdcf0fe7e6581461038cc5c164cd97d2/rpds_py-0.28.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0a403460c9dd91a7f23fc3188de6d8977f1d9603a351d5db6cf20aaea95b538d", size = 401096, upload-time = "2025-10-22T22:22:39.869Z" },
- { url = "https://files.pythonhosted.org/packages/99/90/c1e070620042459d60df6356b666bb1f62198a89d68881816a7ed121595a/rpds_py-0.28.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d7366b6553cdc805abcc512b849a519167db8f5e5c3472010cd1228b224265cb", size = 411465, upload-time = "2025-10-22T22:22:41.395Z" },
- { url = "https://files.pythonhosted.org/packages/68/61/7c195b30d57f1b8d5970f600efee72a4fad79ec829057972e13a0370fd24/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b43c6a3726efd50f18d8120ec0551241c38785b68952d240c45ea553912ac41", size = 558832, upload-time = "2025-10-22T22:22:42.871Z" },
- { url = "https://files.pythonhosted.org/packages/b0/3d/06f3a718864773f69941d4deccdf18e5e47dd298b4628062f004c10f3b34/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0cb7203c7bc69d7c1585ebb33a2e6074492d2fc21ad28a7b9d40457ac2a51ab7", size = 583230, upload-time = "2025-10-22T22:22:44.877Z" },
- { url = "https://files.pythonhosted.org/packages/66/df/62fc783781a121e77fee9a21ead0a926f1b652280a33f5956a5e7833ed30/rpds_py-0.28.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a52a5169c664dfb495882adc75c304ae1d50df552fbd68e100fdc719dee4ff9", size = 553268, upload-time = "2025-10-22T22:22:46.441Z" },
- { url = "https://files.pythonhosted.org/packages/84/85/d34366e335140a4837902d3dea89b51f087bd6a63c993ebdff59e93ee61d/rpds_py-0.28.0-cp313-cp313-win32.whl", hash = "sha256:2e42456917b6687215b3e606ab46aa6bca040c77af7df9a08a6dcfe8a4d10ca5", size = 217100, upload-time = "2025-10-22T22:22:48.342Z" },
- { url = "https://files.pythonhosted.org/packages/3c/1c/f25a3f3752ad7601476e3eff395fe075e0f7813fbb9862bd67c82440e880/rpds_py-0.28.0-cp313-cp313-win_amd64.whl", hash = "sha256:e0a0311caedc8069d68fc2bf4c9019b58a2d5ce3cd7cb656c845f1615b577e1e", size = 227759, upload-time = "2025-10-22T22:22:50.219Z" },
- { url = "https://files.pythonhosted.org/packages/e0/d6/5f39b42b99615b5bc2f36ab90423ea404830bdfee1c706820943e9a645eb/rpds_py-0.28.0-cp313-cp313-win_arm64.whl", hash = "sha256:04c1b207ab8b581108801528d59ad80aa83bb170b35b0ddffb29c20e411acdc1", size = 217326, upload-time = "2025-10-22T22:22:51.647Z" },
- { url = "https://files.pythonhosted.org/packages/5c/8b/0c69b72d1cee20a63db534be0df271effe715ef6c744fdf1ff23bb2b0b1c/rpds_py-0.28.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f296ea3054e11fc58ad42e850e8b75c62d9a93a9f981ad04b2e5ae7d2186ff9c", size = 355736, upload-time = "2025-10-22T22:22:53.211Z" },
- { url = "https://files.pythonhosted.org/packages/f7/6d/0c2ee773cfb55c31a8514d2cece856dd299170a49babd50dcffb15ddc749/rpds_py-0.28.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5a7306c19b19005ad98468fcefeb7100b19c79fc23a5f24a12e06d91181193fa", size = 342677, upload-time = "2025-10-22T22:22:54.723Z" },
- { url = "https://files.pythonhosted.org/packages/e2/1c/22513ab25a27ea205144414724743e305e8153e6abe81833b5e678650f5a/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5d9b86aa501fed9862a443c5c3116f6ead8bc9296185f369277c42542bd646b", size = 371847, upload-time = "2025-10-22T22:22:56.295Z" },
- { url = "https://files.pythonhosted.org/packages/60/07/68e6ccdb4b05115ffe61d31afc94adef1833d3a72f76c9632d4d90d67954/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5bbc701eff140ba0e872691d573b3d5d30059ea26e5785acba9132d10c8c31d", size = 381800, upload-time = "2025-10-22T22:22:57.808Z" },
- { url = "https://files.pythonhosted.org/packages/73/bf/6d6d15df80781d7f9f368e7c1a00caf764436518c4877fb28b029c4624af/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a5690671cd672a45aa8616d7374fdf334a1b9c04a0cac3c854b1136e92374fe", size = 518827, upload-time = "2025-10-22T22:22:59.826Z" },
- { url = "https://files.pythonhosted.org/packages/7b/d3/2decbb2976cc452cbf12a2b0aaac5f1b9dc5dd9d1f7e2509a3ee00421249/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9f1d92ecea4fa12f978a367c32a5375a1982834649cdb96539dcdc12e609ab1a", size = 399471, upload-time = "2025-10-22T22:23:01.968Z" },
- { url = "https://files.pythonhosted.org/packages/b1/2c/f30892f9e54bd02e5faca3f6a26d6933c51055e67d54818af90abed9748e/rpds_py-0.28.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d252db6b1a78d0a3928b6190156042d54c93660ce4d98290d7b16b5296fb7cc", size = 377578, upload-time = "2025-10-22T22:23:03.52Z" },
- { url = "https://files.pythonhosted.org/packages/f0/5d/3bce97e5534157318f29ac06bf2d279dae2674ec12f7cb9c12739cee64d8/rpds_py-0.28.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:d61b355c3275acb825f8777d6c4505f42b5007e357af500939d4a35b19177259", size = 390482, upload-time = "2025-10-22T22:23:05.391Z" },
- { url = "https://files.pythonhosted.org/packages/e3/f0/886bd515ed457b5bd93b166175edb80a0b21a210c10e993392127f1e3931/rpds_py-0.28.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:acbe5e8b1026c0c580d0321c8aae4b0a1e1676861d48d6e8c6586625055b606a", size = 402447, upload-time = "2025-10-22T22:23:06.93Z" },
- { url = "https://files.pythonhosted.org/packages/42/b5/71e8777ac55e6af1f4f1c05b47542a1eaa6c33c1cf0d300dca6a1c6e159a/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8aa23b6f0fc59b85b4c7d89ba2965af274346f738e8d9fc2455763602e62fd5f", size = 552385, upload-time = "2025-10-22T22:23:08.557Z" },
- { url = "https://files.pythonhosted.org/packages/5d/cb/6ca2d70cbda5a8e36605e7788c4aa3bea7c17d71d213465a5a675079b98d/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7b14b0c680286958817c22d76fcbca4800ddacef6f678f3a7c79a1fe7067fe37", size = 575642, upload-time = "2025-10-22T22:23:10.348Z" },
- { url = "https://files.pythonhosted.org/packages/4a/d4/407ad9960ca7856d7b25c96dcbe019270b5ffdd83a561787bc682c797086/rpds_py-0.28.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bcf1d210dfee61a6c86551d67ee1031899c0fdbae88b2d44a569995d43797712", size = 544507, upload-time = "2025-10-22T22:23:12.434Z" },
- { url = "https://files.pythonhosted.org/packages/51/31/2f46fe0efcac23fbf5797c6b6b7e1c76f7d60773e525cb65fcbc582ee0f2/rpds_py-0.28.0-cp313-cp313t-win32.whl", hash = "sha256:3aa4dc0fdab4a7029ac63959a3ccf4ed605fee048ba67ce89ca3168da34a1342", size = 205376, upload-time = "2025-10-22T22:23:13.979Z" },
- { url = "https://files.pythonhosted.org/packages/92/e4/15947bda33cbedfc134490a41841ab8870a72a867a03d4969d886f6594a2/rpds_py-0.28.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7b7d9d83c942855e4fdcfa75d4f96f6b9e272d42fffcb72cd4bb2577db2e2907", size = 215907, upload-time = "2025-10-22T22:23:15.5Z" },
- { url = "https://files.pythonhosted.org/packages/08/47/ffe8cd7a6a02833b10623bf765fbb57ce977e9a4318ca0e8cf97e9c3d2b3/rpds_py-0.28.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:dcdcb890b3ada98a03f9f2bb108489cdc7580176cb73b4f2d789e9a1dac1d472", size = 353830, upload-time = "2025-10-22T22:23:17.03Z" },
- { url = "https://files.pythonhosted.org/packages/f9/9f/890f36cbd83a58491d0d91ae0db1702639edb33fb48eeb356f80ecc6b000/rpds_py-0.28.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f274f56a926ba2dc02976ca5b11c32855cbd5925534e57cfe1fda64e04d1add2", size = 341819, upload-time = "2025-10-22T22:23:18.57Z" },
- { url = "https://files.pythonhosted.org/packages/09/e3/921eb109f682aa24fb76207698fbbcf9418738f35a40c21652c29053f23d/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fe0438ac4a29a520ea94c8c7f1754cdd8feb1bc490dfda1bfd990072363d527", size = 373127, upload-time = "2025-10-22T22:23:20.216Z" },
- { url = "https://files.pythonhosted.org/packages/23/13/bce4384d9f8f4989f1a9599c71b7a2d877462e5fd7175e1f69b398f729f4/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a358a32dd3ae50e933347889b6af9a1bdf207ba5d1a3f34e1a38cd3540e6733", size = 382767, upload-time = "2025-10-22T22:23:21.787Z" },
- { url = "https://files.pythonhosted.org/packages/23/e1/579512b2d89a77c64ccef5a0bc46a6ef7f72ae0cf03d4b26dcd52e57ee0a/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e80848a71c78aa328fefaba9c244d588a342c8e03bda518447b624ea64d1ff56", size = 517585, upload-time = "2025-10-22T22:23:23.699Z" },
- { url = "https://files.pythonhosted.org/packages/62/3c/ca704b8d324a2591b0b0adcfcaadf9c862375b11f2f667ac03c61b4fd0a6/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f586db2e209d54fe177e58e0bc4946bea5fb0102f150b1b2f13de03e1f0976f8", size = 399828, upload-time = "2025-10-22T22:23:25.713Z" },
- { url = "https://files.pythonhosted.org/packages/da/37/e84283b9e897e3adc46b4c88bb3f6ec92a43bd4d2f7ef5b13459963b2e9c/rpds_py-0.28.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ae8ee156d6b586e4292491e885d41483136ab994e719a13458055bec14cf370", size = 375509, upload-time = "2025-10-22T22:23:27.32Z" },
- { url = "https://files.pythonhosted.org/packages/1a/c2/a980beab869d86258bf76ec42dec778ba98151f253a952b02fe36d72b29c/rpds_py-0.28.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a805e9b3973f7e27f7cab63a6b4f61d90f2e5557cff73b6e97cd5b8540276d3d", size = 392014, upload-time = "2025-10-22T22:23:29.332Z" },
- { url = "https://files.pythonhosted.org/packages/da/b5/b1d3c5f9d3fa5aeef74265f9c64de3c34a0d6d5cd3c81c8b17d5c8f10ed4/rpds_py-0.28.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5d3fd16b6dc89c73a4da0b4ac8b12a7ecc75b2864b95c9e5afed8003cb50a728", size = 402410, upload-time = "2025-10-22T22:23:31.14Z" },
- { url = "https://files.pythonhosted.org/packages/74/ae/cab05ff08dfcc052afc73dcb38cbc765ffc86f94e966f3924cd17492293c/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6796079e5d24fdaba6d49bda28e2c47347e89834678f2bc2c1b4fc1489c0fb01", size = 553593, upload-time = "2025-10-22T22:23:32.834Z" },
- { url = "https://files.pythonhosted.org/packages/70/80/50d5706ea2a9bfc9e9c5f401d91879e7c790c619969369800cde202da214/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:76500820c2af232435cbe215e3324c75b950a027134e044423f59f5b9a1ba515", size = 576925, upload-time = "2025-10-22T22:23:34.47Z" },
- { url = "https://files.pythonhosted.org/packages/ab/12/85a57d7a5855a3b188d024b099fd09c90db55d32a03626d0ed16352413ff/rpds_py-0.28.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:bbdc5640900a7dbf9dd707fe6388972f5bbd883633eb68b76591044cfe346f7e", size = 542444, upload-time = "2025-10-22T22:23:36.093Z" },
- { url = "https://files.pythonhosted.org/packages/6c/65/10643fb50179509150eb94d558e8837c57ca8b9adc04bd07b98e57b48f8c/rpds_py-0.28.0-cp314-cp314-win32.whl", hash = "sha256:adc8aa88486857d2b35d75f0640b949759f79dc105f50aa2c27816b2e0dd749f", size = 207968, upload-time = "2025-10-22T22:23:37.638Z" },
- { url = "https://files.pythonhosted.org/packages/b4/84/0c11fe4d9aaea784ff4652499e365963222481ac647bcd0251c88af646eb/rpds_py-0.28.0-cp314-cp314-win_amd64.whl", hash = "sha256:66e6fa8e075b58946e76a78e69e1a124a21d9a48a5b4766d15ba5b06869d1fa1", size = 218876, upload-time = "2025-10-22T22:23:39.179Z" },
- { url = "https://files.pythonhosted.org/packages/0f/e0/3ab3b86ded7bb18478392dc3e835f7b754cd446f62f3fc96f4fe2aca78f6/rpds_py-0.28.0-cp314-cp314-win_arm64.whl", hash = "sha256:a6fe887c2c5c59413353b7c0caff25d0e566623501ccfff88957fa438a69377d", size = 212506, upload-time = "2025-10-22T22:23:40.755Z" },
- { url = "https://files.pythonhosted.org/packages/51/ec/d5681bb425226c3501eab50fc30e9d275de20c131869322c8a1729c7b61c/rpds_py-0.28.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7a69df082db13c7070f7b8b1f155fa9e687f1d6aefb7b0e3f7231653b79a067b", size = 355433, upload-time = "2025-10-22T22:23:42.259Z" },
- { url = "https://files.pythonhosted.org/packages/be/ec/568c5e689e1cfb1ea8b875cffea3649260955f677fdd7ddc6176902d04cd/rpds_py-0.28.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b1cde22f2c30ebb049a9e74c5374994157b9b70a16147d332f89c99c5960737a", size = 342601, upload-time = "2025-10-22T22:23:44.372Z" },
- { url = "https://files.pythonhosted.org/packages/32/fe/51ada84d1d2a1d9d8f2c902cfddd0133b4a5eb543196ab5161d1c07ed2ad/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5338742f6ba7a51012ea470bd4dc600a8c713c0c72adaa0977a1b1f4327d6592", size = 372039, upload-time = "2025-10-22T22:23:46.025Z" },
- { url = "https://files.pythonhosted.org/packages/07/c1/60144a2f2620abade1a78e0d91b298ac2d9b91bc08864493fa00451ef06e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1460ebde1bcf6d496d80b191d854adedcc619f84ff17dc1c6d550f58c9efbba", size = 382407, upload-time = "2025-10-22T22:23:48.098Z" },
- { url = "https://files.pythonhosted.org/packages/45/ed/091a7bbdcf4038a60a461df50bc4c82a7ed6d5d5e27649aab61771c17585/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e3eb248f2feba84c692579257a043a7699e28a77d86c77b032c1d9fbb3f0219c", size = 518172, upload-time = "2025-10-22T22:23:50.16Z" },
- { url = "https://files.pythonhosted.org/packages/54/dd/02cc90c2fd9c2ef8016fd7813bfacd1c3a1325633ec8f244c47b449fc868/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3bbba5def70b16cd1c1d7255666aad3b290fbf8d0fe7f9f91abafb73611a91", size = 399020, upload-time = "2025-10-22T22:23:51.81Z" },
- { url = "https://files.pythonhosted.org/packages/ab/81/5d98cc0329bbb911ccecd0b9e19fbf7f3a5de8094b4cda5e71013b2dd77e/rpds_py-0.28.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3114f4db69ac5a1f32e7e4d1cbbe7c8f9cf8217f78e6e002cedf2d54c2a548ed", size = 377451, upload-time = "2025-10-22T22:23:53.711Z" },
- { url = "https://files.pythonhosted.org/packages/b4/07/4d5bcd49e3dfed2d38e2dcb49ab6615f2ceb9f89f5a372c46dbdebb4e028/rpds_py-0.28.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4b0cb8a906b1a0196b863d460c0222fb8ad0f34041568da5620f9799b83ccf0b", size = 390355, upload-time = "2025-10-22T22:23:55.299Z" },
- { url = "https://files.pythonhosted.org/packages/3f/79/9f14ba9010fee74e4f40bf578735cfcbb91d2e642ffd1abe429bb0b96364/rpds_py-0.28.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cf681ac76a60b667106141e11a92a3330890257e6f559ca995fbb5265160b56e", size = 403146, upload-time = "2025-10-22T22:23:56.929Z" },
- { url = "https://files.pythonhosted.org/packages/39/4c/f08283a82ac141331a83a40652830edd3a4a92c34e07e2bbe00baaea2f5f/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1e8ee6413cfc677ce8898d9cde18cc3a60fc2ba756b0dec5b71eb6eb21c49fa1", size = 552656, upload-time = "2025-10-22T22:23:58.62Z" },
- { url = "https://files.pythonhosted.org/packages/61/47/d922fc0666f0dd8e40c33990d055f4cc6ecff6f502c2d01569dbed830f9b/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3072b16904d0b5572a15eb9d31c1954e0d3227a585fc1351aa9878729099d6c", size = 576782, upload-time = "2025-10-22T22:24:00.312Z" },
- { url = "https://files.pythonhosted.org/packages/d3/0c/5bafdd8ccf6aa9d3bfc630cfece457ff5b581af24f46a9f3590f790e3df2/rpds_py-0.28.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b670c30fd87a6aec281c3c9896d3bae4b205fd75d79d06dc87c2503717e46092", size = 544671, upload-time = "2025-10-22T22:24:02.297Z" },
- { url = "https://files.pythonhosted.org/packages/2c/37/dcc5d8397caa924988693519069d0beea077a866128719351a4ad95e82fc/rpds_py-0.28.0-cp314-cp314t-win32.whl", hash = "sha256:8014045a15b4d2b3476f0a287fcc93d4f823472d7d1308d47884ecac9e612be3", size = 205749, upload-time = "2025-10-22T22:24:03.848Z" },
- { url = "https://files.pythonhosted.org/packages/d7/69/64d43b21a10d72b45939a28961216baeb721cc2a430f5f7c3bfa21659a53/rpds_py-0.28.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7a4e59c90d9c27c561eb3160323634a9ff50b04e4f7820600a2beb0ac90db578", size = 216233, upload-time = "2025-10-22T22:24:05.471Z" },
- { url = "https://files.pythonhosted.org/packages/ae/bc/b43f2ea505f28119bd551ae75f70be0c803d2dbcd37c1b3734909e40620b/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f5e7101145427087e493b9c9b959da68d357c28c562792300dd21a095118ed16", size = 363913, upload-time = "2025-10-22T22:24:07.129Z" },
- { url = "https://files.pythonhosted.org/packages/28/f2/db318195d324c89a2c57dc5195058cbadd71b20d220685c5bd1da79ee7fe/rpds_py-0.28.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:31eb671150b9c62409a888850aaa8e6533635704fe2b78335f9aaf7ff81eec4d", size = 350452, upload-time = "2025-10-22T22:24:08.754Z" },
- { url = "https://files.pythonhosted.org/packages/ae/f2/1391c819b8573a4898cedd6b6c5ec5bc370ce59e5d6bdcebe3c9c1db4588/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48b55c1f64482f7d8bd39942f376bfdf2f6aec637ee8c805b5041e14eeb771db", size = 380957, upload-time = "2025-10-22T22:24:10.826Z" },
- { url = "https://files.pythonhosted.org/packages/5a/5c/e5de68ee7eb7248fce93269833d1b329a196d736aefb1a7481d1e99d1222/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24743a7b372e9a76171f6b69c01aedf927e8ac3e16c474d9fe20d552a8cb45c7", size = 391919, upload-time = "2025-10-22T22:24:12.559Z" },
- { url = "https://files.pythonhosted.org/packages/fb/4f/2376336112cbfeb122fd435d608ad8d5041b3aed176f85a3cb32c262eb80/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:389c29045ee8bbb1627ea190b4976a310a295559eaf9f1464a1a6f2bf84dde78", size = 528541, upload-time = "2025-10-22T22:24:14.197Z" },
- { url = "https://files.pythonhosted.org/packages/68/53/5ae232e795853dd20da7225c5dd13a09c0a905b1a655e92bdf8d78a99fd9/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23690b5827e643150cf7b49569679ec13fe9a610a15949ed48b85eb7f98f34ec", size = 405629, upload-time = "2025-10-22T22:24:16.001Z" },
- { url = "https://files.pythonhosted.org/packages/b9/2d/351a3b852b683ca9b6b8b38ed9efb2347596973849ba6c3a0e99877c10aa/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f0c9266c26580e7243ad0d72fc3e01d6b33866cfab5084a6da7576bcf1c4f72", size = 384123, upload-time = "2025-10-22T22:24:17.585Z" },
- { url = "https://files.pythonhosted.org/packages/e0/15/870804daa00202728cc91cb8e2385fa9f1f4eb49857c49cfce89e304eae6/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:4c6c4db5d73d179746951486df97fd25e92396be07fc29ee8ff9a8f5afbdfb27", size = 400923, upload-time = "2025-10-22T22:24:19.512Z" },
- { url = "https://files.pythonhosted.org/packages/53/25/3706b83c125fa2a0bccceac951de3f76631f6bd0ee4d02a0ed780712ef1b/rpds_py-0.28.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3b695a8fa799dd2cfdb4804b37096c5f6dba1ac7f48a7fbf6d0485bcd060316", size = 413767, upload-time = "2025-10-22T22:24:21.316Z" },
- { url = "https://files.pythonhosted.org/packages/ef/f9/ce43dbe62767432273ed2584cef71fef8411bddfb64125d4c19128015018/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:6aa1bfce3f83baf00d9c5fcdbba93a3ab79958b4c7d7d1f55e7fe68c20e63912", size = 561530, upload-time = "2025-10-22T22:24:22.958Z" },
- { url = "https://files.pythonhosted.org/packages/46/c9/ffe77999ed8f81e30713dd38fd9ecaa161f28ec48bb80fa1cd9118399c27/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:7b0f9dceb221792b3ee6acb5438eb1f02b0cb2c247796a72b016dcc92c6de829", size = 585453, upload-time = "2025-10-22T22:24:24.779Z" },
- { url = "https://files.pythonhosted.org/packages/ed/d2/4a73b18821fd4669762c855fd1f4e80ceb66fb72d71162d14da58444a763/rpds_py-0.28.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5d0145edba8abd3db0ab22b5300c99dc152f5c9021fab861be0f0544dc3cbc5f", size = 552199, upload-time = "2025-10-22T22:24:26.54Z" },
+ { url = "https://files.pythonhosted.org/packages/06/0c/0c411a0ec64ccb6d104dcabe0e713e05e153a9a2c3c2bd2b32ce412166fe/rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288", size = 370490, upload-time = "2025-11-30T20:21:33.256Z" },
+ { url = "https://files.pythonhosted.org/packages/19/6a/4ba3d0fb7297ebae71171822554abe48d7cab29c28b8f9f2c04b79988c05/rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00", size = 359751, upload-time = "2025-11-30T20:21:34.591Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/7c/e4933565ef7f7a0818985d87c15d9d273f1a649afa6a52ea35ad011195ea/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6", size = 389696, upload-time = "2025-11-30T20:21:36.122Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/01/6271a2511ad0815f00f7ed4390cf2567bec1d4b1da39e2c27a41e6e3b4de/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7", size = 403136, upload-time = "2025-11-30T20:21:37.728Z" },
+ { url = "https://files.pythonhosted.org/packages/55/64/c857eb7cd7541e9b4eee9d49c196e833128a55b89a9850a9c9ac33ccf897/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324", size = 524699, upload-time = "2025-11-30T20:21:38.92Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/ed/94816543404078af9ab26159c44f9e98e20fe47e2126d5d32c9d9948d10a/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df", size = 412022, upload-time = "2025-11-30T20:21:40.407Z" },
+ { url = "https://files.pythonhosted.org/packages/61/b5/707f6cf0066a6412aacc11d17920ea2e19e5b2f04081c64526eb35b5c6e7/rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3", size = 390522, upload-time = "2025-11-30T20:21:42.17Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4e/57a85fda37a229ff4226f8cbcf09f2a455d1ed20e802ce5b2b4a7f5ed053/rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221", size = 404579, upload-time = "2025-11-30T20:21:43.769Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/da/c9339293513ec680a721e0e16bf2bac3db6e5d7e922488de471308349bba/rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7", size = 421305, upload-time = "2025-11-30T20:21:44.994Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/be/522cb84751114f4ad9d822ff5a1aa3c98006341895d5f084779b99596e5c/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff", size = 572503, upload-time = "2025-11-30T20:21:46.91Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/9b/de879f7e7ceddc973ea6e4629e9b380213a6938a249e94b0cdbcc325bb66/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7", size = 598322, upload-time = "2025-11-30T20:21:48.709Z" },
+ { url = "https://files.pythonhosted.org/packages/48/ac/f01fc22efec3f37d8a914fc1b2fb9bcafd56a299edbe96406f3053edea5a/rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139", size = 560792, upload-time = "2025-11-30T20:21:50.024Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/da/4e2b19d0f131f35b6146425f846563d0ce036763e38913d917187307a671/rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464", size = 221901, upload-time = "2025-11-30T20:21:51.32Z" },
+ { url = "https://files.pythonhosted.org/packages/96/cb/156d7a5cf4f78a7cc571465d8aec7a3c447c94f6749c5123f08438bcf7bc/rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169", size = 235823, upload-time = "2025-11-30T20:21:52.505Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/6e/f964e88b3d2abee2a82c1ac8366da848fce1c6d834dc2132c3fda3970290/rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425", size = 370157, upload-time = "2025-11-30T20:21:53.789Z" },
+ { url = "https://files.pythonhosted.org/packages/94/ba/24e5ebb7c1c82e74c4e4f33b2112a5573ddc703915b13a073737b59b86e0/rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d", size = 359676, upload-time = "2025-11-30T20:21:55.475Z" },
+ { url = "https://files.pythonhosted.org/packages/84/86/04dbba1b087227747d64d80c3b74df946b986c57af0a9f0c98726d4d7a3b/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4", size = 389938, upload-time = "2025-11-30T20:21:57.079Z" },
+ { url = "https://files.pythonhosted.org/packages/42/bb/1463f0b1722b7f45431bdd468301991d1328b16cffe0b1c2918eba2c4eee/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f", size = 402932, upload-time = "2025-11-30T20:21:58.47Z" },
+ { url = "https://files.pythonhosted.org/packages/99/ee/2520700a5c1f2d76631f948b0736cdf9b0acb25abd0ca8e889b5c62ac2e3/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4", size = 525830, upload-time = "2025-11-30T20:21:59.699Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/ad/bd0331f740f5705cc555a5e17fdf334671262160270962e69a2bdef3bf76/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97", size = 412033, upload-time = "2025-11-30T20:22:00.991Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/1e/372195d326549bb51f0ba0f2ecb9874579906b97e08880e7a65c3bef1a99/rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89", size = 390828, upload-time = "2025-11-30T20:22:02.723Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/2b/d88bb33294e3e0c76bc8f351a3721212713629ffca1700fa94979cb3eae8/rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d", size = 404683, upload-time = "2025-11-30T20:22:04.367Z" },
+ { url = "https://files.pythonhosted.org/packages/50/32/c759a8d42bcb5289c1fac697cd92f6fe01a018dd937e62ae77e0e7f15702/rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038", size = 421583, upload-time = "2025-11-30T20:22:05.814Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/81/e729761dbd55ddf5d84ec4ff1f47857f4374b0f19bdabfcf929164da3e24/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7", size = 572496, upload-time = "2025-11-30T20:22:07.713Z" },
+ { url = "https://files.pythonhosted.org/packages/14/f6/69066a924c3557c9c30baa6ec3a0aa07526305684c6f86c696b08860726c/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed", size = 598669, upload-time = "2025-11-30T20:22:09.312Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/48/905896b1eb8a05630d20333d1d8ffd162394127b74ce0b0784ae04498d32/rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85", size = 561011, upload-time = "2025-11-30T20:22:11.309Z" },
+ { url = "https://files.pythonhosted.org/packages/22/16/cd3027c7e279d22e5eb431dd3c0fbc677bed58797fe7581e148f3f68818b/rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c", size = 221406, upload-time = "2025-11-30T20:22:13.101Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/5b/e7b7aa136f28462b344e652ee010d4de26ee9fd16f1bfd5811f5153ccf89/rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825", size = 236024, upload-time = "2025-11-30T20:22:14.853Z" },
+ { url = "https://files.pythonhosted.org/packages/14/a6/364bba985e4c13658edb156640608f2c9e1d3ea3c81b27aa9d889fff0e31/rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229", size = 229069, upload-time = "2025-11-30T20:22:16.577Z" },
+ { url = "https://files.pythonhosted.org/packages/03/e7/98a2f4ac921d82f33e03f3835f5bf3a4a40aa1bfdc57975e74a97b2b4bdd/rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad", size = 375086, upload-time = "2025-11-30T20:22:17.93Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/a1/bca7fd3d452b272e13335db8d6b0b3ecde0f90ad6f16f3328c6fb150c889/rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05", size = 359053, upload-time = "2025-11-30T20:22:19.297Z" },
+ { url = "https://files.pythonhosted.org/packages/65/1c/ae157e83a6357eceff62ba7e52113e3ec4834a84cfe07fa4b0757a7d105f/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28", size = 390763, upload-time = "2025-11-30T20:22:21.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/36/eb2eb8515e2ad24c0bd43c3ee9cd74c33f7ca6430755ccdb240fd3144c44/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd", size = 408951, upload-time = "2025-11-30T20:22:23.408Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/65/ad8dc1784a331fabbd740ef6f71ce2198c7ed0890dab595adb9ea2d775a1/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f", size = 514622, upload-time = "2025-11-30T20:22:25.16Z" },
+ { url = "https://files.pythonhosted.org/packages/63/8e/0cfa7ae158e15e143fe03993b5bcd743a59f541f5952e1546b1ac1b5fd45/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1", size = 414492, upload-time = "2025-11-30T20:22:26.505Z" },
+ { url = "https://files.pythonhosted.org/packages/60/1b/6f8f29f3f995c7ffdde46a626ddccd7c63aefc0efae881dc13b6e5d5bb16/rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23", size = 394080, upload-time = "2025-11-30T20:22:27.934Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/d5/a266341051a7a3ca2f4b750a3aa4abc986378431fc2da508c5034d081b70/rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6", size = 408680, upload-time = "2025-11-30T20:22:29.341Z" },
+ { url = "https://files.pythonhosted.org/packages/10/3b/71b725851df9ab7a7a4e33cf36d241933da66040d195a84781f49c50490c/rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51", size = 423589, upload-time = "2025-11-30T20:22:31.469Z" },
+ { url = "https://files.pythonhosted.org/packages/00/2b/e59e58c544dc9bd8bd8384ecdb8ea91f6727f0e37a7131baeff8d6f51661/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5", size = 573289, upload-time = "2025-11-30T20:22:32.997Z" },
+ { url = "https://files.pythonhosted.org/packages/da/3e/a18e6f5b460893172a7d6a680e86d3b6bc87a54c1f0b03446a3c8c7b588f/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e", size = 599737, upload-time = "2025-11-30T20:22:34.419Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e2/714694e4b87b85a18e2c243614974413c60aa107fd815b8cbc42b873d1d7/rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394", size = 563120, upload-time = "2025-11-30T20:22:35.903Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/ab/d5d5e3bcedb0a77f4f613706b750e50a5a3ba1c15ccd3665ecc636c968fd/rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf", size = 223782, upload-time = "2025-11-30T20:22:37.271Z" },
+ { url = "https://files.pythonhosted.org/packages/39/3b/f786af9957306fdc38a74cef405b7b93180f481fb48453a114bb6465744a/rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b", size = 240463, upload-time = "2025-11-30T20:22:39.021Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/d2/b91dc748126c1559042cfe41990deb92c4ee3e2b415f6b5234969ffaf0cc/rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e", size = 230868, upload-time = "2025-11-30T20:22:40.493Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/dc/d61221eb88ff410de3c49143407f6f3147acf2538c86f2ab7ce65ae7d5f9/rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2", size = 374887, upload-time = "2025-11-30T20:22:41.812Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/32/55fb50ae104061dbc564ef15cc43c013dc4a9f4527a1f4d99baddf56fe5f/rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8", size = 358904, upload-time = "2025-11-30T20:22:43.479Z" },
+ { url = "https://files.pythonhosted.org/packages/58/70/faed8186300e3b9bdd138d0273109784eea2396c68458ed580f885dfe7ad/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4", size = 389945, upload-time = "2025-11-30T20:22:44.819Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/a8/073cac3ed2c6387df38f71296d002ab43496a96b92c823e76f46b8af0543/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136", size = 407783, upload-time = "2025-11-30T20:22:46.103Z" },
+ { url = "https://files.pythonhosted.org/packages/77/57/5999eb8c58671f1c11eba084115e77a8899d6e694d2a18f69f0ba471ec8b/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7", size = 515021, upload-time = "2025-11-30T20:22:47.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/af/5ab4833eadc36c0a8ed2bc5c0de0493c04f6c06de223170bd0798ff98ced/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2", size = 414589, upload-time = "2025-11-30T20:22:48.872Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/de/f7192e12b21b9e9a68a6d0f249b4af3fdcdff8418be0767a627564afa1f1/rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6", size = 394025, upload-time = "2025-11-30T20:22:50.196Z" },
+ { url = "https://files.pythonhosted.org/packages/91/c4/fc70cd0249496493500e7cc2de87504f5aa6509de1e88623431fec76d4b6/rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e", size = 408895, upload-time = "2025-11-30T20:22:51.87Z" },
+ { url = "https://files.pythonhosted.org/packages/58/95/d9275b05ab96556fefff73a385813eb66032e4c99f411d0795372d9abcea/rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d", size = 422799, upload-time = "2025-11-30T20:22:53.341Z" },
+ { url = "https://files.pythonhosted.org/packages/06/c1/3088fc04b6624eb12a57eb814f0d4997a44b0d208d6cace713033ff1a6ba/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7", size = 572731, upload-time = "2025-11-30T20:22:54.778Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/42/c612a833183b39774e8ac8fecae81263a68b9583ee343db33ab571a7ce55/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31", size = 599027, upload-time = "2025-11-30T20:22:56.212Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/60/525a50f45b01d70005403ae0e25f43c0384369ad24ffe46e8d9068b50086/rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95", size = 563020, upload-time = "2025-11-30T20:22:58.2Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/5d/47c4655e9bcd5ca907148535c10e7d489044243cc9941c16ed7cd53be91d/rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d", size = 223139, upload-time = "2025-11-30T20:23:00.209Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/e1/485132437d20aa4d3e1d8b3fb5a5e65aa8139f1e097080c2a8443201742c/rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15", size = 240224, upload-time = "2025-11-30T20:23:02.008Z" },
+ { url = "https://files.pythonhosted.org/packages/24/95/ffd128ed1146a153d928617b0ef673960130be0009c77d8fbf0abe306713/rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1", size = 230645, upload-time = "2025-11-30T20:23:03.43Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/1b/b10de890a0def2a319a2626334a7f0ae388215eb60914dbac8a3bae54435/rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a", size = 364443, upload-time = "2025-11-30T20:23:04.878Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/bf/27e39f5971dc4f305a4fb9c672ca06f290f7c4e261c568f3dea16a410d47/rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e", size = 353375, upload-time = "2025-11-30T20:23:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/40/58/442ada3bba6e8e6615fc00483135c14a7538d2ffac30e2d933ccf6852232/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000", size = 383850, upload-time = "2025-11-30T20:23:07.825Z" },
+ { url = "https://files.pythonhosted.org/packages/14/14/f59b0127409a33c6ef6f5c1ebd5ad8e32d7861c9c7adfa9a624fc3889f6c/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db", size = 392812, upload-time = "2025-11-30T20:23:09.228Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/66/e0be3e162ac299b3a22527e8913767d869e6cc75c46bd844aa43fb81ab62/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2", size = 517841, upload-time = "2025-11-30T20:23:11.186Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/55/fa3b9cf31d0c963ecf1ba777f7cf4b2a2c976795ac430d24a1f43d25a6ba/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa", size = 408149, upload-time = "2025-11-30T20:23:12.864Z" },
+ { url = "https://files.pythonhosted.org/packages/60/ca/780cf3b1a32b18c0f05c441958d3758f02544f1d613abf9488cd78876378/rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083", size = 383843, upload-time = "2025-11-30T20:23:14.638Z" },
+ { url = "https://files.pythonhosted.org/packages/82/86/d5f2e04f2aa6247c613da0c1dd87fcd08fa17107e858193566048a1e2f0a/rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9", size = 396507, upload-time = "2025-11-30T20:23:16.105Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/9a/453255d2f769fe44e07ea9785c8347edaf867f7026872e76c1ad9f7bed92/rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0", size = 414949, upload-time = "2025-11-30T20:23:17.539Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/31/622a86cdc0c45d6df0e9ccb6becdba5074735e7033c20e401a6d9d0e2ca0/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94", size = 565790, upload-time = "2025-11-30T20:23:19.029Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/5d/15bbf0fb4a3f58a3b1c67855ec1efcc4ceaef4e86644665fff03e1b66d8d/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08", size = 590217, upload-time = "2025-11-30T20:23:20.885Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/61/21b8c41f68e60c8cc3b2e25644f0e3681926020f11d06ab0b78e3c6bbff1/rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27", size = 555806, upload-time = "2025-11-30T20:23:22.488Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/39/7e067bb06c31de48de3eb200f9fc7c58982a4d3db44b07e73963e10d3be9/rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6", size = 211341, upload-time = "2025-11-30T20:23:24.449Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/4d/222ef0b46443cf4cf46764d9c630f3fe4abaa7245be9417e56e9f52b8f65/rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d", size = 225768, upload-time = "2025-11-30T20:23:25.908Z" },
+ { url = "https://files.pythonhosted.org/packages/86/81/dad16382ebbd3d0e0328776d8fd7ca94220e4fa0798d1dc5e7da48cb3201/rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0", size = 362099, upload-time = "2025-11-30T20:23:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/60/19f7884db5d5603edf3c6bce35408f45ad3e97e10007df0e17dd57af18f8/rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be", size = 353192, upload-time = "2025-11-30T20:23:29.151Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/c4/76eb0e1e72d1a9c4703c69607cec123c29028bff28ce41588792417098ac/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f", size = 384080, upload-time = "2025-11-30T20:23:30.785Z" },
+ { url = "https://files.pythonhosted.org/packages/72/87/87ea665e92f3298d1b26d78814721dc39ed8d2c74b86e83348d6b48a6f31/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f", size = 394841, upload-time = "2025-11-30T20:23:32.209Z" },
+ { url = "https://files.pythonhosted.org/packages/77/ad/7783a89ca0587c15dcbf139b4a8364a872a25f861bdb88ed99f9b0dec985/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87", size = 516670, upload-time = "2025-11-30T20:23:33.742Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/3c/2882bdac942bd2172f3da574eab16f309ae10a3925644e969536553cb4ee/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18", size = 408005, upload-time = "2025-11-30T20:23:35.253Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/81/9a91c0111ce1758c92516a3e44776920b579d9a7c09b2b06b642d4de3f0f/rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad", size = 382112, upload-time = "2025-11-30T20:23:36.842Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8e/1da49d4a107027e5fbc64daeab96a0706361a2918da10cb41769244b805d/rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07", size = 399049, upload-time = "2025-11-30T20:23:38.343Z" },
+ { url = "https://files.pythonhosted.org/packages/df/5a/7ee239b1aa48a127570ec03becbb29c9d5a9eb092febbd1699d567cae859/rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f", size = 415661, upload-time = "2025-11-30T20:23:40.263Z" },
+ { url = "https://files.pythonhosted.org/packages/70/ea/caa143cf6b772f823bc7929a45da1fa83569ee49b11d18d0ada7f5ee6fd6/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65", size = 565606, upload-time = "2025-11-30T20:23:42.186Z" },
+ { url = "https://files.pythonhosted.org/packages/64/91/ac20ba2d69303f961ad8cf55bf7dbdb4763f627291ba3d0d7d67333cced9/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f", size = 591126, upload-time = "2025-11-30T20:23:44.086Z" },
+ { url = "https://files.pythonhosted.org/packages/21/20/7ff5f3c8b00c8a95f75985128c26ba44503fb35b8e0259d812766ea966c7/rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53", size = 553371, upload-time = "2025-11-30T20:23:46.004Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c7/81dadd7b27c8ee391c132a6b192111ca58d866577ce2d9b0ca157552cce0/rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed", size = 215298, upload-time = "2025-11-30T20:23:47.696Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d2/1aaac33287e8cfb07aab2e6b8ac1deca62f6f65411344f1433c55e6f3eb8/rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950", size = 228604, upload-time = "2025-11-30T20:23:49.501Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/95/ab005315818cc519ad074cb7784dae60d939163108bd2b394e60dc7b5461/rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6", size = 222391, upload-time = "2025-11-30T20:23:50.96Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/68/154fe0194d83b973cdedcdcc88947a2752411165930182ae41d983dcefa6/rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb", size = 364868, upload-time = "2025-11-30T20:23:52.494Z" },
+ { url = "https://files.pythonhosted.org/packages/83/69/8bbc8b07ec854d92a8b75668c24d2abcb1719ebf890f5604c61c9369a16f/rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8", size = 353747, upload-time = "2025-11-30T20:23:54.036Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/00/ba2e50183dbd9abcce9497fa5149c62b4ff3e22d338a30d690f9af970561/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7", size = 383795, upload-time = "2025-11-30T20:23:55.556Z" },
+ { url = "https://files.pythonhosted.org/packages/05/6f/86f0272b84926bcb0e4c972262f54223e8ecc556b3224d281e6598fc9268/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898", size = 393330, upload-time = "2025-11-30T20:23:57.033Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/e9/0e02bb2e6dc63d212641da45df2b0bf29699d01715913e0d0f017ee29438/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e", size = 518194, upload-time = "2025-11-30T20:23:58.637Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/ca/be7bca14cf21513bdf9c0606aba17d1f389ea2b6987035eb4f62bd923f25/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419", size = 408340, upload-time = "2025-11-30T20:24:00.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c7/736e00ebf39ed81d75544c0da6ef7b0998f8201b369acf842f9a90dc8fce/rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551", size = 383765, upload-time = "2025-11-30T20:24:01.759Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/3f/da50dfde9956aaf365c4adc9533b100008ed31aea635f2b8d7b627e25b49/rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8", size = 396834, upload-time = "2025-11-30T20:24:03.687Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/00/34bcc2565b6020eab2623349efbdec810676ad571995911f1abdae62a3a0/rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5", size = 415470, upload-time = "2025-11-30T20:24:05.232Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/28/882e72b5b3e6f718d5453bd4d0d9cf8df36fddeb4ddbbab17869d5868616/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404", size = 565630, upload-time = "2025-11-30T20:24:06.878Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/97/04a65539c17692de5b85c6e293520fd01317fd878ea1995f0367d4532fb1/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856", size = 591148, upload-time = "2025-11-30T20:24:08.445Z" },
+ { url = "https://files.pythonhosted.org/packages/85/70/92482ccffb96f5441aab93e26c4d66489eb599efdcf96fad90c14bbfb976/rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40", size = 556030, upload-time = "2025-11-30T20:24:10.956Z" },
+ { url = "https://files.pythonhosted.org/packages/20/53/7c7e784abfa500a2b6b583b147ee4bb5a2b3747a9166bab52fec4b5b5e7d/rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0", size = 211570, upload-time = "2025-11-30T20:24:12.735Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/02/fa464cdfbe6b26e0600b62c528b72d8608f5cc49f96b8d6e38c95d60c676/rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3", size = 226532, upload-time = "2025-11-30T20:24:14.634Z" },
+ { url = "https://files.pythonhosted.org/packages/69/71/3f34339ee70521864411f8b6992e7ab13ac30d8e4e3309e07c7361767d91/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58", size = 372292, upload-time = "2025-11-30T20:24:16.537Z" },
+ { url = "https://files.pythonhosted.org/packages/57/09/f183df9b8f2d66720d2ef71075c59f7e1b336bec7ee4c48f0a2b06857653/rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a", size = 362128, upload-time = "2025-11-30T20:24:18.086Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/68/5c2594e937253457342e078f0cc1ded3dd7b2ad59afdbf2d354869110a02/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb", size = 391542, upload-time = "2025-11-30T20:24:20.092Z" },
+ { url = "https://files.pythonhosted.org/packages/49/5c/31ef1afd70b4b4fbdb2800249f34c57c64beb687495b10aec0365f53dfc4/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c", size = 404004, upload-time = "2025-11-30T20:24:22.231Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/63/0cfbea38d05756f3440ce6534d51a491d26176ac045e2707adc99bb6e60a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3", size = 527063, upload-time = "2025-11-30T20:24:24.302Z" },
+ { url = "https://files.pythonhosted.org/packages/42/e6/01e1f72a2456678b0f618fc9a1a13f882061690893c192fcad9f2926553a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5", size = 413099, upload-time = "2025-11-30T20:24:25.916Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/25/8df56677f209003dcbb180765520c544525e3ef21ea72279c98b9aa7c7fb/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738", size = 392177, upload-time = "2025-11-30T20:24:27.834Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/b4/0a771378c5f16f8115f796d1f437950158679bcd2a7c68cf251cfb00ed5b/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f", size = 406015, upload-time = "2025-11-30T20:24:29.457Z" },
+ { url = "https://files.pythonhosted.org/packages/36/d8/456dbba0af75049dc6f63ff295a2f92766b9d521fa00de67a2bd6427d57a/rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877", size = 423736, upload-time = "2025-11-30T20:24:31.22Z" },
+ { url = "https://files.pythonhosted.org/packages/13/64/b4d76f227d5c45a7e0b796c674fd81b0a6c4fbd48dc29271857d8219571c/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a", size = 573981, upload-time = "2025-11-30T20:24:32.934Z" },
+ { url = "https://files.pythonhosted.org/packages/20/91/092bacadeda3edf92bf743cc96a7be133e13a39cdbfd7b5082e7ab638406/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4", size = 599782, upload-time = "2025-11-30T20:24:35.169Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/b7/b95708304cd49b7b6f82fdd039f1748b66ec2b21d6a45180910802f1abf1/rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e", size = 562191, upload-time = "2025-11-30T20:24:36.853Z" },
]
[[package]]
name = "secretstorage"
-version = "3.4.0"
+version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
- { name = "cryptography" },
- { name = "jeepney" },
+ { name = "cryptography", marker = "sys_platform != 'win32'" },
+ { name = "jeepney", marker = "sys_platform != 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/31/9f/11ef35cf1027c1339552ea7bfe6aaa74a8516d8b5caf6e7d338daf54fd80/secretstorage-3.4.0.tar.gz", hash = "sha256:c46e216d6815aff8a8a18706a2fbfd8d53fcbb0dce99301881687a1b0289ef7c", size = 19748, upload-time = "2025-09-09T16:42:13.859Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/03/e834bcd866f2f8a49a85eaff47340affa3bfa391ee9912a952a1faa68c7b/secretstorage-3.5.0.tar.gz", hash = "sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be", size = 19884, upload-time = "2025-11-23T19:02:53.191Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/91/ff/2e2eed29e02c14a5cb6c57f09b2d5b40e65d6cc71f45b52e0be295ccbc2f/secretstorage-3.4.0-py3-none-any.whl", hash = "sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e", size = 15272, upload-time = "2025-09-09T16:42:12.744Z" },
-]
-
-[[package]]
-name = "shellingham"
-version = "1.5.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
-]
-
-[[package]]
-name = "sniffio"
-version = "1.3.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
-]
-
-[[package]]
-name = "sortedcontainers"
-version = "2.4.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
]
[[package]]
name = "sse-starlette"
-version = "3.0.3"
+version = "3.3.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
+ { name = "starlette" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/db/3c/fa6517610dc641262b77cc7bf994ecd17465812c1b0585fe33e11be758ab/sse_starlette-3.0.3.tar.gz", hash = "sha256:88cfb08747e16200ea990c8ca876b03910a23b547ab3bd764c0d8eb81019b971", size = 21943, upload-time = "2025-10-30T18:44:20.117Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/9f/c3695c2d2d4ef70072c3a06992850498b01c6bc9be531950813716b426fa/sse_starlette-3.3.2.tar.gz", hash = "sha256:678fca55a1945c734d8472a6cad186a55ab02840b4f6786f5ee8770970579dcd", size = 32326, upload-time = "2026-02-28T11:24:34.36Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/23/a0/984525d19ca5c8a6c33911a0c164b11490dd0f90ff7fd689f704f84e9a11/sse_starlette-3.0.3-py3-none-any.whl", hash = "sha256:af5bf5a6f3933df1d9c7f8539633dc8444ca6a97ab2e2a7cd3b6e431ac03a431", size = 11765, upload-time = "2025-10-30T18:44:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/61/28/8cb142d3fe80c4a2d8af54ca0b003f47ce0ba920974e7990fa6e016402d1/sse_starlette-3.3.2-py3-none-any.whl", hash = "sha256:5c3ea3dad425c601236726af2f27689b74494643f57017cafcb6f8c9acfbb862", size = 14270, upload-time = "2026-02-28T11:24:32.984Z" },
]
[[package]]
name = "starlette"
-version = "0.50.0"
+version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
]
[[package]]
@@ -1621,72 +1323,62 @@ dependencies = [
requires-dist = [
{ name = "dirty-equals", specifier = ">=0.9.0" },
{ name = "fastmcp", specifier = ">=2.0.0" },
- { name = "pytest", specifier = ">=8.3.3" },
+ { name = "pytest", specifier = ">=9.0.3" },
{ name = "pytest-asyncio", specifier = ">=1.2.0" },
]
[[package]]
name = "tomli"
-version = "2.3.0"
+version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" },
- { url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" },
- { url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" },
- { url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" },
- { url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" },
- { url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" },
- { url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" },
- { url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" },
- { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" },
- { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" },
- { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" },
- { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" },
- { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" },
- { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" },
- { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" },
- { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" },
- { url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" },
- { url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" },
- { url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" },
- { url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" },
- { url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" },
- { url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" },
- { url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" },
- { url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" },
- { url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" },
- { url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" },
- { url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" },
- { url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" },
- { url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" },
- { url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" },
- { url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" },
- { url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" },
- { url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" },
- { url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" },
- { url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" },
- { url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" },
- { url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" },
- { url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" },
- { url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" },
- { url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" },
- { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" },
-]
-
-[[package]]
-name = "typer"
-version = "0.21.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "rich" },
- { name = "shellingham" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/36/bf/8825b5929afd84d0dabd606c67cd57b8388cb3ec385f7ef19c5cc2202069/typer-0.21.1.tar.gz", hash = "sha256:ea835607cd752343b6b2b7ce676893e5a0324082268b48f27aa058bdb7d2145d", size = 110371, upload-time = "2026-01-06T11:21:10.989Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/a0/1d/d9257dd49ff2ca23ea5f132edf1281a0c4f9de8a762b9ae399b670a59235/typer-0.21.1-py3-none-any.whl", hash = "sha256:7985e89081c636b88d172c2ee0cfe33c253160994d47bdfdc302defd7d1f1d01", size = 47381, upload-time = "2026-01-06T11:21:09.824Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
+ { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
+ { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
+ { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
+ { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
+ { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
+ { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
+ { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
+ { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
+ { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
+ { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
+ { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
+ { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
]
[[package]]
@@ -1711,154 +1403,197 @@ wheels = [
]
[[package]]
-name = "urllib3"
-version = "2.6.3"
+name = "uncalled-for"
+version = "0.2.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/02/7c/b5b7d8136f872e3f13b0584e576886de0489d7213a12de6bebf29ff6ebfc/uncalled_for-0.2.0.tar.gz", hash = "sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f", size = 49488, upload-time = "2026-02-27T17:40:58.137Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/7f/4320d9ce3be404e6310b915c3629fe27bf1e2f438a1a7a3cb0396e32e9a9/uncalled_for-0.2.0-py3-none-any.whl", hash = "sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f", size = 11351, upload-time = "2026-02-27T17:40:56.804Z" },
]
[[package]]
name = "uvicorn"
-version = "0.38.0"
+version = "0.41.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/f06b84e2697fef4688ca63bdb2fdf113ca0a3be33f94488f2cadb690b0cf/uvicorn-0.38.0.tar.gz", hash = "sha256:fd97093bdd120a2609fc0d3afe931d4d4ad688b6e75f0f929fde1bc36fe0e91d", size = 80605, upload-time = "2025-10-18T13:46:44.63Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/ee/d9/d88e73ca598f4f6ff671fb5fde8a32925c2e08a637303a1d12883c7305fa/uvicorn-0.38.0-py3-none-any.whl", hash = "sha256:48c0afd214ceb59340075b4a052ea1ee91c16fbc2a9b1469cca0e54566977b02", size = 68109, upload-time = "2025-10-18T13:46:42.958Z" },
+ { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" },
+]
+
+[[package]]
+name = "watchfiles"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a7/1a/206e8cf2dd86fddf939165a57b4df61607a1e0add2785f170a3f616b7d9f/watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c", size = 407318, upload-time = "2025-10-14T15:04:18.753Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/0f/abaf5262b9c496b5dad4ed3c0e799cbecb1f8ea512ecb6ddd46646a9fca3/watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43", size = 394478, upload-time = "2025-10-14T15:04:20.297Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/04/9cc0ba88697b34b755371f5ace8d3a4d9a15719c07bdc7bd13d7d8c6a341/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31", size = 449894, upload-time = "2025-10-14T15:04:21.527Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/9c/eda4615863cd8621e89aed4df680d8c3ec3da6a4cf1da113c17decd87c7f/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac", size = 459065, upload-time = "2025-10-14T15:04:22.795Z" },
+ { url = "https://files.pythonhosted.org/packages/84/13/f28b3f340157d03cbc8197629bc109d1098764abe1e60874622a0be5c112/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d", size = 488377, upload-time = "2025-10-14T15:04:24.138Z" },
+ { url = "https://files.pythonhosted.org/packages/86/93/cfa597fa9389e122488f7ffdbd6db505b3b915ca7435ecd7542e855898c2/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d", size = 595837, upload-time = "2025-10-14T15:04:25.057Z" },
+ { url = "https://files.pythonhosted.org/packages/57/1e/68c1ed5652b48d89fc24d6af905d88ee4f82fa8bc491e2666004e307ded1/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863", size = 473456, upload-time = "2025-10-14T15:04:26.497Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/dc/1a680b7458ffa3b14bb64878112aefc8f2e4f73c5af763cbf0bd43100658/watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab", size = 455614, upload-time = "2025-10-14T15:04:27.539Z" },
+ { url = "https://files.pythonhosted.org/packages/61/a5/3d782a666512e01eaa6541a72ebac1d3aae191ff4a31274a66b8dd85760c/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82", size = 630690, upload-time = "2025-10-14T15:04:28.495Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/73/bb5f38590e34687b2a9c47a244aa4dd50c56a825969c92c9c5fc7387cea1/watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4", size = 622459, upload-time = "2025-10-14T15:04:29.491Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/ac/c9bb0ec696e07a20bd58af5399aeadaef195fb2c73d26baf55180fe4a942/watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844", size = 272663, upload-time = "2025-10-14T15:04:30.435Z" },
+ { url = "https://files.pythonhosted.org/packages/11/a0/a60c5a7c2ec59fa062d9a9c61d02e3b6abd94d32aac2d8344c4bdd033326/watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e", size = 287453, upload-time = "2025-10-14T15:04:31.53Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" },
+ { url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" },
+ { url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" },
+ { url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" },
+ { url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" },
+ { url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" },
+ { url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" },
+ { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" },
+ { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" },
+ { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" },
+ { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" },
+ { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" },
+ { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" },
+ { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" },
+ { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" },
+ { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" },
+ { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" },
+ { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" },
+ { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" },
+ { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" },
+ { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" },
+ { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" },
+ { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" },
+ { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" },
+ { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" },
+ { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" },
+ { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" },
+ { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" },
+ { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" },
+ { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" },
+ { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/4c/a888c91e2e326872fa4705095d64acd8aa2fb9c1f7b9bd0588f33850516c/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3", size = 409611, upload-time = "2025-10-14T15:06:05.809Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/c7/5420d1943c8e3ce1a21c0a9330bcf7edafb6aa65d26b21dbb3267c9e8112/watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2", size = 396889, upload-time = "2025-10-14T15:06:07.035Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/e5/0072cef3804ce8d3aaddbfe7788aadff6b3d3f98a286fdbee9fd74ca59a7/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d", size = 451616, upload-time = "2025-10-14T15:06:08.072Z" },
+ { url = "https://files.pythonhosted.org/packages/83/4e/b87b71cbdfad81ad7e83358b3e447fedd281b880a03d64a760fe0a11fc2e/watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b", size = 458413, upload-time = "2025-10-14T15:06:09.209Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" },
]
[[package]]
name = "websockets"
-version = "15.0.1"
+version = "16.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1e/da/6462a9f510c0c49837bbc9345aca92d767a56c1fb2939e1579df1e1cdcf7/websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b", size = 175423, upload-time = "2025-03-05T20:01:35.363Z" },
- { url = "https://files.pythonhosted.org/packages/1c/9f/9d11c1a4eb046a9e106483b9ff69bce7ac880443f00e5ce64261b47b07e7/websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205", size = 173080, upload-time = "2025-03-05T20:01:37.304Z" },
- { url = "https://files.pythonhosted.org/packages/d5/4f/b462242432d93ea45f297b6179c7333dd0402b855a912a04e7fc61c0d71f/websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a", size = 173329, upload-time = "2025-03-05T20:01:39.668Z" },
- { url = "https://files.pythonhosted.org/packages/6e/0c/6afa1f4644d7ed50284ac59cc70ef8abd44ccf7d45850d989ea7310538d0/websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e", size = 182312, upload-time = "2025-03-05T20:01:41.815Z" },
- { url = "https://files.pythonhosted.org/packages/dd/d4/ffc8bd1350b229ca7a4db2a3e1c482cf87cea1baccd0ef3e72bc720caeec/websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf", size = 181319, upload-time = "2025-03-05T20:01:43.967Z" },
- { url = "https://files.pythonhosted.org/packages/97/3a/5323a6bb94917af13bbb34009fac01e55c51dfde354f63692bf2533ffbc2/websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb", size = 181631, upload-time = "2025-03-05T20:01:46.104Z" },
- { url = "https://files.pythonhosted.org/packages/a6/cc/1aeb0f7cee59ef065724041bb7ed667b6ab1eeffe5141696cccec2687b66/websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d", size = 182016, upload-time = "2025-03-05T20:01:47.603Z" },
- { url = "https://files.pythonhosted.org/packages/79/f9/c86f8f7af208e4161a7f7e02774e9d0a81c632ae76db2ff22549e1718a51/websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9", size = 181426, upload-time = "2025-03-05T20:01:48.949Z" },
- { url = "https://files.pythonhosted.org/packages/c7/b9/828b0bc6753db905b91df6ae477c0b14a141090df64fb17f8a9d7e3516cf/websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c", size = 181360, upload-time = "2025-03-05T20:01:50.938Z" },
- { url = "https://files.pythonhosted.org/packages/89/fb/250f5533ec468ba6327055b7d98b9df056fb1ce623b8b6aaafb30b55d02e/websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256", size = 176388, upload-time = "2025-03-05T20:01:52.213Z" },
- { url = "https://files.pythonhosted.org/packages/1c/46/aca7082012768bb98e5608f01658ff3ac8437e563eca41cf068bd5849a5e/websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41", size = 176830, upload-time = "2025-03-05T20:01:53.922Z" },
- { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
- { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
- { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
- { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
- { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
- { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
- { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
- { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
- { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
- { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
- { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
- { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
- { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
- { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
- { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
- { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
- { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
- { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
- { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
- { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
- { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
- { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
- { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
- { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
- { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
- { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
- { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
- { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
- { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
- { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
- { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
- { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
- { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
- { url = "https://files.pythonhosted.org/packages/02/9e/d40f779fa16f74d3468357197af8d6ad07e7c5a27ea1ca74ceb38986f77a/websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3", size = 173109, upload-time = "2025-03-05T20:03:17.769Z" },
- { url = "https://files.pythonhosted.org/packages/bc/cd/5b887b8585a593073fd92f7c23ecd3985cd2c3175025a91b0d69b0551372/websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1", size = 173343, upload-time = "2025-03-05T20:03:19.094Z" },
- { url = "https://files.pythonhosted.org/packages/fe/ae/d34f7556890341e900a95acf4886833646306269f899d58ad62f588bf410/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475", size = 174599, upload-time = "2025-03-05T20:03:21.1Z" },
- { url = "https://files.pythonhosted.org/packages/71/e6/5fd43993a87db364ec60fc1d608273a1a465c0caba69176dd160e197ce42/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9", size = 174207, upload-time = "2025-03-05T20:03:23.221Z" },
- { url = "https://files.pythonhosted.org/packages/2b/fb/c492d6daa5ec067c2988ac80c61359ace5c4c674c532985ac5a123436cec/websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04", size = 174155, upload-time = "2025-03-05T20:03:25.321Z" },
- { url = "https://files.pythonhosted.org/packages/68/a1/dcb68430b1d00b698ae7a7e0194433bce4f07ded185f0ee5fb21e2a2e91e/websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122", size = 176884, upload-time = "2025-03-05T20:03:27.934Z" },
- { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" },
-]
-
-[[package]]
-name = "wrapt"
-version = "1.17.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/3f/23/bb82321b86411eb51e5a5db3fb8f8032fd30bd7c2d74bfe936136b2fa1d6/wrapt-1.17.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88bbae4d40d5a46142e70d58bf664a89b6b4befaea7b2ecc14e03cedb8e06c04", size = 53482, upload-time = "2025-08-12T05:51:44.467Z" },
- { url = "https://files.pythonhosted.org/packages/45/69/f3c47642b79485a30a59c63f6d739ed779fb4cc8323205d047d741d55220/wrapt-1.17.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6b13af258d6a9ad602d57d889f83b9d5543acd471eee12eb51f5b01f8eb1bc2", size = 38676, upload-time = "2025-08-12T05:51:32.636Z" },
- { url = "https://files.pythonhosted.org/packages/d1/71/e7e7f5670c1eafd9e990438e69d8fb46fa91a50785332e06b560c869454f/wrapt-1.17.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd341868a4b6714a5962c1af0bd44f7c404ef78720c7de4892901e540417111c", size = 38957, upload-time = "2025-08-12T05:51:54.655Z" },
- { url = "https://files.pythonhosted.org/packages/de/17/9f8f86755c191d6779d7ddead1a53c7a8aa18bccb7cea8e7e72dfa6a8a09/wrapt-1.17.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f9b2601381be482f70e5d1051a5965c25fb3625455a2bf520b5a077b22afb775", size = 81975, upload-time = "2025-08-12T05:52:30.109Z" },
- { url = "https://files.pythonhosted.org/packages/f2/15/dd576273491f9f43dd09fce517f6c2ce6eb4fe21681726068db0d0467096/wrapt-1.17.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:343e44b2a8e60e06a7e0d29c1671a0d9951f59174f3709962b5143f60a2a98bd", size = 83149, upload-time = "2025-08-12T05:52:09.316Z" },
- { url = "https://files.pythonhosted.org/packages/0c/c4/5eb4ce0d4814521fee7aa806264bf7a114e748ad05110441cd5b8a5c744b/wrapt-1.17.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:33486899acd2d7d3066156b03465b949da3fd41a5da6e394ec49d271baefcf05", size = 82209, upload-time = "2025-08-12T05:52:10.331Z" },
- { url = "https://files.pythonhosted.org/packages/31/4b/819e9e0eb5c8dc86f60dfc42aa4e2c0d6c3db8732bce93cc752e604bb5f5/wrapt-1.17.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e6f40a8aa5a92f150bdb3e1c44b7e98fb7113955b2e5394122fa5532fec4b418", size = 81551, upload-time = "2025-08-12T05:52:31.137Z" },
- { url = "https://files.pythonhosted.org/packages/f8/83/ed6baf89ba3a56694700139698cf703aac9f0f9eb03dab92f57551bd5385/wrapt-1.17.3-cp310-cp310-win32.whl", hash = "sha256:a36692b8491d30a8c75f1dfee65bef119d6f39ea84ee04d9f9311f83c5ad9390", size = 36464, upload-time = "2025-08-12T05:53:01.204Z" },
- { url = "https://files.pythonhosted.org/packages/2f/90/ee61d36862340ad7e9d15a02529df6b948676b9a5829fd5e16640156627d/wrapt-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:afd964fd43b10c12213574db492cb8f73b2f0826c8df07a68288f8f19af2ebe6", size = 38748, upload-time = "2025-08-12T05:53:00.209Z" },
- { url = "https://files.pythonhosted.org/packages/bd/c3/cefe0bd330d389c9983ced15d326f45373f4073c9f4a8c2f99b50bfea329/wrapt-1.17.3-cp310-cp310-win_arm64.whl", hash = "sha256:af338aa93554be859173c39c85243970dc6a289fa907402289eeae7543e1ae18", size = 36810, upload-time = "2025-08-12T05:52:51.906Z" },
- { url = "https://files.pythonhosted.org/packages/52/db/00e2a219213856074a213503fdac0511203dceefff26e1daa15250cc01a0/wrapt-1.17.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:273a736c4645e63ac582c60a56b0acb529ef07f78e08dc6bfadf6a46b19c0da7", size = 53482, upload-time = "2025-08-12T05:51:45.79Z" },
- { url = "https://files.pythonhosted.org/packages/5e/30/ca3c4a5eba478408572096fe9ce36e6e915994dd26a4e9e98b4f729c06d9/wrapt-1.17.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5531d911795e3f935a9c23eb1c8c03c211661a5060aab167065896bbf62a5f85", size = 38674, upload-time = "2025-08-12T05:51:34.629Z" },
- { url = "https://files.pythonhosted.org/packages/31/25/3e8cc2c46b5329c5957cec959cb76a10718e1a513309c31399a4dad07eb3/wrapt-1.17.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0610b46293c59a3adbae3dee552b648b984176f8562ee0dba099a56cfbe4df1f", size = 38959, upload-time = "2025-08-12T05:51:56.074Z" },
- { url = "https://files.pythonhosted.org/packages/5d/8f/a32a99fc03e4b37e31b57cb9cefc65050ea08147a8ce12f288616b05ef54/wrapt-1.17.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b32888aad8b6e68f83a8fdccbf3165f5469702a7544472bdf41f582970ed3311", size = 82376, upload-time = "2025-08-12T05:52:32.134Z" },
- { url = "https://files.pythonhosted.org/packages/31/57/4930cb8d9d70d59c27ee1332a318c20291749b4fba31f113c2f8ac49a72e/wrapt-1.17.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cccf4f81371f257440c88faed6b74f1053eef90807b77e31ca057b2db74edb1", size = 83604, upload-time = "2025-08-12T05:52:11.663Z" },
- { url = "https://files.pythonhosted.org/packages/a8/f3/1afd48de81d63dd66e01b263a6fbb86e1b5053b419b9b33d13e1f6d0f7d0/wrapt-1.17.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8a210b158a34164de8bb68b0e7780041a903d7b00c87e906fb69928bf7890d5", size = 82782, upload-time = "2025-08-12T05:52:12.626Z" },
- { url = "https://files.pythonhosted.org/packages/1e/d7/4ad5327612173b144998232f98a85bb24b60c352afb73bc48e3e0d2bdc4e/wrapt-1.17.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:79573c24a46ce11aab457b472efd8d125e5a51da2d1d24387666cd85f54c05b2", size = 82076, upload-time = "2025-08-12T05:52:33.168Z" },
- { url = "https://files.pythonhosted.org/packages/bb/59/e0adfc831674a65694f18ea6dc821f9fcb9ec82c2ce7e3d73a88ba2e8718/wrapt-1.17.3-cp311-cp311-win32.whl", hash = "sha256:c31eebe420a9a5d2887b13000b043ff6ca27c452a9a22fa71f35f118e8d4bf89", size = 36457, upload-time = "2025-08-12T05:53:03.936Z" },
- { url = "https://files.pythonhosted.org/packages/83/88/16b7231ba49861b6f75fc309b11012ede4d6b0a9c90969d9e0db8d991aeb/wrapt-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:0b1831115c97f0663cb77aa27d381237e73ad4f721391a9bfb2fe8bc25fa6e77", size = 38745, upload-time = "2025-08-12T05:53:02.885Z" },
- { url = "https://files.pythonhosted.org/packages/9a/1e/c4d4f3398ec073012c51d1c8d87f715f56765444e1a4b11e5180577b7e6e/wrapt-1.17.3-cp311-cp311-win_arm64.whl", hash = "sha256:5a7b3c1ee8265eb4c8f1b7d29943f195c00673f5ab60c192eba2d4a7eae5f46a", size = 36806, upload-time = "2025-08-12T05:52:53.368Z" },
- { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" },
- { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" },
- { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" },
- { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" },
- { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" },
- { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" },
- { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" },
- { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" },
- { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" },
- { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" },
- { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" },
- { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" },
- { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" },
- { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" },
- { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" },
- { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" },
- { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" },
- { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" },
- { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" },
- { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" },
- { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" },
- { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" },
- { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" },
- { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" },
- { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" },
- { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" },
- { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" },
- { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" },
- { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" },
- { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" },
- { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" },
- { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" },
- { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" },
- { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" },
- { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" },
- { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" },
- { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" },
- { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" },
- { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" },
- { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" },
- { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
+ { url = "https://files.pythonhosted.org/packages/20/74/221f58decd852f4b59cc3354cccaf87e8ef695fede361d03dc9a7396573b/websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a", size = 177343, upload-time = "2026-01-10T09:22:21.28Z" },
+ { url = "https://files.pythonhosted.org/packages/19/0f/22ef6107ee52ab7f0b710d55d36f5a5d3ef19e8a205541a6d7ffa7994e5a/websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0", size = 175021, upload-time = "2026-01-10T09:22:22.696Z" },
+ { url = "https://files.pythonhosted.org/packages/10/40/904a4cb30d9b61c0e278899bf36342e9b0208eb3c470324a9ecbaac2a30f/websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957", size = 175320, upload-time = "2026-01-10T09:22:23.94Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/2f/4b3ca7e106bc608744b1cdae041e005e446124bebb037b18799c2d356864/websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72", size = 183815, upload-time = "2026-01-10T09:22:25.469Z" },
+ { url = "https://files.pythonhosted.org/packages/86/26/d40eaa2a46d4302becec8d15b0fc5e45bdde05191e7628405a19cf491ccd/websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde", size = 185054, upload-time = "2026-01-10T09:22:27.101Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/ba/6500a0efc94f7373ee8fefa8c271acdfd4dca8bd49a90d4be7ccabfc397e/websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3", size = 184565, upload-time = "2026-01-10T09:22:28.293Z" },
+ { url = "https://files.pythonhosted.org/packages/04/b4/96bf2cee7c8d8102389374a2616200574f5f01128d1082f44102140344cc/websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3", size = 183848, upload-time = "2026-01-10T09:22:30.394Z" },
+ { url = "https://files.pythonhosted.org/packages/02/8e/81f40fb00fd125357814e8c3025738fc4ffc3da4b6b4a4472a82ba304b41/websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9", size = 178249, upload-time = "2026-01-10T09:22:32.083Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/5f/7e40efe8df57db9b91c88a43690ac66f7b7aa73a11aa6a66b927e44f26fa/websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35", size = 178685, upload-time = "2026-01-10T09:22:33.345Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" },
+ { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" },
+ { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" },
+ { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" },
+ { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" },
+ { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" },
+ { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" },
+ { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" },
+ { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" },
+ { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" },
+ { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" },
+ { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" },
+ { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" },
+ { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" },
+ { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" },
+ { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" },
+ { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" },
+ { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
+ { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" },
+ { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
]
[[package]]
diff --git a/examples/text_me.py b/examples/text_me.py
index f45cca688..2a90f06d4 100644
--- a/examples/text_me.py
+++ b/examples/text_me.py
@@ -20,7 +20,7 @@ Visit https://surgemsg.com/ and click "Get Started" to obtain these values.
from typing import Annotated
-import httpx
+import httpx2
from pydantic import BeforeValidator
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -28,9 +28,7 @@ from fastmcp import FastMCP
class SurgeSettings(BaseSettings):
- model_config: SettingsConfigDict = SettingsConfigDict(
- env_prefix="SURGE_", env_file=".env"
- )
+ model_config = SettingsConfigDict(env_prefix="SURGE_", env_file=".env")
api_key: str
account_id: str
@@ -43,13 +41,13 @@ class SurgeSettings(BaseSettings):
# Create server
mcp = FastMCP("Text me")
-surge_settings = SurgeSettings() # type: ignore
+surge_settings = SurgeSettings() # type: ignore[call-arg]
@mcp.tool(name="textme", description="Send a text message to me")
def text_me(text_content: str) -> str:
"""Send a text message to a phone number via https://surgemsg.com/"""
- with httpx.Client() as client:
+ with httpx2.Client() as client:
response = client.post(
"https://api.surgemsg.com/messages",
headers={
diff --git a/examples/tool_result_echo.py b/examples/tool_result_echo.py
index bd1185f29..54ed151de 100644
--- a/examples/tool_result_echo.py
+++ b/examples/tool_result_echo.py
@@ -10,7 +10,7 @@ import time
from dataclasses import dataclass
from fastmcp import FastMCP
-from fastmcp.tools.tool import ToolResult
+from fastmcp.tools import ToolResult
mcp = FastMCP("Echo Server")
diff --git a/fastmcp_remote/README.md b/fastmcp_remote/README.md
new file mode 100644
index 000000000..7f576eb70
--- /dev/null
+++ b/fastmcp_remote/README.md
@@ -0,0 +1,105 @@
+# fastmcp-remote
+
+`fastmcp-remote` is FastMCP's standalone Python stdio bridge for remote MCP servers. It lets MCP clients that launch local stdio processes connect to MCP servers hosted over Streamable HTTP or SSE.
+
+```json
+{
+ "mcpServers": {
+ "linear": {
+ "command": "uvx",
+ "args": ["fastmcp-remote", "https://mcp.linear.app/mcp"]
+ }
+ }
+}
+```
+
+The CLI is powered by [FastMCP](https://gofastmcp.com). Its command shape is inspired by the original [`mcp-remote`](https://github.com/geelen/mcp-remote) npm project, which established the stdio-to-remote bridge pattern used across the MCP ecosystem.
+
+`fastmcp-remote` is intentionally smaller than the general FastMCP CLI. It does not load Python files, discover local MCP configs, prepare project environments, or run development reload loops. It builds one FastMCP client for the URL you provide, exposes that client as a local stdio proxy, and leaves the rest alone.
+
+## Usage
+
+Run a remote MCP server through a local stdio bridge:
+
+```bash
+uvx fastmcp-remote https://example.com/mcp
+```
+
+Use the full MCP endpoint URL for the remote server. Many FastMCP HTTP servers expose MCP at `/mcp`, so a local development server may need `http://localhost:8000/mcp` rather than `http://localhost:8000`.
+
+`fastmcp-remote` starts a local stdio bridge, then connects to the upstream server when the MCP host initializes that bridge. If the upstream server is unavailable, the URL does not point to an MCP endpoint, or authentication cannot complete, initialization fails and the host should report the remote server as failed.
+
+For authenticated MCP servers, OAuth is enabled automatically. To pass a bearer token or other custom header instead, provide a header. The header name ends at the first colon, so values can contain additional colons. Quote the header when the value contains spaces, just like any other shell argument:
+
+```bash
+uvx fastmcp-remote https://example.com/mcp \
+ --header "Authorization: Bearer "
+```
+
+Repeat `--header` to send multiple headers. Header values use `Name: Value` format:
+
+```bash
+uvx fastmcp-remote https://example.com/mcp \
+ --header "Authorization: Bearer " \
+ --header "X-Workspace: production" \
+ --header "X-Client-Name: My MCP Host" \
+ --header "X-Callback-Url: https://example.com/oauth/callback"
+```
+
+Some MCP hosts on Windows have trouble preserving spaces inside command arguments. Put the spaced value in an environment variable and reference it from the header value:
+
+```json
+{
+ "mcpServers": {
+ "remote-api": {
+ "command": "uvx",
+ "args": [
+ "fastmcp-remote",
+ "https://example.com/mcp",
+ "--header",
+ "Authorization:${AUTH_HEADER}"
+ ],
+ "env": {
+ "AUTH_HEADER": "Bearer "
+ }
+ }
+ }
+}
+```
+
+Use `--auth none` for unauthenticated development servers:
+
+```bash
+uvx fastmcp-remote http://localhost:8000/mcp --auth none
+```
+
+For servers behind a self-signed certificate, point `--verify` at a CA bundle that trusts the certificate:
+
+```bash
+uvx fastmcp-remote https://internal.example.com/mcp --verify /path/to/ca-bundle.pem
+```
+
+To disable certificate verification entirely (insecure, only for trusted private networks), pass `--verify false`:
+
+```bash
+uvx fastmcp-remote https://internal.example.com/mcp --verify false
+```
+
+A CA bundle can also be supplied through the standard `SSL_CERT_FILE` environment variable, which OpenSSL reads automatically:
+
+```bash
+SSL_CERT_FILE=/path/to/ca-bundle.pem uvx fastmcp-remote https://internal.example.com/mcp
+```
+
+## Options
+
+- `--transport`: Choose `http` or `sse`. Defaults to `http`.
+- `--header`: Add a header to upstream requests, for example `--header "Authorization: Bearer "`. Values may contain colons. Quote headers whose values contain spaces. Use `${VAR}` to expand environment variables inside values. Repeat for multiple headers.
+- `--resource`: Isolate OAuth token storage for a named remote resource.
+- `--host`: Set the OAuth callback hostname. Defaults to `localhost`.
+- `--auth-timeout`: Set how long to wait for the OAuth callback. Defaults to 300 seconds.
+- `--ignore-tool`: Hide tools whose names match a glob pattern.
+- `--auth`: Choose `oauth` or `none`. The default uses OAuth unless an `Authorization` header is provided.
+- `--verify`: Control TLS certificate verification. Pass a path to a CA bundle to trust a self-signed certificate, or `false` to disable verification (insecure). Defaults to verification enabled.
+
+OAuth tokens are stored under `~/.fastmcp/remote` by default. Set `FASTMCP_REMOTE_CONFIG_DIR` to use another directory.
diff --git a/fastmcp_remote/fastmcp_remote/__init__.py b/fastmcp_remote/fastmcp_remote/__init__.py
new file mode 100644
index 000000000..a8af334a2
--- /dev/null
+++ b/fastmcp_remote/fastmcp_remote/__init__.py
@@ -0,0 +1,10 @@
+"""Python stdio bridge for remote MCP servers."""
+
+from importlib.metadata import PackageNotFoundError, version
+
+try:
+ __version__ = version("fastmcp-remote")
+except PackageNotFoundError:
+ __version__ = "0.0.0"
+
+__all__ = ["__version__"]
diff --git a/fastmcp_remote/fastmcp_remote/cli.py b/fastmcp_remote/fastmcp_remote/cli.py
new file mode 100644
index 000000000..6c917eef9
--- /dev/null
+++ b/fastmcp_remote/fastmcp_remote/cli.py
@@ -0,0 +1,280 @@
+from __future__ import annotations
+
+import argparse
+import fnmatch
+import hashlib
+import os
+import re
+from collections.abc import Sequence
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Literal
+from urllib.parse import urlparse
+
+import anyio
+from key_value.aio.protocols import AsyncKeyValue
+from key_value.aio.stores.filetree import (
+ FileTreeStore,
+ FileTreeV1CollectionSanitizationStrategy,
+ FileTreeV1KeySanitizationStrategy,
+)
+
+from fastmcp import Client
+from fastmcp.client.auth import OAuth
+from fastmcp.client.transports import SSETransport, StreamableHttpTransport
+from fastmcp.server import create_proxy
+from fastmcp.server.transforms import GetToolNext, Transform
+from fastmcp.tools import Tool
+from fastmcp.utilities.versions import VersionSpec
+
+RemoteTransport = Literal["http", "sse"]
+AuthMode = Literal["oauth", "none"]
+ENV_VAR_PATTERN = re.compile(r"\$\{([A-Za-z_][A-Za-z0-9_]*)\}")
+
+
+@dataclass(frozen=True)
+class RemoteConfig:
+ url: str
+ headers: dict[str, str]
+ transport: RemoteTransport
+ auth: AuthMode | None
+ callback_port: int | None
+ callback_host: str
+ callback_timeout: float
+ storage_dir: Path
+ ignore_tools: tuple[str, ...]
+ show_banner: bool
+ log_level: str | None
+ verify: bool | str | None
+
+
+class IgnoreTools(Transform):
+ def __init__(self, patterns: Sequence[str]) -> None:
+ self.patterns = tuple(patterns)
+
+ def _matches(self, name: str) -> bool:
+ return any(fnmatch.fnmatchcase(name, pattern) for pattern in self.patterns)
+
+ async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
+ return [tool for tool in tools if not self._matches(tool.name)]
+
+ async def get_tool(
+ self,
+ name: str,
+ call_next: GetToolNext,
+ *,
+ version: VersionSpec | None = None,
+ ) -> Tool | None:
+ if self._matches(name):
+ return None
+ return await call_next(name, version=version)
+
+
+def parse_header(value: str) -> tuple[str, str]:
+ name, separator, header_value = value.partition(":")
+ if not separator or not name.strip():
+ raise argparse.ArgumentTypeError("Headers must use the format 'Name: Value'.")
+ try:
+ expanded_value = ENV_VAR_PATTERN.sub(
+ lambda match: os.environ[match.group(1)], header_value
+ )
+ except KeyError as exc:
+ raise argparse.ArgumentTypeError(
+ f"Environment variable {exc.args[0]} is not set."
+ ) from exc
+ return name.strip(), expanded_value.strip()
+
+
+def parse_verify(value: str) -> bool | str:
+ """Interpret the --verify value as a boolean toggle or a CA bundle path."""
+ lowered = value.strip().lower()
+ if lowered in {"false", "0", "no", "off"}:
+ return False
+ if lowered in {"true", "1", "yes", "on"}:
+ return True
+ return value
+
+
+def default_storage_dir(resource: str | None = None) -> Path:
+ if config_dir := os.environ.get("FASTMCP_REMOTE_CONFIG_DIR"):
+ base = Path(config_dir).expanduser()
+ else:
+ base = Path.home() / ".fastmcp" / "remote"
+ if resource is None:
+ return base
+ digest = hashlib.sha256(resource.encode()).hexdigest()[:16]
+ return base / "resources" / digest
+
+
+def build_parser() -> argparse.ArgumentParser:
+ parser = argparse.ArgumentParser(
+ prog="fastmcp-remote",
+ description="Bridge a remote MCP server to a local stdio MCP process.",
+ )
+ parser.add_argument("url", help="Remote MCP server URL.")
+ parser.add_argument(
+ "callback_port",
+ nargs="?",
+ type=int,
+ help="OAuth callback port. Defaults to an available local port.",
+ )
+ parser.add_argument(
+ "--transport",
+ choices=["http", "sse"],
+ default="http",
+ help="Remote transport. Defaults to http.",
+ )
+ parser.add_argument(
+ "--header",
+ action="append",
+ default=[],
+ type=parse_header,
+ help="Header to send upstream, in 'Name: Value' form. Repeat for multiple headers.",
+ )
+ parser.add_argument(
+ "--auth",
+ choices=["oauth", "none"],
+ default=None,
+ help="Authentication mode. Defaults to OAuth unless Authorization is provided.",
+ )
+ parser.add_argument(
+ "--resource",
+ help="Resource identifier used to isolate OAuth token storage.",
+ )
+ parser.add_argument(
+ "--host",
+ default="localhost",
+ help="OAuth callback hostname. Defaults to localhost.",
+ )
+ parser.add_argument(
+ "--auth-timeout",
+ type=float,
+ default=300.0,
+ help="Seconds to wait for the OAuth callback. Defaults to 300.",
+ )
+ parser.add_argument(
+ "--ignore-tool",
+ action="append",
+ default=[],
+ help="Hide tools matching this glob pattern. Repeat for multiple patterns.",
+ )
+ parser.add_argument(
+ "--verify",
+ type=parse_verify,
+ default=None,
+ metavar="VERIFY",
+ help=(
+ "SSL certificate verification. Pass a path to a CA bundle file, or "
+ "'false' to disable verification (insecure, for self-signed "
+ "certificates). Defaults to verification enabled."
+ ),
+ )
+ parser.add_argument(
+ "--debug",
+ action="store_true",
+ help="Enable debug logging.",
+ )
+ parser.add_argument(
+ "--silent",
+ action="store_true",
+ help="Suppress non-critical logs.",
+ )
+ return parser
+
+
+def parse_args(argv: Sequence[str] | None = None) -> RemoteConfig:
+ parser = build_parser()
+ args = parser.parse_args(argv)
+
+ parsed_url = urlparse(args.url)
+ if parsed_url.scheme not in {"http", "https"}:
+ parser.error("The remote MCP server URL must start with http:// or https://.")
+
+ headers = dict(args.header)
+ if args.silent and args.debug:
+ parser.error("--silent and --debug cannot be used together.")
+ if args.auth_timeout <= 0:
+ parser.error("--auth-timeout must be greater than 0.")
+
+ log_level = "DEBUG" if args.debug else None
+ if args.silent:
+ log_level = "CRITICAL"
+
+ return RemoteConfig(
+ url=args.url,
+ headers=headers,
+ transport=args.transport,
+ auth=args.auth,
+ callback_port=args.callback_port,
+ callback_host=args.host,
+ callback_timeout=args.auth_timeout,
+ storage_dir=default_storage_dir(args.resource),
+ ignore_tools=tuple(args.ignore_tool),
+ show_banner=not args.silent,
+ log_level=log_level,
+ verify=args.verify,
+ )
+
+
+def build_token_storage(storage_dir: Path) -> AsyncKeyValue:
+ storage_dir.mkdir(parents=True, exist_ok=True)
+ return FileTreeStore(
+ data_directory=storage_dir,
+ key_sanitization_strategy=FileTreeV1KeySanitizationStrategy(storage_dir),
+ collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(
+ storage_dir
+ ),
+ )
+
+
+def resolve_auth(config: RemoteConfig) -> OAuth | None:
+ authorization_header = any(
+ name.lower() == "authorization" for name in config.headers
+ )
+ auth_mode = config.auth
+ if auth_mode is None and authorization_header:
+ auth_mode = "none"
+ elif auth_mode is None:
+ auth_mode = "oauth"
+
+ if auth_mode == "none":
+ return None
+
+ return OAuth(
+ token_storage=build_token_storage(config.storage_dir),
+ callback_port=config.callback_port,
+ callback_host=config.callback_host,
+ callback_timeout=config.callback_timeout,
+ )
+
+
+def build_transport(config: RemoteConfig) -> SSETransport | StreamableHttpTransport:
+ auth = resolve_auth(config)
+ if config.transport == "sse":
+ return SSETransport(
+ config.url, headers=config.headers, auth=auth, verify=config.verify
+ )
+ return StreamableHttpTransport(
+ config.url, headers=config.headers, auth=auth, verify=config.verify
+ )
+
+
+async def run(config: RemoteConfig) -> None:
+ client = Client(build_transport(config))
+ server = create_proxy(
+ client,
+ name="fastmcp-remote",
+ provider_error_strategy="raise",
+ )
+ if config.ignore_tools:
+ server.add_transform(IgnoreTools(config.ignore_tools))
+ await server.run_async(
+ transport="stdio",
+ show_banner=config.show_banner,
+ log_level=config.log_level,
+ )
+
+
+def main(argv: Sequence[str] | None = None) -> None:
+ config = parse_args(argv)
+ anyio.run(run, config)
diff --git a/src/fastmcp/experimental/transforms/__init__.py b/fastmcp_remote/fastmcp_remote/py.typed
similarity index 100%
rename from src/fastmcp/experimental/transforms/__init__.py
rename to fastmcp_remote/fastmcp_remote/py.typed
diff --git a/fastmcp_remote/pyproject.toml b/fastmcp_remote/pyproject.toml
new file mode 100644
index 000000000..b75259529
--- /dev/null
+++ b/fastmcp_remote/pyproject.toml
@@ -0,0 +1,62 @@
+[project]
+name = "fastmcp-remote"
+dynamic = ["version", "dependencies"]
+description = "A Python stdio bridge for remote MCP servers, powered by FastMCP."
+authors = [{ name = "Jeremiah Lowin" }]
+
+requires-python = ">=3.10"
+readme = "README.md"
+license = "Apache-2.0"
+
+keywords = [
+ "mcp",
+ "fastmcp remote",
+ "mcp remote",
+ "model context protocol",
+ "fastmcp",
+ "stdio",
+ "oauth",
+]
+classifiers = [
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Apache Software License",
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Typing :: Typed",
+]
+
+[project.urls]
+Homepage = "https://gofastmcp.com"
+Repository = "https://github.com/PrefectHQ/fastmcp"
+Documentation = "https://gofastmcp.com"
+"Original npm project" = "https://github.com/geelen/mcp-remote"
+
+[project.scripts]
+fastmcp-remote = "fastmcp_remote.cli:main"
+
+[build-system]
+requires = ["hatchling", "uv-dynamic-versioning>=0.7.0"]
+build-backend = "hatchling.build"
+
+[tool.hatch.version]
+source = "uv-dynamic-versioning"
+
+[tool.hatch.metadata]
+allow-direct-references = true
+
+[tool.hatch.build.targets.wheel]
+packages = ["fastmcp_remote"]
+
+[tool.uv-dynamic-versioning]
+vcs = "git"
+style = "pep440"
+bump = true
+fallback-version = "0.0.0"
+
+[tool.hatch.metadata.hooks.uv-dynamic-versioning]
+dependencies = [
+ "fastmcp-slim[client,server]=={{ version }}",
+]
diff --git a/fastmcp_slim/README.md b/fastmcp_slim/README.md
new file mode 100644
index 000000000..4a813d96b
--- /dev/null
+++ b/fastmcp_slim/README.md
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+# FastMCP 🚀
+
+
Move fast and make things.
+
+*Made with 💙 by [Prefect](https://www.prefect.io/)*
+
+[](https://gofastmcp.com)
+[](https://discord.gg/uu8dJCgttd)
+[](https://pypi.org/project/fastmcp)
+[](https://github.com/PrefectHQ/fastmcp/actions/workflows/run-tests.yml)
+[](https://github.com/PrefectHQ/fastmcp/blob/main/LICENSE)
+
+
+
+
+---
+
+The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) connects LLMs to tools and data. FastMCP gives you everything you need to go from prototype to production:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("Demo 🚀")
+
+@mcp.tool
+def add(a: int, b: int) -> int:
+ """Add two numbers"""
+ return a + b
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+## 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:
+
+
+
+
+
+
+Servers
+
+ Expose tools, resources, and prompts to LLMs.
+
+
+
+
+Apps
+
+ Give your tools interactive UIs rendered directly in the conversation.
+
+
+
+
+Clients
+
+ Connect to any MCP server — local or remote, programmatic or CLI.
+
+
+
+
+**[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.
+
+Ready to build? Start with the [installation guide](https://gofastmcp.com/getting-started/installation) or jump straight to the [quickstart](https://gofastmcp.com/getting-started/quickstart).
+
+## Run FastMCP in production with Horizon
+
+FastMCP is the standard way to build MCP servers. **[Prefect Horizon](https://www.prefect.io/horizon?utm_source=github&utm_medium=readme&utm_campaign=readme_horizon&utm_content=readme_body)** is the enterprise MCP gateway for running them safely.
+
+Built by the FastMCP team, Horizon packages the best practices we've learned shipping the world's most popular MCP framework.
+
+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 installing FastMCP with [uv](https://docs.astral.sh/uv/):
+
+```bash
+uv pip install fastmcp
+```
+
+For full installation instructions, including verification and upgrading, see the [**Installation Guide**](https://gofastmcp.com/getting-started/installation).
+
+**Upgrading?** We have guides for:
+- [Upgrading from FastMCP 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
+
+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.
+
+## Contributing
+
+We welcome contributions! See the [Contributing Guide](https://gofastmcp.com/development/contributing) for setup instructions, testing requirements, and PR guidelines.
diff --git a/fastmcp_slim/fastmcp/__init__.py b/fastmcp_slim/fastmcp/__init__.py
new file mode 100644
index 000000000..9d64f128e
--- /dev/null
+++ b/fastmcp_slim/fastmcp/__init__.py
@@ -0,0 +1,97 @@
+"""FastMCP - An ergonomic MCP interface."""
+
+import importlib
+import warnings
+from importlib.metadata import PackageNotFoundError, version as _version
+from typing import TYPE_CHECKING
+
+from fastmcp import _install_hints
+from fastmcp._warnings import FastMCPDeprecationWarning
+from fastmcp.settings import Settings
+from fastmcp.utilities.logging import configure_logging as _configure_logging
+
+if TYPE_CHECKING:
+ from fastmcp.client import Client as Client
+ from fastmcp.apps.app import FastMCPApp as FastMCPApp
+ from fastmcp.server.context import Context as Context
+ from fastmcp.server.server import FastMCP as FastMCP
+
+settings = Settings()
+if settings.log_enabled:
+ _configure_logging(
+ level=settings.log_level,
+ enable_rich_tracebacks=settings.enable_rich_tracebacks,
+ )
+
+# Install camelCase compatibility shims for MCP SDK v2's snake_case rename.
+# Installed unconditionally; each shim's getter checks the live
+# `mcp_camelcase_compat` setting at read time, so the bridge can be toggled at
+# runtime. Patches only mcp_types model classes, no client chain.
+from fastmcp import _compat
+
+_compat.install()
+
+try:
+ __version__ = _version("fastmcp-slim")
+except PackageNotFoundError:
+ __version__ = _version("fastmcp")
+
+if settings.deprecation_warnings:
+ warnings.simplefilter("default", FastMCPDeprecationWarning)
+
+
+# --- Lazy imports for performance (see #3292) ---
+# Client and the client submodule are deferred so that server-only users
+# don't pay for the client import chain. Do not convert back to top-level.
+
+
+def __getattr__(name: str) -> object:
+ if name == "Client":
+ try:
+ from fastmcp.client import Client
+ except ImportError as exc:
+ raise ImportError(_install_hints.CLIENT_SUPPORT) from exc
+
+ return Client
+ if name == "Context":
+ try:
+ from fastmcp.server.context import Context
+ except ImportError as exc:
+ raise ImportError(_install_hints.SERVER_SUPPORT) from exc
+
+ return Context
+ if name == "FastMCP":
+ try:
+ from fastmcp.server.server import FastMCP
+ except ImportError as exc:
+ raise ImportError(_install_hints.SERVER_SUPPORT) from exc
+
+ return FastMCP
+ if name == "FastMCPApp":
+ try:
+ from fastmcp.apps.app import FastMCPApp
+ except ImportError as exc:
+ raise ImportError(_install_hints.APP_SUPPORT) from exc
+
+ return FastMCPApp
+ if name == "client":
+ try:
+ return importlib.import_module("fastmcp.client")
+ except ImportError as exc:
+ raise ImportError(_install_hints.CLIENT_SUPPORT) from exc
+ if name == "server":
+ try:
+ return importlib.import_module("fastmcp.server")
+ except ImportError as exc:
+ raise ImportError(_install_hints.SERVER_SUPPORT) from exc
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+
+
+__all__ = [
+ "Client",
+ "Context",
+ "FastMCP",
+ "FastMCPApp",
+ "FastMCPDeprecationWarning",
+ "settings",
+]
diff --git a/fastmcp_slim/fastmcp/_compat.py b/fastmcp_slim/fastmcp/_compat.py
new file mode 100644
index 000000000..cefc67b49
--- /dev/null
+++ b/fastmcp_slim/fastmcp/_compat.py
@@ -0,0 +1,158 @@
+"""camelCase compatibility bridge for MCP SDK v2.
+
+MCP Python SDK v2 renamed protocol fields from camelCase (`inputSchema`) to
+snake_case (`input_schema`). FastMCP returns these SDK models directly from
+client calls, middleware hooks, and handler callbacks, so legacy user code that
+reads the old camelCase spellings would break.
+
+This module installs warn-once `@property` shims that route a small set of
+documented camelCase reads to their snake_case attributes. Only fields users
+actually read (per the docs boundary inventory) are bridged; each read emits a
+single `FastMCPDeprecationWarning` per (class, name) and returns the correct
+value. Installation is idempotent.
+
+The properties are installed unconditionally, but each getter checks the live
+`mcp_camelcase_compat` setting at read time: when the setting is enabled it
+warns and returns the snake_case value; when disabled it raises `AttributeError`
+exactly as if the property were never installed. This makes the setting a
+genuine runtime toggle (`fastmcp.settings.mcp_camelcase_compat = False` after
+import turns the bridge off) at negligible overhead.
+
+Guards ensure we never shadow a real upstream attribute: if a class already
+defines the camelCase name in its own `__dict__` or in its pydantic
+`model_fields`, we skip it. The property is a plain descriptor read, so values
+survive `model_copy`/`model_validate` (the underlying snake field is what gets
+copied/validated; the property reads through it every time).
+
+# TODO(sdk-v2-migration): remove once user code has migrated off camelCase reads.
+"""
+
+from __future__ import annotations
+
+import warnings
+
+import mcp_types
+
+from fastmcp._warnings import FastMCPDeprecationWarning
+
+# Map each SDK model class to the camelCase -> snake_case field reads we bridge.
+# Limited to fields FastMCP users actually read (docs boundary inventory).
+_ALIASES: dict[type, dict[str, str]] = {
+ mcp_types.Tool: {
+ "inputSchema": "input_schema",
+ "outputSchema": "output_schema",
+ },
+ mcp_types.ToolAnnotations: {
+ "readOnlyHint": "read_only_hint",
+ "destructiveHint": "destructive_hint",
+ "idempotentHint": "idempotent_hint",
+ "openWorldHint": "open_world_hint",
+ },
+ mcp_types.Resource: {
+ "mimeType": "mime_type",
+ },
+ mcp_types.ResourceTemplate: {
+ "mimeType": "mime_type",
+ "uriTemplate": "uri_template",
+ },
+ mcp_types.TextResourceContents: {
+ "mimeType": "mime_type",
+ },
+ mcp_types.BlobResourceContents: {
+ "mimeType": "mime_type",
+ },
+ mcp_types.ImageContent: {
+ "mimeType": "mime_type",
+ },
+ mcp_types.AudioContent: {
+ "mimeType": "mime_type",
+ },
+ mcp_types.CallToolResult: {
+ "isError": "is_error",
+ "structuredContent": "structured_content",
+ },
+ mcp_types.Completion: {
+ "hasMore": "has_more",
+ },
+ mcp_types.InitializeResult: {
+ "serverInfo": "server_info",
+ "protocolVersion": "protocol_version",
+ },
+ mcp_types.ListToolsResult: {
+ "nextCursor": "next_cursor",
+ },
+ mcp_types.ListResourcesResult: {
+ "nextCursor": "next_cursor",
+ },
+ mcp_types.ListResourceTemplatesResult: {
+ "nextCursor": "next_cursor",
+ "resourceTemplates": "resource_templates",
+ },
+ mcp_types.ListPromptsResult: {
+ "nextCursor": "next_cursor",
+ },
+ mcp_types.CreateMessageRequestParams: {
+ "systemPrompt": "system_prompt",
+ "maxTokens": "max_tokens",
+ "stopSequences": "stop_sequences",
+ "modelPreferences": "model_preferences",
+ "toolChoice": "tool_choice",
+ },
+ mcp_types.ElicitRequestFormParams: {
+ "requestedSchema": "requested_schema",
+ },
+}
+
+_installed = False
+
+
+def _make_property(cls_name: str, camel: str, snake: str) -> property:
+ """Build a warn-once property routing a camelCase read to a snake attr.
+
+ The getter reads the live `mcp_camelcase_compat` setting on every access: if
+ the bridge is disabled it raises `AttributeError` (matching the message
+ Python raises for a genuinely missing attribute) so the shim is transparent;
+ if enabled it warns once and returns the snake_case value.
+ """
+ warned = False
+
+ def getter(self: object) -> object:
+ nonlocal warned
+ import fastmcp
+
+ if not fastmcp.settings.mcp_camelcase_compat:
+ raise AttributeError(f"{cls_name!r} object has no attribute {camel!r}")
+ if not warned:
+ warned = True
+ warnings.warn(
+ f"Accessing `{cls_name}.{camel}` is deprecated; MCP SDK v2 "
+ f"renamed this field to `{snake}`. Update your code to read "
+ f"`.{snake}` instead.",
+ FastMCPDeprecationWarning,
+ stacklevel=2,
+ )
+ return getattr(self, snake)
+
+ return property(getter)
+
+
+def install() -> None:
+ """Install camelCase compatibility properties on SDK v2 model classes.
+
+ Idempotent. Each bridged read warns once per (class, name) and returns the
+ snake_case value. Skips any camelCase name a class already defines to avoid
+ shadowing real upstream attributes.
+ """
+ global _installed
+ if _installed:
+ return
+
+ for cls, mapping in _ALIASES.items():
+ model_fields = getattr(cls, "model_fields", {})
+ for camel, snake in mapping.items():
+ # Never shadow a real upstream attribute or field.
+ if camel in cls.__dict__ or camel in model_fields:
+ continue
+ setattr(cls, camel, _make_property(cls.__name__, camel, snake))
+
+ _installed = True
diff --git a/fastmcp_slim/fastmcp/_install_hints.py b/fastmcp_slim/fastmcp/_install_hints.py
new file mode 100644
index 000000000..89c25f490
--- /dev/null
+++ b/fastmcp_slim/fastmcp/_install_hints.py
@@ -0,0 +1,25 @@
+CLIENT_SUPPORT = (
+ "FastMCP client support is not installed. Install `fastmcp` or "
+ "`fastmcp-slim[client]`."
+)
+
+SERVER_SUPPORT = (
+ "FastMCP server support is not installed. Install `fastmcp` or "
+ "`fastmcp-slim[server]`."
+)
+
+APP_SUPPORT = (
+ "FastMCP app support is not installed. Install `fastmcp[apps]` or "
+ "`fastmcp-slim[server,apps]`."
+)
+
+CLI_SUPPORT = (
+ "FastMCP CLI support is not installed. Install `fastmcp` or `fastmcp-slim[server]`."
+)
+
+
+def full_package(feature: str) -> str:
+ return (
+ f"{feature} require the full `fastmcp` package. "
+ "Install it with `pip install fastmcp`."
+ )
diff --git a/fastmcp_slim/fastmcp/_warnings.py b/fastmcp_slim/fastmcp/_warnings.py
new file mode 100644
index 000000000..c63b97a5d
--- /dev/null
+++ b/fastmcp_slim/fastmcp/_warnings.py
@@ -0,0 +1,10 @@
+"""Warning types that can be imported without loading FastMCP's exception stack."""
+
+
+class FastMCPDeprecationWarning(DeprecationWarning):
+ """Deprecation warning for FastMCP APIs.
+
+ Subclass of DeprecationWarning so that standard warning filters
+ still apply, but FastMCP can selectively enable its own warnings
+ without affecting other libraries in the process.
+ """
diff --git a/fastmcp_slim/fastmcp/apps/__init__.py b/fastmcp_slim/fastmcp/apps/__init__.py
new file mode 100644
index 000000000..3c0ec32c6
--- /dev/null
+++ b/fastmcp_slim/fastmcp/apps/__init__.py
@@ -0,0 +1,42 @@
+"""FastMCP Apps — interactive UIs for MCP tools.
+
+This package contains the app-related components:
+
+- ``FastMCPApp`` — composable provider for interactive apps with backend tools
+- ``AppConfig`` — configuration for MCP App tools and resources
+- ``ResourceCSP`` / ``ResourcePermissions`` — security configuration
+"""
+
+from typing import TYPE_CHECKING as _TYPE_CHECKING
+
+from fastmcp.apps.config import AppConfig as AppConfig
+from fastmcp.apps.config import PrefabAppConfig as PrefabAppConfig
+from fastmcp.apps.config import ResourceCSP as ResourceCSP
+from fastmcp.apps.config import ResourcePermissions as ResourcePermissions
+from fastmcp.apps.config import UI_EXTENSION_ID as UI_EXTENSION_ID
+from fastmcp.apps.config import app_config_to_meta_dict as app_config_to_meta_dict
+from fastmcp.utilities.mime import UI_MIME_TYPE as UI_MIME_TYPE
+from fastmcp.utilities.mime import resolve_ui_mime_type as resolve_ui_mime_type
+
+__all__ = [
+ "UI_EXTENSION_ID",
+ "UI_MIME_TYPE",
+ "AppConfig",
+ "FastMCPApp",
+ "PrefabAppConfig",
+ "ResourceCSP",
+ "ResourcePermissions",
+ "app_config_to_meta_dict",
+ "resolve_ui_mime_type",
+]
+
+if _TYPE_CHECKING:
+ from fastmcp.apps.app import FastMCPApp as FastMCPApp
+
+
+def __getattr__(name: str) -> object:
+ if name == "FastMCPApp":
+ from fastmcp.apps.app import FastMCPApp
+
+ return FastMCPApp
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
diff --git a/fastmcp_slim/fastmcp/apps/app.py b/fastmcp_slim/fastmcp/apps/app.py
new file mode 100644
index 000000000..8a4dc64d0
--- /dev/null
+++ b/fastmcp_slim/fastmcp/apps/app.py
@@ -0,0 +1,450 @@
+"""FastMCPApp — a Provider that represents a composable MCP application.
+
+FastMCPApp binds entry-point tools (model calls these) together with backend
+tools (the UI calls these via CallTool). Backend tools are tagged with
+``meta["fastmcp"]["app"]`` so they can be found through the provider chain
+even when transforms (namespace, visibility, etc.) have renamed or hidden
+them — the server sets a context var that tells ``Provider.get_tool`` to
+fall back to a direct lookup for app-visible tools.
+
+Usage::
+
+ from fastmcp import FastMCP, FastMCPApp
+
+ app = FastMCPApp("Dashboard")
+
+ @app.ui()
+ def show_dashboard() -> Component:
+ return Column(...)
+
+ @app.tool()
+ def save_contact(name: str, email: str) -> str:
+ return name
+
+ server = FastMCP("Platform")
+ server.add_provider(app)
+"""
+
+from __future__ import annotations
+
+import inspect
+from collections.abc import AsyncIterator, Callable, Sequence
+from contextlib import asynccontextmanager
+from typing import TYPE_CHECKING, Any, Literal, TypeVar, overload
+
+from mcp_types import Icon, ToolAnnotations
+
+from fastmcp.server.providers.base import Provider
+from fastmcp.utilities.authorization import AuthCheck
+from fastmcp.utilities.logging import get_logger
+from fastmcp.utilities.types import AnyFunction
+
+if TYPE_CHECKING:
+ from fastmcp.server.providers.local_provider import LocalProvider
+ from fastmcp.tools.base import Tool
+
+logger = get_logger(__name__)
+
+F = TypeVar("F", bound=Callable[..., Any])
+
+
+# ---------------------------------------------------------------------------
+# CallTool resolver
+# ---------------------------------------------------------------------------
+
+
+def _make_resolver(app_name: str | None = None) -> Any:
+ """Create a CallTool resolver that addresses peer tools by identity.
+
+ ``app_name`` is the FastMCPApp's name, known at serialization time from
+ the tool's ``meta["fastmcp"]["app"]`` tag. Serialization happens deep
+ inside whatever composition the server has, so nothing here can know
+ what these tools will be *called* by the time the payload reaches a
+ host. References therefore start out identity-addressed, as
+ ``_``.
+
+ Each FastMCP server rewrites those references on the way out to the
+ name it lists that tool under, so what a renderer finally receives is
+ an ordinary tool name (see ``server.providers.prefab_payload``). A
+ reference no server could resolve keeps this form, which the dispatcher
+ still routes via ``get_tool_by_hash``.
+ """
+ from fastmcp.server.providers.addressing import (
+ hashed_backend_name,
+ parse_hashed_backend_name,
+ )
+
+ def _prefix(local_name: str) -> str:
+ if app_name:
+ # Don't re-hash an already-addressed name (same guard the
+ # old ___ resolver had with "___" not in name).
+ if parse_hashed_backend_name(local_name) is not None:
+ return local_name
+ return hashed_backend_name(app_name, local_name)
+ return local_name
+
+ def _resolve_tool_ref(fn: Any) -> Any:
+ from prefab_ui.app import ResolvedTool
+
+ if isinstance(fn, str):
+ return ResolvedTool(name=_prefix(fn))
+
+ fmeta: Any = None
+ try:
+ from fastmcp.decorators import get_fastmcp_meta
+
+ fmeta = get_fastmcp_meta(fn)
+ except Exception:
+ pass
+
+ if fmeta is not None:
+ name: str | None = getattr(fmeta, "name", None)
+ if name is not None:
+ return ResolvedTool(name=_prefix(name))
+
+ fn_name = getattr(fn, "__name__", None)
+ if fn_name is not None:
+ return ResolvedTool(name=_prefix(fn_name))
+
+ raise ValueError(f"Cannot resolve tool reference: {fn!r}")
+
+ return _resolve_tool_ref
+
+
+def _dispatch_decorator(
+ name_or_fn: str | AnyFunction | None,
+ name: str | None,
+ register: Callable[[Any, str | None], Any],
+ decorator_name: str,
+) -> Any:
+ """Shared dispatch logic for @app.tool() and @app.ui() calling patterns."""
+ if inspect.isroutine(name_or_fn):
+ return register(name_or_fn, name)
+
+ if isinstance(name_or_fn, str):
+ if name is not None:
+ raise TypeError(
+ "Cannot specify both a name as first argument and as keyword argument."
+ )
+ tool_name: str | None = name_or_fn
+ elif name_or_fn is None:
+ tool_name = name
+ else:
+ raise TypeError(
+ f"First argument to @{decorator_name} must be a function, string, or None, "
+ f"got {type(name_or_fn)}"
+ )
+
+ def decorator(fn: F) -> F:
+ return register(fn, tool_name)
+
+ return decorator
+
+
+# ---------------------------------------------------------------------------
+# FastMCPApp
+# ---------------------------------------------------------------------------
+
+
+class FastMCPApp(Provider):
+ """A Provider that represents an MCP application.
+
+ Binds together entry-point tools (``@app.ui``), backend tools
+ (``@app.tool``), and the Prefab renderer resource. Backend tools
+ are tagged with ``meta["fastmcp"]["app"]`` so ``Provider.get_tool``
+ can find them by original name even when transforms have been applied.
+ """
+
+ def __init__(self, name: str) -> None:
+ from fastmcp.server.providers.local_provider import LocalProvider
+
+ super().__init__()
+ self.name = name
+ self._local: LocalProvider = LocalProvider(on_duplicate="error")
+
+ def __repr__(self) -> str:
+ return f"FastMCPApp({self.name!r})"
+
+ # ------------------------------------------------------------------
+ # @app.tool() — backend tools called by the UI
+ # ------------------------------------------------------------------
+
+ @overload
+ def tool(
+ self,
+ name_or_fn: F,
+ *,
+ name: str | None = None,
+ description: str | None = None,
+ model: bool = False,
+ auth: AuthCheck | list[AuthCheck] | None = None,
+ timeout: float | None = None,
+ ) -> F: ...
+
+ @overload
+ def tool(
+ self,
+ name_or_fn: str | None = None,
+ *,
+ name: str | None = None,
+ description: str | None = None,
+ model: bool = False,
+ auth: AuthCheck | list[AuthCheck] | None = None,
+ timeout: float | None = None,
+ ) -> Callable[[F], F]: ...
+
+ def tool(
+ self,
+ name_or_fn: str | AnyFunction | None = None,
+ *,
+ name: str | None = None,
+ description: str | None = None,
+ model: bool = False,
+ auth: AuthCheck | list[AuthCheck] | None = None,
+ timeout: float | None = None,
+ ) -> Any:
+ """Register a backend tool that the UI calls via CallTool.
+
+ Backend tools default to ``visibility=["app"]``. Pass ``model=True``
+ to also expose the tool to the model (``visibility=["app", "model"]``).
+
+ Supports multiple calling patterns::
+
+ @app.tool
+ def save(name: str): ...
+
+ @app.tool()
+ def save(name: str): ...
+
+ @app.tool("custom_name")
+ def save(name: str): ...
+ """
+ visibility: list[Literal["app", "model"]] = (
+ ["app", "model"] if model else ["app"]
+ )
+
+ def _register(fn: F, tool_name: str | None) -> F:
+ from fastmcp.tools.base import Tool
+
+ resolved_name = tool_name or getattr(fn, "__name__", None)
+ if resolved_name is None:
+ raise ValueError(f"Cannot determine tool name for {fn!r}")
+
+ from fastmcp.apps.config import AppConfig, app_config_to_meta_dict
+ from fastmcp.server.providers.addressing import (
+ TOOL_HASH_META_KEY,
+ hash_tool,
+ )
+
+ app_config = AppConfig(visibility=visibility)
+ meta: dict[str, Any] = {
+ "ui": app_config_to_meta_dict(app_config),
+ "fastmcp": {
+ "app": self.name,
+ TOOL_HASH_META_KEY: hash_tool(self.name, resolved_name),
+ },
+ }
+
+ tool_obj = Tool.from_function(
+ fn,
+ name=resolved_name,
+ description=description,
+ meta=meta,
+ timeout=timeout,
+ auth=auth,
+ )
+ self._local._add_component(tool_obj)
+ return fn
+
+ return _dispatch_decorator(name_or_fn, name, _register, "tool")
+
+ # ------------------------------------------------------------------
+ # @app.ui() — entry-point tools the model calls to open the app
+ # ------------------------------------------------------------------
+
+ @overload
+ def ui(
+ self,
+ name_or_fn: F,
+ *,
+ name: str | None = None,
+ description: str | None = None,
+ title: str | None = None,
+ tags: set[str] | None = None,
+ icons: list[Icon] | None = None,
+ annotations: ToolAnnotations | None = None,
+ auth: AuthCheck | list[AuthCheck] | None = None,
+ timeout: float | None = None,
+ ) -> F: ...
+
+ @overload
+ def ui(
+ self,
+ name_or_fn: str | None = None,
+ *,
+ name: str | None = None,
+ description: str | None = None,
+ title: str | None = None,
+ tags: set[str] | None = None,
+ icons: list[Icon] | None = None,
+ annotations: ToolAnnotations | None = None,
+ auth: AuthCheck | list[AuthCheck] | None = None,
+ timeout: float | None = None,
+ ) -> Callable[[F], F]: ...
+
+ def ui(
+ self,
+ name_or_fn: str | AnyFunction | None = None,
+ *,
+ name: str | None = None,
+ description: str | None = None,
+ title: str | None = None,
+ tags: set[str] | None = None,
+ icons: list[Icon] | None = None,
+ annotations: ToolAnnotations | None = None,
+ auth: AuthCheck | list[AuthCheck] | None = None,
+ timeout: float | None = None,
+ ) -> Any:
+ """Register a UI entry-point tool that the model calls.
+
+ Entry-point tools default to ``visibility=["model"]`` and auto-wire
+ the Prefab renderer resource and CSP. They are tagged with the app
+ name so structured content includes ``_meta.fastmcp.app``.
+
+ Supports multiple calling patterns::
+
+ @app.ui
+ def dashboard() -> Component: ...
+
+ @app.ui()
+ def dashboard() -> Component: ...
+
+ @app.ui("my_dashboard")
+ def dashboard() -> Component: ...
+ """
+
+ def _register(fn: F, tool_name: str | None) -> F:
+ from fastmcp.apps.config import AppConfig, app_config_to_meta_dict
+ from fastmcp.server.providers.addressing import (
+ TOOL_HASH_META_KEY,
+ hash_tool,
+ )
+ from fastmcp.server.providers.local_provider.decorators.tools import (
+ PREFAB_RENDERER_URI,
+ )
+ from fastmcp.tools.base import Tool
+
+ resolved = tool_name or getattr(fn, "__name__", None) or "unknown"
+ app_config = AppConfig(
+ resource_uri=PREFAB_RENDERER_URI,
+ visibility=["model"],
+ )
+
+ meta: dict[str, Any] = {
+ "ui": app_config_to_meta_dict(app_config),
+ "fastmcp": {
+ "app": self.name,
+ TOOL_HASH_META_KEY: hash_tool(self.name, resolved),
+ },
+ }
+
+ tool_obj = Tool.from_function(
+ fn,
+ name=tool_name,
+ description=description,
+ title=title,
+ tags=tags,
+ icons=icons,
+ annotations=annotations,
+ meta=meta,
+ timeout=timeout,
+ auth=auth,
+ )
+ self._local._add_component(tool_obj)
+
+ return fn
+
+ return _dispatch_decorator(name_or_fn, name, _register, "ui")
+
+ # ------------------------------------------------------------------
+ # Programmatic tool addition
+ # ------------------------------------------------------------------
+
+ def add_tool(
+ self,
+ tool: Tool | Callable[..., Any],
+ ) -> Tool:
+ """Add a tool to this app programmatically.
+
+ The tool is tagged with this app's name for routing.
+ """
+ from fastmcp.tools.base import Tool
+
+ if not isinstance(tool, Tool):
+ tool = Tool._ensure_tool(tool)
+
+ from fastmcp.server.providers.addressing import (
+ TOOL_HASH_META_KEY,
+ hash_tool,
+ )
+
+ meta = dict(tool.meta) if tool.meta else {}
+ fm = meta.setdefault("fastmcp", {})
+ fm["app"] = self.name
+ fm[TOOL_HASH_META_KEY] = hash_tool(self.name, tool.name)
+ ui = meta.setdefault("ui", {})
+ if "visibility" not in ui:
+ ui["visibility"] = ["app"]
+ tool.meta = meta
+
+ self._local._add_component(tool)
+ return tool
+
+ # ------------------------------------------------------------------
+ # Provider interface — delegate to internal LocalProvider
+ # ------------------------------------------------------------------
+
+ async def _list_tools(self) -> Sequence[Tool]:
+ return await self._local._list_tools()
+
+ async def _get_tool(self, name: str, version: Any = None) -> Tool | None:
+ return await self._local._get_tool(name, version)
+
+ async def _list_resources(self) -> Sequence[Any]:
+ return await self._local._list_resources()
+
+ async def _get_resource(self, uri: str, version: Any = None) -> Any | None:
+ return await self._local._get_resource(uri, version)
+
+ async def _list_resource_templates(self) -> Sequence[Any]:
+ return await self._local._list_resource_templates()
+
+ async def _get_resource_template(self, uri: str, version: Any = None) -> Any | None:
+ return await self._local._get_resource_template(uri, version)
+
+ async def _list_prompts(self) -> Sequence[Any]:
+ return await self._local._list_prompts()
+
+ async def _get_prompt(self, name: str, version: Any = None) -> Any | None:
+ return await self._local._get_prompt(name, version)
+
+ @asynccontextmanager
+ async def lifespan(self) -> AsyncIterator[None]:
+ async with self._local.lifespan():
+ yield
+
+ # ------------------------------------------------------------------
+ # Convenience runner
+ # ------------------------------------------------------------------
+
+ def run(
+ self,
+ transport: Literal["stdio", "http", "sse", "streamable-http"] | None = None,
+ **kwargs: Any,
+ ) -> None:
+ """Create a temporary FastMCP server and run this app standalone."""
+ from fastmcp.server.server import FastMCP
+
+ server = FastMCP(self.name)
+ server.add_provider(self)
+ server.run(transport=transport, **kwargs)
diff --git a/fastmcp_slim/fastmcp/apps/approval.py b/fastmcp_slim/fastmcp/apps/approval.py
new file mode 100644
index 000000000..17b124e1f
--- /dev/null
+++ b/fastmcp_slim/fastmcp/apps/approval.py
@@ -0,0 +1,198 @@
+"""Approval — a Provider that adds human-in-the-loop approval to any server.
+
+The LLM presents a summary of what it's about to do, and the user
+approves or rejects via buttons. The result is sent back into the
+conversation as a message, prompting the LLM's next turn.
+
+Requires ``fastmcp[apps]`` (prefab-ui).
+
+Usage::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.approval import Approval
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(Approval())
+"""
+
+from __future__ import annotations
+
+from typing import Literal
+
+try:
+ from prefab_ui.actions import SetState
+ from prefab_ui.actions.mcp import SendMessage
+ from prefab_ui.app import PrefabApp
+ from prefab_ui.components import (
+ H3,
+ Button,
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ Column,
+ Muted,
+ Row,
+ Text,
+ )
+ from prefab_ui.components.control_flow import If
+ from prefab_ui.rx import STATE
+except ImportError as _exc:
+ raise ImportError(
+ "Approval requires prefab-ui. Install with: pip install 'fastmcp[apps]'"
+ ) from _exc
+
+
+from fastmcp.apps.app import FastMCPApp
+
+
+class Approval(FastMCPApp):
+ """A Provider that adds human-in-the-loop approval to a server.
+
+ The LLM calls the ``request_approval`` tool with a summary and
+ optional details. The user sees an approval card with Approve and
+ Reject buttons. Clicking either sends a message back into the
+ conversation (via ``SendMessage``), triggering the LLM's next turn.
+
+ The message appears as if the user sent it, so the LLM sees
+ something like ``'"Deploy v3.2 to production" is APPROVED'``.
+
+ Example::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.approval import Approval
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(Approval())
+
+ Customized::
+
+ Approval(
+ title="Deploy Gate",
+ approve_text="Ship it",
+ approve_variant="default",
+ reject_text="Abort",
+ reject_variant="destructive",
+ )
+ """
+
+ def __init__(
+ self,
+ name: str = "Approval",
+ *,
+ title: str = "Approval Required",
+ approve_text: str = "Approve",
+ reject_text: str = "Reject",
+ approve_variant: Literal[
+ "default", "destructive", "success", "info"
+ ] = "default",
+ reject_variant: Literal[
+ "default", "outline", "destructive", "success", "info"
+ ] = "outline",
+ ) -> None:
+ super().__init__(name)
+ self._title = title
+ self._approve_text = approve_text
+ self._reject_text = reject_text
+ self._approve_variant = approve_variant
+ self._reject_variant = reject_variant
+ self._register_tools()
+
+ def __repr__(self) -> str:
+ return f"Approval({self.name!r})"
+
+ def _register_tools(self) -> None:
+ provider = self
+
+ @self.ui()
+ def request_approval(
+ summary: str,
+ details: str | None = None,
+ title: str | None = None,
+ approve_text: str | None = None,
+ reject_text: str | None = None,
+ approve_variant: str | None = None,
+ reject_variant: str | None = None,
+ ) -> PrefabApp:
+ """Request human approval before proceeding with an action.
+
+ Call this tool proactively whenever you are about to take a
+ significant or irreversible action and want the user to
+ confirm first. Do NOT wait for the user to ask you to seek
+ approval — use your judgment about when confirmation is
+ appropriate.
+
+ The user will see an approval card with the summary, optional
+ details, and Approve/Reject buttons. When they click a button,
+ their decision appears as a message in the conversation (as if
+ the user typed it), like:
+
+ "Deploy v3.2 to production" — I selected: Approve
+
+ or:
+
+ "Deploy v3.2 to production" — I selected: Reject
+
+ IMPORTANT: After calling this tool, you MUST stop and wait
+ for the user's response. Do not continue, do not take any
+ other actions, do not generate further output until you see
+ the "I selected:" message. If approved, continue with the
+ action. If rejected, acknowledge and ask how to proceed.
+
+ Args:
+ summary: Brief description of the action requiring approval
+ (shown prominently to the user).
+ details: Optional longer explanation, context, or
+ consequences of the action.
+ title: Heading for the approval card (default: "Approval Required").
+ approve_text: Label for the approve button (default: "Approve").
+ reject_text: Label for the reject button (default: "Reject").
+ approve_variant: Button style — "default", "destructive",
+ "success", or "info".
+ reject_variant: Button style for the reject button
+ (same options plus "outline").
+ """
+ _title = title or provider._title
+ _approve = approve_text or provider._approve_text
+ _reject = reject_text or provider._reject_text
+ _approve_v = approve_variant or provider._approve_variant
+ _reject_v = reject_variant or provider._reject_variant
+
+ approve_msg = f'"{summary}" — I selected: {_approve}'
+ reject_msg = f'"{summary}" — I selected: {_reject}'
+
+ with Card(css_class="max-w-lg mx-auto") as view:
+ with CardHeader():
+ H3(_title)
+
+ with CardContent(), Column(gap=3):
+ Text(summary, css_class="font-medium")
+ if details:
+ Muted(details)
+
+ with CardFooter():
+ with If(STATE.decided):
+ Muted("Response sent.")
+ with If(~STATE.decided): # noqa: SIM117
+ with Row(gap=2, css_class="w-full justify-end"):
+ Button(
+ _reject,
+ variant=_reject_v,
+ on_click=[
+ SendMessage(reject_msg),
+ SetState("decided", True),
+ ],
+ )
+ Button(
+ _approve,
+ variant=_approve_v,
+ on_click=[
+ SendMessage(approve_msg),
+ SetState("decided", True),
+ ],
+ )
+
+ return PrefabApp(
+ view=view,
+ state={"decided": False},
+ )
diff --git a/fastmcp_slim/fastmcp/apps/choice.py b/fastmcp_slim/fastmcp/apps/choice.py
new file mode 100644
index 000000000..aaffef903
--- /dev/null
+++ b/fastmcp_slim/fastmcp/apps/choice.py
@@ -0,0 +1,141 @@
+"""Choice — a Provider that lets the user pick from a set of options.
+
+The LLM presents options, the user clicks one, and the selection
+flows back into the conversation as a message.
+
+Requires ``fastmcp[apps]`` (prefab-ui).
+
+Usage::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.choice import Choice
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(Choice())
+"""
+
+from __future__ import annotations
+
+from typing import Literal
+
+try:
+ from prefab_ui.actions import SetState
+ from prefab_ui.actions.mcp import SendMessage
+ from prefab_ui.app import PrefabApp
+ from prefab_ui.components import (
+ H3,
+ Button,
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ Column,
+ Muted,
+ Text,
+ )
+ from prefab_ui.components.control_flow import If
+ from prefab_ui.rx import STATE
+except ImportError as _exc:
+ raise ImportError(
+ "Choice requires prefab-ui. Install with: pip install 'fastmcp[apps]'"
+ ) from _exc
+
+from fastmcp.apps.app import FastMCPApp
+
+
+class Choice(FastMCPApp):
+ """A Provider that lets the user choose from a set of options.
+
+ The LLM calls ``choose`` with a prompt and a list of options.
+ The user sees a card with one button per option. Clicking a button
+ sends the selection back into the conversation via ``SendMessage``,
+ triggering the LLM's next turn.
+
+ Example::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.choice import Choice
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(Choice())
+ """
+
+ def __init__(
+ self,
+ name: str = "Choice",
+ *,
+ title: str = "Choose an Option",
+ variant: Literal[
+ "default", "outline", "destructive", "success", "info"
+ ] = "outline",
+ ) -> None:
+ super().__init__(name)
+ self._title = title
+ self._variant = variant
+ self._register_tools()
+
+ def __repr__(self) -> str:
+ return f"Choice({self.name!r})"
+
+ def _register_tools(self) -> None:
+ provider = self
+
+ @self.ui()
+ def choose(
+ prompt: str,
+ options: list[str],
+ title: str | None = None,
+ ) -> PrefabApp:
+ """Present the user with a set of options to choose from.
+
+ Call this tool when you need the user to make a decision
+ between discrete alternatives. Use it proactively — don't
+ ask the user to type their choice in chat when you can
+ present clean, clickable options instead.
+
+ The user will see a card with one button per option. When
+ they click one, their choice appears as a message in the
+ conversation (as if the user typed it), like:
+
+ "Which deployment strategy?" — I selected: Blue-green
+
+ IMPORTANT: After calling this tool, you MUST stop and wait
+ for the user's response. Do not continue or take any other
+ actions until you see the "I selected:" message.
+
+ Args:
+ prompt: The question or decision to present to the user.
+ options: List of options the user can choose from.
+ title: Optional heading for the card.
+ """
+ _title = title or provider._title
+
+ with Card(css_class="max-w-lg mx-auto") as view:
+ with CardHeader():
+ H3(_title)
+
+ with CardContent():
+ Text(prompt, css_class="font-medium")
+
+ with CardFooter():
+ with If(STATE.decided):
+ Muted("Response sent.")
+ with If(~STATE.decided): # noqa: SIM117
+ with Column(gap=2, css_class="w-full"):
+ for option in options:
+ Button(
+ option,
+ variant=provider._variant,
+ css_class="w-full justify-start",
+ on_click=[
+ SendMessage(
+ f'"{prompt}" — I selected: {option}'
+ ),
+ SetState("decided", True),
+ ],
+ )
+
+ return PrefabApp(
+ view=view,
+ state={"decided": False},
+ )
diff --git a/src/fastmcp/server/apps.py b/fastmcp_slim/fastmcp/apps/config.py
similarity index 50%
rename from src/fastmcp/server/apps.py
rename to fastmcp_slim/fastmcp/apps/config.py
index fcb0b673c..1686d8626 100644
--- a/src/fastmcp/server/apps.py
+++ b/fastmcp_slim/fastmcp/apps/config.py
@@ -11,8 +11,11 @@ from typing import Any, Literal
from pydantic import BaseModel, Field
+from fastmcp.utilities.components import FastMCPComponent
+from fastmcp.utilities.mime import UI_MIME_TYPE as UI_MIME_TYPE
+from fastmcp.utilities.mime import resolve_ui_mime_type as resolve_ui_mime_type
+
UI_EXTENSION_ID = "io.modelcontextprotocol/ui"
-UI_MIME_TYPE = "text/html;profile=mcp-app"
class ResourceCSP(BaseModel):
@@ -25,22 +28,26 @@ class ResourceCSP(BaseModel):
connect_domains: list[str] | None = Field(
default=None,
- alias="connectDomains",
+ validation_alias="connectDomains",
+ serialization_alias="connectDomains",
description="Origins allowed for fetch/XHR/WebSocket (connect-src)",
)
resource_domains: list[str] | None = Field(
default=None,
- alias="resourceDomains",
+ validation_alias="resourceDomains",
+ serialization_alias="resourceDomains",
description="Origins allowed for scripts, images, styles, fonts (script-src etc.)",
)
frame_domains: list[str] | None = Field(
default=None,
- alias="frameDomains",
+ validation_alias="frameDomains",
+ serialization_alias="frameDomains",
description="Origins allowed for nested iframes (frame-src)",
)
base_uri_domains: list[str] | None = Field(
default=None,
- alias="baseUriDomains",
+ validation_alias="baseUriDomains",
+ serialization_alias="baseUriDomains",
description="Allowed base URIs for the document (base-uri)",
)
@@ -67,7 +74,8 @@ class ResourcePermissions(BaseModel):
)
clipboard_write: dict[str, Any] | None = Field(
default=None,
- alias="clipboardWrite",
+ validation_alias="clipboardWrite",
+ serialization_alias="clipboardWrite",
description="Request clipboard-write access",
)
@@ -89,7 +97,8 @@ class AppConfig(BaseModel):
resource_uri: str | None = Field(
default=None,
- alias="resourceUri",
+ validation_alias="resourceUri",
+ serialization_alias="resourceUri",
description="URI of the UI resource (typically ui:// scheme). Tools only.",
)
visibility: list[Literal["app", "model"]] | None = Field(
@@ -105,13 +114,70 @@ class AppConfig(BaseModel):
domain: str | None = Field(default=None, description="Domain for the iframe")
prefers_border: bool | None = Field(
default=None,
- alias="prefersBorder",
+ validation_alias="prefersBorder",
+ serialization_alias="prefersBorder",
description="Whether the UI prefers a visible border",
)
model_config = {"populate_by_name": True, "extra": "allow"}
+class PrefabAppConfig(AppConfig):
+ """App configuration for Prefab tools with sensible defaults.
+
+ Like ``app=True`` but customizable. Auto-wires the Prefab renderer
+ URI and merges the renderer's CSP with any additional domains you
+ specify. The renderer resource is registered automatically.
+
+ Example::
+
+ @mcp.tool(app=PrefabAppConfig()) # same as app=True
+
+ @mcp.tool(app=PrefabAppConfig(
+ csp=ResourceCSP(frame_domains=["https://example.com"]),
+ ))
+ """
+
+ def model_post_init(self, __context: Any) -> None:
+ # Set the renderer URI if not explicitly overridden
+ if self.resource_uri is None:
+ self.resource_uri = "ui://prefab/renderer.html"
+
+ # Merge renderer CSP with user-provided CSP
+ try:
+ from prefab_ui.renderer import get_renderer_csp
+
+ renderer_csp = get_renderer_csp()
+ except ImportError:
+ renderer_csp = {}
+
+ if renderer_csp:
+ user_csp = self.csp or ResourceCSP()
+ # Start from the user's CSP (preserves model_extra for
+ # forward-compat directives), then merge renderer domains.
+ merged_data = user_csp.model_dump(exclude_none=True)
+ merged_data["connect_domains"] = _merge_domains(
+ renderer_csp.get("connect_domains"),
+ user_csp.connect_domains,
+ )
+ merged_data["resource_domains"] = _merge_domains(
+ renderer_csp.get("resource_domains"),
+ user_csp.resource_domains,
+ )
+ self.csp = ResourceCSP(**merged_data)
+
+
+def _merge_domains(base: list[str] | None, extra: list[str] | None) -> list[str] | None:
+ """Merge two domain lists, deduplicating."""
+ if base is None and extra is None:
+ return None
+ combined = list(base or [])
+ for d in extra or []:
+ if d not in combined:
+ combined.append(d)
+ return combined or None
+
+
def app_config_to_meta_dict(app: AppConfig | dict[str, Any]) -> dict[str, Any]:
"""Convert an AppConfig or dict to the wire-format dict for ``meta["ui"]``."""
if isinstance(app, AppConfig):
@@ -119,24 +185,29 @@ def app_config_to_meta_dict(app: AppConfig | dict[str, Any]) -> dict[str, Any]:
return app
-def resolve_ui_mime_type(uri: str, explicit_mime_type: str | None) -> str | None:
- """Return the appropriate MIME type for a resource URI.
+def is_model_visible(component: FastMCPComponent) -> bool:
+ """Whether a component may be shown to, or invoked by, the model.
- For ``ui://`` scheme resources, defaults to ``UI_MIME_TYPE`` when no
- explicit MIME type is provided. This ensures UI resources are correctly
- identified regardless of how they're registered (via FastMCP.resource,
- the standalone @resource decorator, or resource templates).
+ Visibility is a declaration, and the MCP Apps spec puts the filtering on
+ the host — so ``tools/list`` carries app-only tools and the host keeps
+ them from the model. That division only works where a host stands between
+ the server and the model.
- Args:
- uri: The resource URI string
- explicit_mime_type: The MIME type explicitly provided by the user
+ It does not hold for surfaces a server drives itself. A search result or
+ a code-mode catalog reaches the model as ordinary tool output, and a
+ call-tool proxy invokes on a name the model supplies; nothing downstream
+ can filter either. Those surfaces have to apply the declaration here.
- Returns:
- The resolved MIME type (explicit value, UI default, or None)
+ A component with no ``visibility`` is visible: the field marks the
+ exception, and the spec's default is both audiences.
"""
- if explicit_mime_type is not None:
- return explicit_mime_type
- # Case-insensitive scheme check per RFC 3986
- if uri.lower().startswith("ui://"):
- return UI_MIME_TYPE
- return None
+ meta = component.meta
+ if not meta:
+ return True
+ ui_meta = meta.get("ui")
+ if not isinstance(ui_meta, dict):
+ return True
+ visibility = ui_meta.get("visibility")
+ if not isinstance(visibility, list):
+ return True
+ return "model" in visibility
diff --git a/fastmcp_slim/fastmcp/apps/file_upload.py b/fastmcp_slim/fastmcp/apps/file_upload.py
new file mode 100644
index 000000000..ed302ce9d
--- /dev/null
+++ b/fastmcp_slim/fastmcp/apps/file_upload.py
@@ -0,0 +1,405 @@
+"""FileUpload — a Provider that adds drag-and-drop file upload to any server.
+
+Lets users upload files directly to the server through an interactive UI,
+bypassing the LLM context window entirely. The LLM can then read and work
+with uploaded files through model-visible tools.
+
+Requires ``fastmcp[apps]`` (prefab-ui).
+
+Usage::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps import FileUpload
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(FileUpload())
+
+For custom persistence, override the storage methods::
+
+ class S3Upload(FileUpload):
+ def on_store(self, files, ctx):
+ # write to S3, return summaries
+ ...
+
+ def on_list(self, ctx):
+ # list from S3
+ ...
+
+ def on_read(self, name, ctx):
+ # read from S3
+ ...
+"""
+
+from __future__ import annotations
+
+try:
+ 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 (
+ H3,
+ Badge,
+ Button,
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ Column,
+ DropZone,
+ Muted,
+ Row,
+ Separator,
+ Small,
+ Text,
+ )
+ from prefab_ui.components.control_flow import Else, ForEach, If
+ from prefab_ui.rx import ERROR, RESULT, STATE, Rx
+except ImportError as _exc:
+ raise ImportError(
+ "FileUpload requires prefab-ui. Install with: pip install 'fastmcp[apps]'"
+ ) from _exc
+
+import base64
+from datetime import datetime, timezone
+from typing import Any
+
+from fastmcp.apps.app import FastMCPApp
+from fastmcp.server.context import Context
+
+_TEXT_EXTENSIONS = frozenset(
+ (".csv", ".json", ".txt", ".md", ".py", ".yaml", ".yml", ".toml")
+)
+
+
+def _b64_decoded_size(b64: str) -> int:
+ """Return the exact decoded byte-length of a base64 string without decoding it."""
+ n = len(b64)
+ if n == 0:
+ return 0
+ padding = b64.count("=", max(0, n - 2))
+ return n * 3 // 4 - padding
+
+
+def _format_size(size: int) -> str:
+ if size < 1024:
+ return f"{size} B"
+ elif size < 1024 * 1024:
+ return f"{size / 1024:.1f} KB"
+ else:
+ return f"{size / (1024 * 1024):.1f} MB"
+
+
+def _make_summary(entry: dict[str, Any]) -> dict[str, Any]:
+ return {
+ "name": entry["name"],
+ "type": entry["type"],
+ "size": entry["size"],
+ "size_display": _format_size(entry["size"]),
+ "uploaded_at": entry["uploaded_at"],
+ }
+
+
+class FileUpload(FastMCPApp):
+ """A Provider that adds file upload capabilities to a server.
+
+ Registers a drag-and-drop UI tool, a backend storage tool, and
+ model-visible tools for listing and reading uploaded files.
+
+ Files are scoped by MCP session and stored in memory by default.
+ Override ``on_store``, ``on_list``, and ``on_read`` for custom
+ persistence (filesystem, S3, database, etc.). Each method receives
+ the current ``Context``, giving access to session ID, auth tokens,
+ and request metadata for partitioning and authorization.
+
+ **Session scoping:** The default storage uses ``ctx.session_id`` to
+ isolate files by session. This works with stdio, SSE, and stateful
+ HTTP transports. In **stateless HTTP** mode, each request creates a
+ new session, so files won't persist across requests. For stateless
+ deployments, override the storage methods to partition by a stable
+ identifier from the auth context::
+
+ class UserScopedUpload(FileUpload):
+ def on_store(self, files, ctx):
+ user_id = ctx.access_token["sub"]
+ ...
+
+ Example::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.file_upload import FileUpload
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(FileUpload())
+ """
+
+ def __init__(
+ self,
+ name: str = "Files",
+ *,
+ max_file_size: int = 10 * 1024 * 1024,
+ title: str = "File Upload",
+ description: str = (
+ "Drop files to upload them to the server. "
+ "The model can then read and analyze them "
+ "without using the context window."
+ ),
+ drop_label: str = "Drop files here",
+ ) -> None:
+ super().__init__(name)
+ self._max_file_size = max_file_size
+ self._title = title
+ self._description = description
+ self._drop_label = drop_label
+
+ # Default in-memory store, keyed by session_id
+ self._store: dict[str, dict[str, dict[str, Any]]] = {}
+
+ self._register_tools()
+
+ def __repr__(self) -> str:
+ return f"FileUpload({self.name!r})"
+
+ # ------------------------------------------------------------------
+ # Storage interface — override these for custom persistence
+ # ------------------------------------------------------------------
+
+ def _get_scope_key(self, ctx: Context) -> str:
+ """Return the key used to partition file storage.
+
+ Defaults to ``ctx.session_id``, which is stable for stdio, SSE,
+ and stateful HTTP. The default ``on_store``/``on_list``/``on_read``
+ implementations call this to partition the in-memory store.
+
+ Override to scope by user, tenant, or any other dimension::
+
+ def _get_scope_key(self, ctx):
+ return ctx.access_token["sub"]
+ """
+ try:
+ return ctx.session_id
+ except RuntimeError:
+ return "__default__"
+
+ def on_store(
+ self,
+ files: list[dict[str, Any]],
+ ctx: Context,
+ ) -> list[dict[str, Any]]:
+ """Store uploaded files and return summaries.
+
+ Args:
+ files: List of file dicts, each with ``name``, ``size``,
+ ``type``, and ``data`` (base64-encoded content).
+ ctx: The current request context. Use for session ID,
+ auth tokens, or any metadata needed for partitioning.
+
+ Override this method for custom persistence. The default
+ implementation stores files in memory, scoped by
+ ``_get_scope_key(ctx)``.
+
+ Returns:
+ List of file summary dicts (``name``, ``type``, ``size``,
+ ``size_display``, ``uploaded_at``).
+ """
+ scope = self._get_scope_key(ctx)
+ session_files = self._store.setdefault(scope, {})
+ for f in files:
+ session_files[f["name"]] = {
+ "name": f["name"],
+ "size": f["size"],
+ "type": f["type"],
+ "data": f["data"],
+ "uploaded_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
+ }
+ return [_make_summary(e) for e in session_files.values()]
+
+ def on_list(self, ctx: Context) -> list[dict[str, Any]]:
+ """List all stored files.
+
+ Args:
+ ctx: The current request context.
+
+ Override this method for custom persistence. The default
+ implementation returns files from the current scope.
+
+ Returns:
+ List of file summary dicts.
+ """
+ scope = self._get_scope_key(ctx)
+ session_files = self._store.get(scope, {})
+ return [_make_summary(e) for e in session_files.values()]
+
+ def on_read(self, name: str, ctx: Context) -> dict[str, Any]:
+ """Read a file's contents by name.
+
+ Args:
+ name: The filename to read.
+ ctx: The current request context.
+
+ Override this method for custom persistence. The default
+ implementation reads from the current scope's in-memory store.
+ Text files are decoded from base64; binary files return a
+ truncated base64 preview.
+
+ Returns:
+ Dict with file metadata and ``content`` (text) or
+ ``content_base64`` (binary preview).
+
+ Raises:
+ ValueError: If the file is not found.
+ """
+ scope = self._get_scope_key(ctx)
+ session_files = self._store.get(scope, {})
+ if name not in session_files:
+ available = list(session_files.keys())
+ raise ValueError(f"File {name!r} not found. Available: {available}")
+ entry = session_files[name]
+ result: dict[str, Any] = {
+ "name": entry["name"],
+ "size": entry["size"],
+ "type": entry["type"],
+ "uploaded_at": entry["uploaded_at"],
+ }
+ is_text = entry["type"].startswith("text/") or any(
+ entry["name"].endswith(ext) for ext in _TEXT_EXTENSIONS
+ )
+ if is_text:
+ try:
+ result["content"] = base64.b64decode(entry["data"]).decode("utf-8")
+ except UnicodeDecodeError:
+ result["content_base64"] = entry["data"][:200] + "..."
+ else:
+ result["content_base64"] = entry["data"][:200] + "..."
+ return result
+
+ # ------------------------------------------------------------------
+ # Tool registration
+ # ------------------------------------------------------------------
+
+ def _register_tools(self) -> None:
+ provider = self
+
+ @self.tool()
+ def store_files(files: list[dict], ctx: Context) -> list[dict]:
+ """Store uploaded files. Receives file objects with name, size, type, data (base64)."""
+ for f in files:
+ # Compute actual data size from the base64 payload rather
+ # than trusting the client-reported ``size`` field.
+ actual_size = _b64_decoded_size(f.get("data", ""))
+ if actual_size > provider._max_file_size:
+ raise ValueError(
+ f"File {f.get('name', '?')!r} exceeds max size "
+ f"({_format_size(actual_size)} > "
+ f"{_format_size(provider._max_file_size)})"
+ )
+ return provider.on_store(files, ctx)
+
+ @self.tool(model=True)
+ def list_files(ctx: Context) -> list[dict]:
+ """List all uploaded files with metadata."""
+ return provider.on_list(ctx)
+
+ @self.tool(model=True)
+ def read_file(name: str, ctx: Context) -> dict:
+ """Read an uploaded file's contents by name."""
+ return provider.on_read(name, ctx)
+
+ @self.ui()
+ def file_manager(ctx: Context) -> PrefabApp:
+ """Upload and manage files. Drop files here to send them to the server."""
+ with Card(css_class="max-w-2xl mx-auto") as view:
+ with CardHeader(), Row(gap=2, align="center"):
+ H3(provider._title)
+ with If(STATE.stored.length()):
+ Badge(
+ STATE.stored.length(),
+ variant="secondary",
+ )
+
+ with CardContent(), Column(gap=4):
+ Muted(provider._description)
+
+ DropZone(
+ name="pending",
+ icon="inbox",
+ label=provider._drop_label,
+ description=(
+ "Any file type, up to "
+ f"{_format_size(provider._max_file_size)}"
+ ),
+ multiple=True,
+ max_size=provider._max_file_size,
+ )
+
+ with If(STATE.pending.length()), Column(gap=2):
+ with (
+ ForEach("pending"),
+ Row(gap=2, align="center"),
+ Column(gap=0),
+ ):
+ Small(Rx("$item.name"))
+ Muted(Rx("$item.type"))
+
+ Button(
+ "Upload to Server",
+ on_click=CallTool(
+ "store_files",
+ arguments={
+ "files": Rx("pending"),
+ },
+ on_success=[
+ SetState("stored", RESULT),
+ SetState("pending", []),
+ ShowToast(
+ "Files uploaded!",
+ variant="success",
+ ),
+ ],
+ on_error=ShowToast(
+ ERROR,
+ variant="error",
+ ),
+ ),
+ )
+
+ with If(STATE.stored.length()):
+ Separator()
+ Text(
+ "Uploaded",
+ css_class="font-medium text-sm",
+ )
+ with (
+ ForEach("stored") as f,
+ Row(
+ gap=2,
+ align="center",
+ css_class="justify-between",
+ ),
+ ):
+ with Column(gap=0):
+ Small(f.name)
+ Muted(f.uploaded_at)
+ with Row(gap=2):
+ Badge(f.type, variant="secondary")
+ Badge(
+ f.size_display,
+ variant="outline",
+ )
+
+ with CardFooter(), Row(align="center", css_class="w-full"):
+ with If(STATE.stored.length()):
+ Muted(
+ f"{STATE.stored.length()}"
+ f" {STATE.stored.length().pluralize('file')}"
+ " on server"
+ )
+ with Else():
+ Muted("No files uploaded yet")
+
+ return PrefabApp(
+ view=view,
+ state={
+ "pending": [],
+ "stored": provider.on_list(ctx),
+ },
+ )
diff --git a/fastmcp_slim/fastmcp/apps/form.py b/fastmcp_slim/fastmcp/apps/form.py
new file mode 100644
index 000000000..96ee35adf
--- /dev/null
+++ b/fastmcp_slim/fastmcp/apps/form.py
@@ -0,0 +1,229 @@
+"""FormInput — a Provider that collects structured input from the user.
+
+Define a Pydantic model for the data you need, and ``FormInput``
+generates a form UI. The user fills it out, the submission is
+validated, and an optional callback processes the result.
+
+Requires ``fastmcp[apps]`` (prefab-ui).
+
+Usage::
+
+ from pydantic import BaseModel
+ from fastmcp import FastMCP
+ from fastmcp.apps.form import FormInput
+
+ class ShippingAddress(BaseModel):
+ street: str
+ city: str
+ state: str
+ zip_code: str
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(FormInput(model=ShippingAddress))
+"""
+
+from __future__ import annotations
+
+import json
+from collections.abc import Callable
+from typing import Any
+
+from packaging.version import InvalidVersion, Version
+
+try:
+ import prefab_ui
+ from prefab_ui.actions import SetState
+ from prefab_ui.actions.mcp import CallTool, SendMessage
+ from prefab_ui.app import PrefabApp
+ from prefab_ui.components import (
+ H3,
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ Column,
+ Form,
+ Muted,
+ )
+ from prefab_ui.components.control_flow import If
+ from prefab_ui.rx import RESULT, STATE
+except ImportError as _exc:
+ raise ImportError(
+ "FormInput requires prefab-ui. Install with: pip install 'fastmcp[apps]'"
+ ) from _exc
+
+# `defaults` kwarg on Form.from_model was added in prefab-ui 0.19.1. Gate on
+# version so that older prefab-ui keeps working — `default` silently no-ops.
+try:
+ _FORM_SUPPORTS_DEFAULTS = Version(prefab_ui.__version__) >= Version("0.19.1")
+except InvalidVersion:
+ _FORM_SUPPORTS_DEFAULTS = False
+
+import pydantic
+
+from fastmcp.apps.app import FastMCPApp
+
+
+def _backfill_boolean_defaults(
+ model: type[pydantic.BaseModel],
+ data: dict[str, Any],
+) -> dict[str, Any]:
+ """Fill in missing boolean fields with their model defaults.
+
+ HTML checkboxes omit the field entirely when unchecked, so the
+ submitted data dict won't contain a key for ``False`` booleans.
+ This backfills those missing keys so Pydantic validation succeeds.
+ """
+ for name, field_info in model.model_fields.items():
+ if name in data:
+ continue
+ if field_info.annotation is bool:
+ if field_info.default is not pydantic.fields.PydanticUndefined:
+ data[name] = field_info.default
+ else:
+ data[name] = False
+ return data
+
+
+class FormInput(FastMCPApp):
+ """A Provider that collects structured input via a Pydantic model.
+
+ Define a model for the data you need, and ``FormInput`` generates
+ a form from it using ``Form.from_model()``. Field types, labels,
+ descriptions, and validation are all derived from the model.
+
+ Optionally provide an ``on_submit`` callback to process the
+ validated data. The callback receives a model instance and returns
+ a string that goes back to the LLM. Without a callback, the
+ validated JSON is sent directly.
+
+ Example::
+
+ from pydantic import BaseModel
+ from fastmcp import FastMCP
+ from fastmcp.apps.form import FormInput
+
+ class Contact(BaseModel):
+ name: str
+ email: str
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(FormInput(model=Contact))
+
+ With a callback::
+
+ def save_contact(contact: Contact) -> str:
+ db.insert(contact.model_dump())
+ return f"Saved {contact.name}"
+
+ mcp.add_provider(FormInput(model=Contact, on_submit=save_contact))
+ """
+
+ def __init__(
+ self,
+ model: type[pydantic.BaseModel],
+ *,
+ name: str | None = None,
+ title: str | None = None,
+ submit_text: str = "Submit",
+ tool_name: str | None = None,
+ on_submit: Callable[..., str] | None = None,
+ send_message: bool = False,
+ ) -> None:
+ app_name = name or model.__name__
+ super().__init__(app_name)
+ self._model = model
+ self._title = title or model.__name__
+ self._submit_text = submit_text
+ self._tool_name = tool_name or f"collect_{model.__name__.lower()}"
+ self._on_submit = on_submit
+ self._send_message = send_message
+ self._register_tools()
+
+ def __repr__(self) -> str:
+ return f"FormInput({self._model.__name__!r})"
+
+ def _register_tools(self) -> None:
+ provider = self
+ model = self._model
+
+ @self.tool()
+ def submit_form(data: dict[str, Any] | None = None) -> str:
+ """Validate and process form submission."""
+ if data is None:
+ data = {}
+ data = _backfill_boolean_defaults(model, data)
+ validated = model.model_validate(data)
+ if provider._on_submit is not None:
+ return provider._on_submit(validated)
+ return json.dumps(validated.model_dump(mode="json"))
+
+ @self.ui(
+ name=provider._tool_name,
+ description=(
+ f"Collect {model.__name__} information from the user via a form. "
+ f"Call this tool when you need the user to provide "
+ f"{model.__name__} data. The user will see a validated form. "
+ f"After calling this tool, STOP and wait for the user to submit."
+ ),
+ )
+ def collect_input(
+ prompt: str,
+ title: str | None = None,
+ submit_text: str | None = None,
+ default: dict[str, Any] | None = None,
+ ) -> PrefabApp:
+ """Collect structured input from the user.
+
+ Args:
+ prompt: Tell the user what you need and why.
+ title: Optional heading for the form card.
+ submit_text: Optional label for the submit button.
+ default: Optional suggested response — a partial dict of form
+ field values keyed by field name. The form renders with
+ those values pre-filled so the user can confirm or edit
+ rather than start from a blank form. Use this when you
+ already know (or can infer) what the answer should be.
+ Requires prefab-ui>=0.19.1; silently ignored on older
+ versions.
+ """
+ _title = title or provider._title
+ _submit = submit_text or provider._submit_text
+
+ with Card(css_class="max-w-lg mx-auto") as view:
+ with CardHeader():
+ H3(_title)
+
+ with CardContent(), Column(gap=4):
+ Muted(prompt)
+
+ on_success_actions: list[Any] = [
+ SetState("submitted", True),
+ ]
+ if provider._send_message:
+ on_success_actions.insert(
+ 0,
+ SendMessage(RESULT),
+ )
+
+ from_model_kwargs: dict[str, Any] = {
+ "submit_label": _submit,
+ "on_submit": [
+ CallTool(
+ "submit_form",
+ on_success=on_success_actions,
+ ),
+ ],
+ }
+ if default and _FORM_SUPPORTS_DEFAULTS:
+ from_model_kwargs["defaults"] = default
+
+ Form.from_model(model, **from_model_kwargs)
+
+ with CardFooter(), If(STATE.submitted):
+ Muted("Submitted.")
+
+ return PrefabApp(
+ view=view,
+ state={"submitted": False},
+ )
diff --git a/fastmcp_slim/fastmcp/apps/generative.py b/fastmcp_slim/fastmcp/apps/generative.py
new file mode 100644
index 000000000..0ad78f12d
--- /dev/null
+++ b/fastmcp_slim/fastmcp/apps/generative.py
@@ -0,0 +1,199 @@
+"""GenerativeUI — a Provider that adds LLM-generated UI capabilities.
+
+Registers tools and resources from ``prefab_ui.generative`` so that an
+LLM can write Prefab Python code, execute it in a sandbox, and render
+the result as a streaming interactive UI.
+
+Requires ``fastmcp[apps]`` (prefab-ui).
+
+Usage::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.generative import GenerativeUI
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(GenerativeUI())
+"""
+
+try:
+ import prefab_ui.generative as _gen
+ from prefab_ui.renderer import (
+ get_generative_renderer_csp,
+ get_generative_renderer_html,
+ )
+except ImportError as _exc:
+ raise ImportError(
+ "GenerativeUI requires prefab-ui. Install with: pip install 'fastmcp[apps]'"
+ ) from _exc
+
+import json
+from collections.abc import AsyncIterator, Sequence
+from contextlib import asynccontextmanager
+from typing import Any
+
+from fastmcp.apps.config import AppConfig, ResourceCSP, app_config_to_meta_dict
+from fastmcp.server.providers.base import Provider
+from fastmcp.server.providers.local_provider import LocalProvider
+from fastmcp.tools.base import Tool
+from fastmcp.utilities.logging import get_logger
+from fastmcp.utilities.mime import UI_MIME_TYPE
+
+logger = get_logger(__name__)
+
+
+def _build_csp() -> ResourceCSP:
+ """Build CSP from the generative renderer's declared requirements."""
+ csp = get_generative_renderer_csp()
+ return ResourceCSP(
+ resource_domains=csp.get("resource_domains"),
+ connect_domains=csp.get("connect_domains"),
+ )
+
+
+class GenerativeUI(Provider):
+ """A Provider that adds generative UI capabilities to a server.
+
+ Registers:
+
+ - A ``generate_ui`` tool that accepts Prefab Python code, executes
+ it in a Pyodide sandbox, and returns the rendered PrefabApp.
+ Supports streaming via ``ontoolinputpartial``.
+ - A ``components`` tool that searches the Prefab component library.
+ - The generative renderer resource with CSP for Pyodide CDN access.
+
+ Example::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.generative import GenerativeUI
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(GenerativeUI())
+ """
+
+ def __init__(
+ self,
+ *,
+ tool_name: str = "generate_prefab_ui",
+ include_components_tool: bool = True,
+ components_tool_name: str = "search_prefab_components",
+ ) -> None:
+ super().__init__()
+ self._tool_name = tool_name
+ self._components_tool_name = components_tool_name
+ self._include_components_tool = include_components_tool
+ self._local = LocalProvider(on_duplicate="error")
+ self._sandbox: Any = None
+ self._setup_done = False
+
+ def __repr__(self) -> str:
+ return f"GenerativeUI(tool_name={self._tool_name!r})"
+
+ def _get_sandbox(self) -> Any:
+ """Lazily create the Pyodide sandbox."""
+ if self._sandbox is None:
+ from prefab_ui.sandbox import Sandbox
+
+ self._sandbox = Sandbox()
+ return self._sandbox
+
+ def _ensure_setup(self) -> None:
+ """Lazily register tools and resources on first access."""
+ if self._setup_done:
+ return
+
+ csp = _build_csp()
+ app_config = AppConfig(resource_uri=_gen.RESOURCE_URI, csp=csp)
+
+ # -- generate_ui tool --
+ # Wraps prefab_ui.generative.execute with sandbox lifecycle management.
+
+ from prefab_ui.app import PrefabApp
+
+ sandbox_ref = self # capture for closure
+
+ async def generate_ui(
+ code: str,
+ data: str | dict[str, Any] | None = None,
+ ) -> PrefabApp:
+ parsed_data: dict[str, Any] | None
+ if isinstance(data, str):
+ parsed_data = json.loads(data) if data.strip() else None
+ else:
+ parsed_data = data
+ return await _gen.execute(
+ code,
+ data=parsed_data,
+ sandbox=sandbox_ref._get_sandbox(),
+ )
+
+ tool = Tool.from_function(
+ generate_ui,
+ name=self._tool_name,
+ description=_gen.execute.__doc__ or "",
+ meta={"ui": app_config_to_meta_dict(app_config)},
+ )
+ self._local._add_component(tool)
+
+ # -- components tool --
+
+ if self._include_components_tool:
+ components_tool = Tool.from_function(
+ _gen.search_components,
+ name=self._components_tool_name,
+ description=_gen.search_components.__doc__ or "",
+ )
+ self._local._add_component(components_tool)
+
+ # -- generative renderer resource --
+
+ from fastmcp.resources.types import TextResource
+
+ resource_config = AppConfig(csp=csp)
+ resource = TextResource(
+ uri=_gen.RESOURCE_URI, # type: ignore[arg-type]
+ name="Prefab Generative Renderer",
+ text=get_generative_renderer_html(),
+ mime_type=UI_MIME_TYPE,
+ meta={"ui": app_config_to_meta_dict(resource_config)},
+ )
+ self._local._add_component(resource)
+
+ self._setup_done = True
+
+ # ------------------------------------------------------------------
+ # Provider interface
+ # ------------------------------------------------------------------
+
+ async def _list_tools(self) -> Sequence[Tool]:
+ self._ensure_setup()
+ return await self._local._list_tools()
+
+ async def _get_tool(self, name: str, version: Any = None) -> Tool | None:
+ self._ensure_setup()
+ return await self._local._get_tool(name, version)
+
+ async def _list_resources(self) -> Sequence[Any]:
+ self._ensure_setup()
+ return await self._local._list_resources()
+
+ async def _get_resource(self, uri: str, version: Any = None) -> Any | None:
+ self._ensure_setup()
+ return await self._local._get_resource(uri, version)
+
+ async def _list_resource_templates(self) -> Sequence[Any]:
+ return []
+
+ async def _get_resource_template(self, uri: str, version: Any = None) -> Any | None:
+ return None
+
+ async def _list_prompts(self) -> Sequence[Any]:
+ return []
+
+ async def _get_prompt(self, name: str, version: Any = None) -> Any | None:
+ return None
+
+ @asynccontextmanager
+ async def lifespan(self) -> AsyncIterator[None]:
+ self._ensure_setup()
+ async with self._local.lifespan():
+ yield
diff --git a/fastmcp_slim/fastmcp/cli/__init__.py b/fastmcp_slim/fastmcp/cli/__init__.py
new file mode 100644
index 000000000..9afa62132
--- /dev/null
+++ b/fastmcp_slim/fastmcp/cli/__init__.py
@@ -0,0 +1,8 @@
+"""FastMCP CLI package."""
+
+try:
+ from .cli import app
+except ImportError as exc:
+ from fastmcp import _install_hints
+
+ raise ImportError(_install_hints.CLI_SUPPORT) from exc
diff --git a/src/fastmcp/cli/__main__.py b/fastmcp_slim/fastmcp/cli/__main__.py
similarity index 69%
rename from src/fastmcp/cli/__main__.py
rename to fastmcp_slim/fastmcp/cli/__main__.py
index aca24b145..92500fb7c 100644
--- a/src/fastmcp/cli/__main__.py
+++ b/fastmcp_slim/fastmcp/cli/__main__.py
@@ -1,5 +1,5 @@
"""FastMCP CLI as a runnable package"""
-from .cli import app
+from . import app
app()
diff --git a/fastmcp_slim/fastmcp/cli/apps_dev.py b/fastmcp_slim/fastmcp/cli/apps_dev.py
new file mode 100644
index 000000000..1c7547277
--- /dev/null
+++ b/fastmcp_slim/fastmcp/cli/apps_dev.py
@@ -0,0 +1,1874 @@
+"""Dev server for previewing FastMCPApp UIs locally.
+
+Starts the user's MCP server on a configurable port, then starts a lightweight
+Starlette dev server that:
+
+ - Serves a Prefab-based tool picker at GET /
+ - Proxies /mcp to the user's server (avoids browser CORS restrictions)
+ - Serves the AppBridge host page at GET /launch
+
+The host page uses @modelcontextprotocol/ext-apps to connect to the MCP server
+and render the selected UI tool inside an iframe.
+
+Startup sequence
+----------------
+1. Download ext-apps app-bridge.js from npm and patch its bare
+ ``@modelcontextprotocol/sdk/…`` imports to use concrete esm.sh URLs.
+2. Detect the exact Zod v4 module URL that esm.sh serves for that SDK version
+ and build an import-map entry that redirects the broken ``v4.mjs`` (which
+ only re-exports ``{z, default}``) to ``v4/classic/index.mjs`` (which
+ correctly exports every named Zod v4 function). Import maps apply to the
+ full module graph in the document, including cross-origin esm.sh modules.
+3. Serve both the patched JS and the import-map JSON from the dev server.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import contextlib
+import html
+import io
+import json
+import logging
+import os
+import re
+import signal
+import sys
+import tarfile
+import tempfile
+import time
+import webbrowser
+from pathlib import Path
+from typing import Any
+from urllib.parse import urlencode
+
+import httpcore2
+import httpx2
+import uvicorn
+from starlette.applications import Starlette
+from starlette.requests import Request
+from starlette.responses import HTMLResponse, Response, StreamingResponse
+from starlette.routing import Route
+
+from fastmcp.utilities.logging import get_logger
+
+logger = get_logger(__name__)
+
+
+def _json_for_script(value: Any) -> str:
+ """Serialize JSON for embedding inside an HTML script element."""
+ return (
+ json.dumps(value)
+ .replace("&", "\\u0026")
+ .replace("<", "\\u003c")
+ .replace(">", "\\u003e")
+ .replace("\u2028", "\\u2028")
+ .replace("\u2029", "\\u2029")
+ )
+
+
+# ---------------------------------------------------------------------------
+# MCP message log (captures proxy traffic for the dev UI log panel)
+# ---------------------------------------------------------------------------
+
+
+class _MessageLog:
+ """In-memory buffer of MCP JSON-RPC messages flowing through the proxy."""
+
+ def __init__(self) -> None:
+ self._entries: list[dict[str, Any]] = []
+ self._counter = 0
+ self._request_methods: dict[int | str, str] = {}
+ self._request_times: dict[int | str, float] = {}
+
+ def log_request(self, body: dict[str, Any]) -> None:
+ method = body.get("method", "unknown")
+ jsonrpc_id = body.get("id")
+ timestamp = time.time()
+ if jsonrpc_id is not None:
+ self._request_methods[jsonrpc_id] = method
+ self._request_times[jsonrpc_id] = timestamp
+ self._counter += 1
+ self._entries.append(
+ {
+ "id": self._counter,
+ "timestamp": timestamp,
+ "direction": "request",
+ "method": method,
+ "body": body,
+ }
+ )
+
+ def log_response(self, body: dict[str, Any]) -> None:
+ # Server-initiated notifications have "method" but no "id"
+ if "method" in body and "id" not in body:
+ self._counter += 1
+ self._entries.append(
+ {
+ "id": self._counter,
+ "timestamp": time.time(),
+ "direction": "notification",
+ "method": body.get("method", "unknown"),
+ "body": body,
+ }
+ )
+ return
+
+ jsonrpc_id = body.get("id")
+ method = (
+ self._request_methods.pop(jsonrpc_id, None)
+ if jsonrpc_id is not None
+ else None
+ )
+ request_time = (
+ self._request_times.pop(jsonrpc_id, None)
+ if jsonrpc_id is not None
+ else None
+ )
+ timestamp = time.time()
+ duration_ms = (
+ round((timestamp - request_time) * 1000, 1) if request_time else None
+ )
+ self._counter += 1
+ self._entries.append(
+ {
+ "id": self._counter,
+ "timestamp": timestamp,
+ "direction": "response",
+ "method": method,
+ "body": body,
+ "duration_ms": duration_ms,
+ }
+ )
+
+ def get_since(self, since_id: int = 0) -> list[dict[str, Any]]:
+ return [e for e in self._entries if e["id"] > since_id]
+
+ def log_bridge(self, body: dict[str, Any]) -> None:
+ method = body.get("method", "unknown")
+ self._counter += 1
+ self._entries.append(
+ {
+ "id": self._counter,
+ "timestamp": time.time(),
+ "direction": "bridge",
+ "method": method,
+ "body": body,
+ }
+ )
+
+ def clear(self) -> None:
+ self._entries.clear()
+ self._request_methods.clear()
+ self._request_times.clear()
+
+
+def _log_response_bytes(log: _MessageLog, raw: bytes, content_type: str) -> None:
+ """Parse accumulated proxy response bytes and log as message entries."""
+ if not raw:
+ return
+ try:
+ if "text/event-stream" in content_type:
+ for line in raw.decode("utf-8", errors="replace").splitlines():
+ if line.startswith("data: "):
+ with contextlib.suppress(json.JSONDecodeError):
+ log.log_response(json.loads(line[6:]))
+ else:
+ body = json.loads(raw)
+ if isinstance(body, list):
+ for item in body:
+ log.log_response(item)
+ else:
+ log.log_response(body)
+ except (json.JSONDecodeError, TypeError):
+ pass
+
+
+_EXT_APPS_VERSION = "1.0.1"
+# Pin to the SDK version ext-apps 1.0.1 was compiled against so the client
+# and transport modules are API-compatible with the app-bridge internals.
+_MCP_SDK_VERSION = "1.25.2"
+
+# ---------------------------------------------------------------------------
+# Shared AppBridge host shell
+# ---------------------------------------------------------------------------
+
+# Both the picker and the app launcher use the same host-page structure: an
+# iframe that hosts a Prefab renderer, wired to the MCP server via AppBridge.
+# The only differences are (a) which URL loads in the iframe and (b) what
+# oninitialized does.
+#
+# app-bridge.js is served locally (see _fetch_app_bridge_bundle).
+# Client/Transport are loaded from esm.sh.
+# The import map (injected as {import_map_tag}) patches the broken esm.sh
+# Zod v4 module so all Zod named exports are visible to the SDK at runtime.
+
+_HOST_SHELL = """\
+
+
+
+
+ {title}
+{import_map_tag}
+
+
+
+ {status_text}
+
+
+
+
+"""
+
+# ---------------------------------------------------------------------------
+# Host page HTML
+# ---------------------------------------------------------------------------
+
+_HOST_HTML_TEMPLATE = """\
+
+
+
+
+ FastMCP Dev — {tool_name}
+{import_map_tag}
+
+
+
+ Launching {tool_name}…
+
+
+
+
+"""
+
+# ---------------------------------------------------------------------------
+# Dev log panel (injected into host pages)
+# ---------------------------------------------------------------------------
+
+_LOG_PANEL_HTML = """\
+
+
+
+
+
+
Show
+
+ Tools
+ Logs
+ Host
+ Errors
+
+
+ Debug+
+ Info+
+ Warn+
+ Error+
+ Critical+
+
+
+
+
+MCP Log
+
+"""
+
+
+def _inject_log_panel(html: str) -> str:
+ """Inject the MCP message log panel before
prefab-ui not installed. Run: pip install 'fastmcp[apps]'