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-pr/SKILL.md b/.claude/skills/review-pr/SKILL.md
new file mode 100644
index 000000000..da5f1ff3d
--- /dev/null
+++ b/.claude/skills/review-pr/SKILL.md
@@ -0,0 +1,102 @@
+---
+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.
+
+## 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-pytest/action.yml b/.github/actions/run-pytest/action.yml
index ff429a4cc..b7e5509e5 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"
@@ -23,8 +23,13 @@ runs:
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 conformance"
TIMEOUT="5"
MAX_PROCS="4"
EXTRA_FLAGS=""
@@ -38,6 +43,7 @@ runs:
uv run --no-sync pytest \
--inline-snapshot=disable \
--timeout=$TIMEOUT \
+ --durations=50 \
-m "$MARKER" \
$PARALLEL_FLAGS \
$EXTRA_FLAGS \
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/workflows/auto-close-duplicates.yml b/.github/workflows/auto-close-duplicates.yml
index d358a3a1e..ce601c5c3 100644
--- a/.github/workflows/auto-close-duplicates.yml
+++ b/.github/workflows/auto-close-duplicates.yml
@@ -20,7 +20,7 @@ jobs:
- 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..de2fd0422 100644
--- a/.github/workflows/auto-close-needs-mre.yml
+++ b/.github/workflows/auto-close-needs-mre.yml
@@ -20,7 +20,7 @@ jobs:
- 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/martian-test-failure.yml b/.github/workflows/martian-test-failure.yml
index 9f7724fbd..5d9f7d4ae 100644
--- a/.github/workflows/martian-test-failure.yml
+++ b/.github/workflows/martian-test-failure.yml
@@ -29,7 +29,7 @@ jobs:
- 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/martian-triage-issue.yml b/.github/workflows/martian-triage-issue.yml
index 4c7ef711e..cb5c8b55d 100644
--- a/.github/workflows/martian-triage-issue.yml
+++ b/.github/workflows/martian-triage-issue.yml
@@ -33,7 +33,7 @@ jobs:
- 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..8029d4ab9 100644
--- a/.github/workflows/marvin-comment-on-issue.yml
+++ b/.github/workflows/marvin-comment-on-issue.yml
@@ -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 }}
diff --git a/.github/workflows/marvin-comment-on-pr.yml b/.github/workflows/marvin-comment-on-pr.yml
index 09f699522..9e4e4fd9d 100644
--- a/.github/workflows/marvin-comment-on-pr.yml
+++ b/.github/workflows/marvin-comment-on-pr.yml
@@ -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 }}
diff --git a/.github/workflows/marvin-dedupe-issues.yml b/.github/workflows/marvin-dedupe-issues.yml
index a71c590c0..727063f9a 100644
--- a/.github/workflows/marvin-dedupe-issues.yml
+++ b/.github/workflows/marvin-dedupe-issues.yml
@@ -25,7 +25,7 @@ jobs:
- 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-label-triage.yml b/.github/workflows/marvin-label-triage.yml
index 8a3f5f47d..0e74fedbe 100644
--- a/.github/workflows/marvin-label-triage.yml
+++ b/.github/workflows/marvin-label-triage.yml
@@ -36,7 +36,7 @@ jobs:
- 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 +49,7 @@ jobs:
PROMPT<` (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 v3.2.0 --target main --title "v3.2.0: Theme Here" --generate-notes --notes-file /tmp/release-notes.md
+```
+
+Most releases target `main`, but maintenance or backport releases may target a different branch (e.g., `release/2.x`). Confirm the target with the maintainer if there's any ambiguity.
+
+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.
+
+**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.
+
### Commit Messages and Agent Attribution
- **Agents NOT acting on behalf of @jlowin MUST identify themselves** (e.g., "🤖 Generated with Claude Code" in commits/PRs)
@@ -79,6 +103,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
@@ -102,6 +134,7 @@ When modifying MCP functionality, changes typically need to be applied across al
- 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.
- **Core Principle:** A feature doesn't exist unless it is documented!
+- When adding or modifying settings in `src/fastmcp/settings.py`, update `docs/more/settings.mdx` to match.
### Documentation Guidelines
@@ -110,6 +143,7 @@ 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.
## Critical Patterns
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 000000000..6f861c50a
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,53 @@
+# 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.
+
+## 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.
+
+## When to open a pull request
+
+An open issue is not an invitation to submit a PR. Issues track problems; whether and how to solve them is a separate decision. If you want to work on something, propose your approach in the issue first — especially for anything beyond a trivial fix.
+
+**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.** Every PR should address a tracked issue. If there isn't one, open an issue first. This isn't a permission step — you don't need to wait for a response. But the issue gives us context on the problem, and if a maintainer is already working on it, we can let you know before you invest time in code.
+- **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..47787b295 100644
--- a/README.md
+++ b/README.md
@@ -77,7 +77,13 @@ 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.
+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 how teams build MCP servers. **[Prefect Horizon](https://www.prefect.io/horizon)** is how enterprises run them in production. Register any MCP server behind a managed gateway with SSO, tool-level RBAC, audit logs, and observability. Deploy FastMCP servers and go from PR to preview in 60 seconds, then remix tools from across your registry into use-case-specific, permissioned endpoints. Horizon is everything we've learned about MCP at scale from building the world's most popular MCP framework. Free for individuals, built for teams.
+
+[Deploy FastMCP with Horizon →](https://www.prefect.io/horizon)
## Installation
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/apps/architecture.mdx b/docs/apps/architecture.mdx
new file mode 100644
index 000000000..26588c2f8
--- /dev/null
+++ b/docs/apps/architecture.mdx
@@ -0,0 +1,119 @@
+---
+title: App Architecture
+sidebarTitle: Architecture
+description: How FastMCP apps work under the hood — from Python to pixels.
+icon: sitemap
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+This page explains how Prefab apps work under the hood — how your Python code becomes an interactive UI inside a host client's conversation. You don't need any of this to build apps, but the mental model is useful when something isn't rendering the way you expect, when tool calls from the UI aren't reaching your server, or when you're building [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 in Python. 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 into it, and the renderer paints the UI. If the UI needs to call server tools, it talks back through the same `postMessage` channel.
+
+The following sections walk through 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
+
+The `app` parameter on `@mcp.tool` accepts `True`, an `AppConfig` object, 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 the tool 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 triggers registration of the shared Prefab renderer resource (discussed 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 needs to display the result.
+
+Type inference works the same way. If your return type annotation 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 underlying 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. This tag is how the server identifies which app a tool belongs to when routing calls from the UI.
+
+Second, it sets `meta["ui"]["visibility"]` to control who can see each tool. Entry points default to `["model"]` (visible to the LLM). Backend tools default to `["app"]` (visible only to the UI). Hosts use this to filter the tool list — the model sees entry points, and the UI sees backends.
+
+## Serialization
+
+When a Prefab tool runs, its return value — a `PrefabApp` or a raw `Component` — needs to become a JSON blob that the renderer can interpret.
+
+### PrefabApp.to_json()
+
+The serialization entry point is `PrefabApp.to_json()`. This method 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 component 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")` in the wire format. The resolver also handles `unwrap_result` — a flag that tells the renderer to unwrap single-value results from the `{"result": value}` envelope that FastMCP uses for schema compliance.
+
+### The _meta.fastmcp.app Tag
+
+After `to_json()` produces the JSON 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 — more on this in the next section.
+
+### 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
+
+When a host calls a tool, the server needs to find it. Normal tool calls go through the provider chain, which applies transforms (namespace prefixes, visibility filters, etc.) before resolving the tool by name. But app UI calls need a different path.
+
+### The get_app_tool Bypass
+
+Backend tools registered with `@app.tool()` 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` — but 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 method walks the provider tree directly, skipping the transform chain entirely. It finds the tool by its original registered name and verifies that its `meta["fastmcp"]["app"]` matches the expected app identity.
+
+This is why `CallTool("save_contact")` keeps working even when the server is mounted under a namespace prefix. The renderer sends the original name plus the app identity; the server uses `get_app_tool` to find the tool without transforms getting in the way.
+
+Authorization checks still apply — `get_app_tool` bypasses transforms, but it runs auth checks against the tool's `auth` configuration before executing.
+
+### Provider Delegation
+
+The `get_app_tool` method is defined on the `Provider` base class and overridden by aggregate and wrapped providers. Aggregate providers fan out the lookup across all child providers in parallel. Wrapped providers (like `FastMCPProvider`, which wraps a nested `FastMCP` server) delegate to the inner server's `get_app_tool`. This means backend tools are reachable through any depth of server 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 renderer 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, regardless of how many tools or apps are registered.
+
+The resource also carries CSP metadata (via `get_renderer_csp()`) declaring which CDN domains the renderer needs to load its JavaScript dependencies. Hosts use this to configure the iframe's Content Security Policy.
+
+### postMessage Communication
+
+The renderer lives in a sandboxed iframe. It communicates with the host using `postMessage` — the standard browser API for cross-origin iframe communication. The protocol follows the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) specification:
+
+The host pushes the tool result (including `structuredContent`) into the iframe. The renderer parses the JSON component tree, initializes state, and renders the UI. When the user interacts with the UI — submitting a form, clicking a button — and that interaction triggers a `CallTool` action, the renderer sends a `callServerTool` message back to the host via `postMessage`. The host forwards this as a regular MCP `tools/call` request to the server, including `_meta.fastmcp.app` for routing.
+
+The response flows back the same way: server to host, host to iframe via `postMessage`, 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 (like safe area insets and theme preferences). The Prefab renderer uses this SDK internally — you only interact with it directly when building [custom HTML apps](/apps/low-level).
+
+## The Dev Server
+
+`fastmcp dev apps` provides a local preview environment that simulates the host-side behavior without requiring a real MCP host client.
+
+### Proxy Architecture
+
+The dev server runs two HTTP servers. Your MCP server starts on port 8000 (configurable) 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 is important 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 blocked by the browser. The proxy makes 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 (fetched from the proxy) in an iframe, creates an AppBridge instance, and pushes the tool result into the renderer. From this point forward, the experience matches what a real host would provide — the renderer displays the UI, and any `CallTool` actions route back through the proxy to your MCP server.
+
+Auto-reload is enabled by default, so changes to your server code restart the MCP server automatically. The dev UI stays running — just re-launch the tool to see your changes.
diff --git a/docs/apps/development.mdx b/docs/apps/development.mdx
new file mode 100644
index 000000000..7045cd939
--- /dev/null
+++ b/docs/apps/development.mdx
@@ -0,0 +1,66 @@
+---
+title: Development
+sidebarTitle: Development
+description: Preview and test your app tools locally without a full MCP host.
+icon: flask
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+
+
+
+
+`fastmcp dev apps` launches a browser-based preview for your app tools. It starts your MCP server and a local dev UI side by side — you pick a tool, fill in its arguments, and see the rendered result in a new tab. No MCP host client needed.
+
+This works with both [Prefab apps](/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/apps/examples.mdx b/docs/apps/examples.mdx
new file mode 100644
index 000000000..024808f5d
--- /dev/null
+++ b/docs/apps/examples.mdx
@@ -0,0 +1,140 @@
+---
+title: Examples
+sidebarTitle: Examples
+description: Example apps you can run right now.
+icon: images
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+Every example below is a working FastMCP server you can run with `fastmcp dev apps` or connect to from any MCP host. The source is in `examples/apps/` in the repository.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Running 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 server opens an interactive browser UI where you can select a tool and provide arguments. In a real deployment, the LLM provides these arguments on the fly based on the conversation. For example, the quiz example works best when connected to an MCP host like Goose or Claude Desktop, where the LLM generates the questions itself.
+
+## Standalone Examples
+
+### 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 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. Proves that Prefab apps aren't limited to built-in components.
+
+```bash
+fastmcp dev apps examples/apps/map/map_server.py
+```
+
+## Built-in Providers
+
+These are ready-made capabilities you add with a single `add_provider()` call.
+
+### [File Upload](/apps/providers/file-upload)
+
+Drag-and-drop file upload. The user drops files, clicks Upload, and the server stores them. The LLM can list and read uploaded files through model-visible tools.
+
+```python
+from fastmcp.apps.file_upload import FileUpload
+mcp.add_provider(FileUpload())
+```
+
+### [Approval](/apps/providers/approval)
+
+Human-in-the-loop confirmation. The LLM presents what it's about to do, the user clicks Approve or Reject, and the decision flows back as a message.
+
+```python
+from fastmcp.apps.approval import Approval
+mcp.add_provider(Approval())
+```
+
+### [Choice](/apps/providers/choice)
+
+Present clickable options instead of asking users to type. Clean structured input without parsing free text.
+
+```python
+from fastmcp.apps.choice import Choice
+mcp.add_provider(Choice())
+```
+
+### [Form Input](/apps/providers/form)
+
+Generate a validated form from a Pydantic model. Submission is validated against the model before being returned.
+
+```python
+from fastmcp.apps.form import FormInput
+mcp.add_provider(FormInput(model=MyModel))
+```
+
+### [Generative UI](/apps/providers/generative)
+
+The LLM writes Prefab Python code at runtime and the result renders as a streaming interactive UI. Tailored visualizations for any data. See the [full guide](/apps/generative) for details.
+
+```python
+from fastmcp.apps.generative import GenerativeUI
+mcp.add_provider(GenerativeUI())
+```
diff --git a/docs/apps/generative.mdx b/docs/apps/generative.mdx
new file mode 100644
index 000000000..86d306dd7
--- /dev/null
+++ b/docs/apps/generative.mdx
@@ -0,0 +1,133 @@
+---
+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'
+
+
+
+Generative UI means the LLM writes the UI code at runtime. Instead of calling a pre-built tool with a fixed interface, the model writes Prefab Python code tailored to the current data and request. The user watches the UI build up in real time as the model generates code.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.apps.generative import GenerativeUI
+
+mcp = FastMCP("Prefab Studio")
+mcp.add_provider(GenerativeUI())
+```
+
+That's it. The `GenerativeUI` provider registers everything:
+
+- **`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 that lets the LLM search the Prefab component library to discover what's available
+- **The generative renderer** — a `ui://` resource with browser-side Pyodide for streaming progressive rendering
+
+## How It Works
+
+When the LLM decides to call `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 when 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 replaces the streaming preview with the final server-validated result.
+
+## What the LLM Writes
+
+The tool description includes code examples that teach the LLM 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's component library gives it charts, tables, forms, cards, badges, and layout primitives to work with.
+
+## 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 the actual 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 passed here 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 real data from earlier in the conversation to build visualizations.
+
+## Configuration
+
+`GenerativeUI` accepts 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 requires `fastmcp[apps]` which installs `prefab-ui`. The Pyodide sandbox (for server-side 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 needed.
+
+## 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 components. If the LLM tries to import an unavailable package, the sandbox will raise an `ImportError`.
+
+## Next Steps
+
+- **[GenerativeUI Provider Reference](/apps/providers/generative)** — Configuration options and quick setup
+- **[Prefab UI](/apps/prefab)** — The component library and state system the LLM writes code against
+- **[Prefab Component Reference](https://prefab.prefect.io/docs/components)** — Full component library documentation
+- **[Development](/apps/development)** — Preview generative UI 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/docs/apps/images/app-datatable.png b/docs/apps/images/app-datatable.png
new file mode 100644
index 000000000..e69de29bb
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/interactive-apps.mdx b/docs/apps/interactive-apps.mdx
new file mode 100644
index 000000000..fb2963114
--- /dev/null
+++ b/docs/apps/interactive-apps.mdx
@@ -0,0 +1,538 @@
+---
+title: FastMCPApp
+sidebarTitle: FastMCPApp
+description: Managed tool binding, visibility, and composition for apps with heavy server interaction.
+icon: puzzle-piece
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+
+[Prefab](https://prefab.prefect.io) is in early, active development — its API changes frequently and breaking changes can occur with any release. Always pin `prefab-ui` to a specific version in your dependencies.
+
+
+Any [Prefab app](/apps/prefab) can call server tools — there's nothing stopping you from using `CallTool("tool_name")` in a regular `@mcp.tool(app=True)`. But once you have multiple backend tools, the management overhead adds up: Which tools should the model see vs. only the UI? What happens to string-based tool references when servers are composed under namespaces? How do you keep things wired correctly as the app grows?
+
+`FastMCPApp` is a class that solves these problems. It gives you two decorators that work together:
+
+- **`@app.ui()`** — entry-point tools the model calls to open the app. These return a Prefab UI.
+- **`@app.tool()`** — backend tools the UI calls via `CallTool`. These do the work.
+
+Backend tools get globally stable identifiers that survive namespacing. Visibility is managed automatically — the model sees entry points, the UI sees backends. And `CallTool` accepts function references instead of strings, so references are refactorable and composition-safe.
+
+## Your First Interactive App
+
+Here's a minimal app with a form that saves data:
+
+```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])
+```
+
+When the model calls `notes_app`, the user sees a form. Submitting it calls `add_note` on the server, updates the state with the result, and shows a toast — all without leaving the UI.
+
+Let's break down the key concepts.
+
+## Entry Points: @app.ui()
+
+Entry points are what the model sees and calls to open your app. They return a Prefab UI, just like display tools:
+
+```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")
+ # ... build UI ...
+ return PrefabApp(view=view)
+```
+
+Entry points default to `visibility=["model"]` — they show up in the tool list for the LLM but aren't callable from within the app UI. They support the same options as `@mcp.tool`: `name`, `description`, `title`, `tags`, `icons`, `auth`, and `timeout`.
+
+```python
+@app.ui(title="Contact Manager", description="Open the contact management interface")
+def contact_manager() -> PrefabApp:
+ ...
+```
+
+## Backend Tools: @app.tool()
+
+Backend tools do the work. The UI calls them via `CallTool`; they run on the server and return data:
+
+```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)
+```
+
+By default, backend tools are only visible to the app UI (`visibility=["app"]`). The model doesn't see them in the tool list. 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`:
+
+```python
+@app.tool(description="Search contacts by name or email", timeout=10.0)
+def search(query: str) -> list[dict]:
+ ...
+```
+
+## Connecting UI to Backend: CallTool
+
+`CallTool` is the bridge between the UI and the server. Pass the name of a backend tool registered with `@app.tool()`:
+
+```python
+from prefab_ui.actions.mcp import CallTool
+
+# Reference a backend tool by name
+CallTool("save_contact", arguments={"name": "Alice", "email": "alice@example.com"})
+
+# Arguments can reference state with Rx
+from prefab_ui.rx import STATE
+
+CallTool("search", arguments={"query": STATE.search_term})
+```
+
+FastMCPApp resolves the name to the tool's stable global key automatically, so `CallTool("save_contact")` keeps working even when the server is mounted under a namespace.
+
+You can also pass the function directly — `CallTool(save_contact)` — which can be convenient when the tool is defined in the same file. Both forms resolve identically.
+
+### Handling Results
+
+Server calls are asynchronous. Use `on_success` and `on_error` callbacks to handle outcomes:
+
+```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 value the tool returned — available inside `on_success` callbacks. Similarly, `ERROR` (from `prefab_ui.rx`) is available inside `on_error`.
+
+Callbacks can be a single action or a list of actions. They execute in order, and an error in any action short-circuits the rest.
+
+### result_key Shorthand
+
+When a tool returns data that should replace a state key, `result_key` is a convenient shorthand for `on_success=SetState(key, RESULT)`:
+
+```python
+CallTool("list_contacts", result_key="contacts")
+
+# equivalent to:
+CallTool(
+ "list_contacts",
+ on_success=SetState("contacts", RESULT),
+)
+```
+
+## Actions
+
+`CallTool` is one of several actions available in Prefab. Actions are events attached to component handlers like `on_click`, `on_submit`, and `on_change`.
+
+### Client Actions
+
+These run instantly in the browser — no server round-trip:
+
+```python
+from prefab_ui.actions import SetState, ToggleState, AppendState, PopState, ShowToast
+
+# Set a value
+SetState("count", 42)
+
+# Toggle a boolean
+ToggleState("expanded")
+
+# Append to a list
+AppendState("items", {"name": "New Item"})
+
+# Remove by index
+PopState("items", 0)
+
+# Show a notification
+ShowToast("Done!", variant="success")
+```
+
+### Chaining Actions
+
+Pass a list to execute multiple actions in sequence:
+
+```python
+from prefab_ui.components import Button
+from prefab_ui.actions import SetState, ShowToast
+
+Button(
+ "Reset",
+ on_click=[
+ SetState("query", ""),
+ SetState("results", []),
+ ShowToast("Cleared", variant="default"),
+ ],
+)
+```
+
+### Loading States
+
+A common pattern: show a loading indicator while a server call is in flight.
+
+```python
+from prefab_ui.actions import SetState, ShowToast
+from prefab_ui.actions.mcp import CallTool
+from prefab_ui.components import Button
+from prefab_ui.rx import RESULT, 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"),
+ ],
+ ),
+ ],
+)
+
+# Pass state={"saving": False} to PrefabApp when returning
+```
+
+## Forms
+
+Forms are the most common way to collect input and send it to the server. When a form submits, all named input values are gathered and passed as arguments to the `CallTool` action.
+
+### Manual Forms
+
+Build forms with individual input components:
+
+```python
+from prefab_ui.components import Form, Input, Select, SelectOption, Textarea, Button
+from prefab_ui.actions.mcp import CallTool
+from prefab_ui.actions import ShowToast
+
+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")
+ SelectOption("Critical", value="critical")
+ Textarea(name="description", label="Description")
+ Button("Create Ticket")
+```
+
+When submitted, the CallTool receives `{"title": "...", "priority": "...", "description": "..."}` as arguments to `create_ticket`.
+
+### Pydantic Model Forms
+
+For structured data, `Form.from_model()` generates the entire form from a Pydantic model — inputs, labels, 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.actions import SetState, ShowToast
+from prefab_ui.app import PrefabApp
+from prefab_ui.rx import RESULT
+
+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:
+ """File a bug report."""
+ 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"),
+ on_error=ShowToast("Failed to submit", variant="error"),
+ ),
+ )
+ return PrefabApp(view=view)
+
+
+@app.tool()
+def create_bug(data: BugReport) -> str:
+ """Create a bug report."""
+ # save to database...
+ return f"Created: {data.title}"
+```
+
+`str` fields become text inputs, `Literal` becomes a select dropdown, `bool` becomes a checkbox. Field titles and defaults are respected.
+
+## Composition and Namespacing
+
+The reason `FastMCPApp` exists — and why you'd use it instead of plain `@mcp.tool(app=True)` with `CallTool("tool_name")` — is composition safety.
+
+When you mount a server under a namespace, tool names get prefixed:
+
+```python
+from fastmcp import FastMCP
+
+platform = FastMCP("Platform")
+platform.mount("contacts", contacts_server)
+
+# "save_contact" becomes "contacts_save_contact"
+```
+
+If your UI used `CallTool("save_contact")`, it would break — the tool is now named `contacts_save_contact`. But `CallTool(save_contact)` with a function reference resolves to a globally stable key (like `save_contact-a1b2c3d4`) that bypasses the namespace entirely.
+
+This is why `FastMCPApp` assigns global keys to backend tools, and why `CallTool` accepts function references. Your app works the same whether it's running standalone or mounted inside a larger platform.
+
+### Mounting an App
+
+`FastMCPApp` is a Provider. Add it to a server with `providers=` or `add_provider`:
+
+```python
+from fastmcp import FastMCP, FastMCPApp
+
+app = FastMCPApp("Contacts")
+
+@app.ui()
+def contact_manager() -> PrefabApp:
+ ...
+
+@app.tool()
+def save_contact(name: str, email: str) -> dict:
+ ...
+
+
+# Option 1: providers list
+mcp = FastMCP("Platform", providers=[app])
+
+# Option 2: add_provider
+mcp = FastMCP("Platform")
+mcp.add_provider(app)
+```
+
+Multiple apps can coexist on the same server:
+
+```python
+mcp = FastMCP("Platform", providers=[contacts_app, inventory_app, billing_app])
+```
+
+Each app's backend tools have their own global keys, so there's no collision even if two apps have a tool named `save`.
+
+### Running Standalone
+
+For development, `FastMCPApp` has a convenience `run()` method that wraps itself in a temporary `FastMCP` server:
+
+```python
+app = FastMCPApp("Contacts")
+# ... register tools ...
+
+if __name__ == "__main__":
+ app.run()
+```
+
+## Complete Example: Contact Manager
+
+This pulls together everything — entry points, backend tools, callable references, forms (both manual and Pydantic), state management, and actions:
+
+```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
+
+# Data
+
+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
+
+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()
+```
+
+This example is also available as a runnable server at `examples/apps/contacts/contacts_server.py`.
+
+## Next Steps
+
+- **[Prefab Apps](/apps/prefab)** — Components, state, and reactive displays (the building blocks)
+- **[Patterns](/apps/patterns)** — Copy-paste examples for common UIs
+- **[Development](/apps/development)** — Preview and test app tools locally
+- **[Prefab UI Docs](https://prefab.prefect.io)** — Full component reference and advanced patterns
diff --git a/docs/apps/low-level.mdx b/docs/apps/low-level.mdx
index 944e48498..adda74799 100644
--- a/docs/apps/low-level.mdx
+++ b/docs/apps/low-level.mdx
@@ -27,7 +27,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")
@@ -47,7 +47,7 @@ def chart_view() -> str:
`AppConfig` controls how a tool or resource participates in the Apps extension. Import it from `fastmcp.server.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:
@@ -145,6 +145,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 +160,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 +187,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",
@@ -214,7 +216,7 @@ 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
mcp = FastMCP("QR Code Server")
@@ -290,7 +292,7 @@ Not all hosts support the Apps extension. You can check at runtime using the too
```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..cb3c7c1ea 100644
--- a/docs/apps/overview.mdx
+++ b/docs/apps/overview.mdx
@@ -10,67 +10,172 @@ import { VersionBadge } from '/snippets/version-badge.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.
+MCP tools normally return text. That works for answers, but not for data the user wants to *explore* — a revenue chart they can hover over, a sortable employee directory, a form that submits structured input. MCP Apps let your tools return interactive UIs rendered right inside the conversation.
-FastMCP implements the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) and provides two approaches:
+
+
+
-## Prefab Apps (Recommended)
+FastMCP builds on the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) with [Prefab](https://prefab.prefect.io), a Python component library that compiles to interactive UIs. You write Python; the user sees charts, tables, forms, and dashboards.
+
+
+The examples throughout the Apps docs require the `apps` extra:
+
+```bash
+pip install "fastmcp[apps]"
+```
+
+This installs [Prefab UI](https://prefab.prefect.io), the component library used to build app UIs.
+
+
+
+FastMCP pins a **minimum** version of `prefab-ui` for compatibility but intentionally does **not** pin an upper bound. Prefab is a rapidly evolving library with frequent breaking changes. If you are deploying to production, you **must** pin `prefab-ui` to a specific version in your own dependencies. Without a pin, a fresh deploy could pull a newer Prefab version that changes component APIs, breaking your app.
+
+
+## Which Approach?
+
+Most apps start with **[Prefab Apps](/apps/prefab)** — add `app=True` to a tool and return components. That covers charts, tables, dashboards, and client-side interactivity.
+
+When your UI needs multiple backend tools with managed visibility and composition safety, use **[FastMCPApp](/apps/interactive-apps)**.
+
+When you want the LLM to design the UI at runtime, use **[Generative UI](/apps/generative)**.
+
+When you need your own HTML/JS (maps, 3D, video), use **[Custom HTML](/apps/low-level)**.
+
+FastMCP also includes ready-made **[app providers](/apps/providers/approval)** that add common capabilities with a single `add_provider()` call.
+
+## Building Apps
+
+### Prefab Apps
-
-[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.
-
-
-[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).
+The quickest way to give a tool a visual UI. Add `app=True` to any tool and return a Prefab component — when the host calls it, the user sees an interactive UI instead of a JSON blob:
```python
-from prefab_ui.components import Column, Heading, BarChart, ChartSeries
from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column, Heading
+from prefab_ui.components.charts import BarChart, ChartSeries
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)
+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},
+ ]
with Column(gap=4, css_class="p-6") as view:
- Heading(f"{year} Sales")
+ Heading(f"{year} Revenue")
BarChart(
data=data,
series=[ChartSeries(data_key="revenue", label="Revenue")],
- x_axis="month",
+ x_axis="quarter",
)
return PrefabApp(view=view)
```
-Install with `pip install "fastmcp[apps]"` and see [Prefab Apps](/apps/prefab) for the integration guide.
+Prefab apps aren't limited to static displays. Prefab's state system and client-side actions (toggles, tabs, conditionals) all work. You can even call other tools from the UI using `CallTool`. There's no hard wall on what a Prefab app can do.
-## Custom HTML Apps
+See [Prefab Apps](/apps/prefab) for the full guide.
-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.
+### FastMCPApp
-This is the right choice for custom rendering (maps, 3D, video), specific JavaScript frameworks, or capabilities beyond what the component library offers.
+
+
+When your app has a lot of server-side interaction — forms that save data, search that queries a database, multi-step workflows — managing the connection between UI and backend tools gets complicated fast. Which tools should the model see vs. only the UI? What happens to tool references when servers are composed under namespaces? How do you keep `CallTool("save_contact")` working when the tool name changes?
+
+`FastMCPApp` is a class that solves these problems. It gives you two decorators that work together:
+
+- **`@app.ui()`** — entry-point tools the model calls to open the app
+- **`@app.tool()`** — backend tools the UI calls via `CallTool`
+
+Backend tools get stable identifiers that survive namespacing, visibility is managed automatically (the model sees entry points, the UI sees backends), and `CallTool` accepts tool names that resolve correctly regardless of how servers are composed:
+
+```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 (
+ Column, Heading, Form, Input, Button, ForEach, Row, Text, Badge, Separator,
+)
+from prefab_ui.rx import RESULT
+from fastmcp import FastMCP, FastMCPApp
+
+app = FastMCPApp("Contacts")
+
+
+@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)
+
+
+@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):
+ Text(contact.name)
+ Badge(contact.email)
+ Separator()
+ with Form(
+ on_submit=CallTool(
+ "save_contact",
+ on_success=[
+ SetState("contacts", RESULT),
+ ShowToast("Saved!", variant="success"),
+ ],
+ )
+ ):
+ Input(name="name", label="Name", required=True)
+ Input(name="email", label="Email", required=True)
+ Button("Save")
+
+ return PrefabApp(view=view, state={"contacts": list(db)})
+
+
+mcp = FastMCP("Server", providers=[app])
+```
+
+You *can* build server-interactive UIs without `FastMCPApp` — it's all the same protocol underneath. But once you have multiple tools, composition concerns, or visibility requirements, `FastMCPApp` handles the complexity so you don't have to.
+
+See [FastMCPApp](/apps/interactive-apps) for the full guide.
+
+### Generative UI
+
+
+
+Instead of pre-building a UI, the LLM can write one from scratch. The `GenerativeUI` provider registers tools that let the model write Prefab Python code, execute it in a sandbox, and render the result — with streaming so the user watches the UI build up in real time.
```python
from fastmcp import FastMCP
-from fastmcp.server.apps import AppConfig, ResourceCSP
+from fastmcp.apps.generative import GenerativeUI
-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 = FastMCP("Prefab Studio")
+mcp.add_provider(GenerativeUI())
```
-See [Custom HTML Apps](/apps/low-level) for the full reference.
+See [Generative UI](/apps/generative) for the full guide, or the [provider reference](/apps/providers/generative) for configuration options.
+
+### Custom HTML
+
+All the approaches above use [Prefab UI](https://prefab.prefect.io) to build UIs in pure Python. If you need full control — your own HTML, CSS, JavaScript, a specific framework — you can use the [MCP Apps extension directly](/apps/low-level). You write the HTML yourself and communicate with the host via the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) SDK.
+
+## Previewing Apps Locally
+
+The `fastmcp dev apps` command launches a browser-based preview for your app tools — no MCP host client needed. See [Development](/apps/development).
+
+```bash
+fastmcp dev apps server.py
+```
diff --git a/docs/apps/patterns.mdx b/docs/apps/patterns.mdx
index ffc699f89..b0ff80376 100644
--- a/docs/apps/patterns.mdx
+++ b/docs/apps/patterns.mdx
@@ -1,30 +1,29 @@
---
title: Patterns
sidebarTitle: Patterns
-description: Charts, tables, forms, and other common tool UIs.
+description: Copy-paste examples for common tool UIs.
icon: grid-2-plus
-tag: SOON
+tag: NEW
---
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.
-
+Each pattern below is a complete, copy-pasteable tool. They're organized by what you're building — pick the one closest to your use case, paste it, and adapt.
-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.
+For the full set of available components — layout containers, form controls, overlays, and more — see the [Prefab component reference](https://prefab.prefect.io/docs/components).
## 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.
+Prefab includes [bar, line, area, pie, radar, and radial charts](https://prefab.prefect.io/docs/components/charts). They 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 prefab_ui.components import Column, Heading
+from prefab_ui.components.charts import BarChart, ChartSeries
from fastmcp import FastMCP
mcp = FastMCP("Charts")
@@ -59,11 +58,12 @@ Multiple `ChartSeries` entries plot different data keys. Add `stacked=True` to s
### Area Chart
-`LineChart` and `AreaChart` share the same API as `BarChart`, with `curve` for interpolation (`"linear"`, `"smooth"`, `"step"`) and `show_dots` for data points:
+`LineChart` and `AreaChart` share the same API as `BarChart`, with `curve` for interpolation and `show_dots` for data points:
```python
-from prefab_ui.components import Column, Heading, AreaChart, ChartSeries
from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column, Heading
+from prefab_ui.components.charts import AreaChart, ChartSeries
from fastmcp import FastMCP
mcp = FastMCP("Charts")
@@ -95,11 +95,12 @@ def usage_trend() -> PrefabApp:
### 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:
+`PieChart` uses `data_key` (the numeric value) and `name_key` (the label). Set `inner_radius` for a donut:
```python
-from prefab_ui.components import Column, Heading, PieChart
from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column, Heading
+from prefab_ui.components.charts import PieChart
from fastmcp import FastMCP
mcp = FastMCP("Charts")
@@ -130,11 +131,11 @@ def ticket_breakdown() -> PrefabApp:
## 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.
+[DataTable](https://prefab.prefect.io/docs/components/data-display/data-table) provides sortable columns, full-text search, and pagination — all client-side:
```python
-from prefab_ui.components import Column, Heading, DataTable, DataTableColumn
from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column, Heading, DataTable, DataTableColumn
from fastmcp import FastMCP
mcp = FastMCP("Directory")
@@ -161,7 +162,7 @@ def employee_directory() -> PrefabApp:
DataTableColumn(key="location", header="Office", sortable=True),
],
rows=employees,
- searchable=True,
+ search=True,
paginated=True,
page_size=15,
)
@@ -169,133 +170,16 @@ def employee_directory() -> PrefabApp:
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.
+Cards, badges, progress bars, and grids combine naturally for dashboards:
```python
+from prefab_ui.app import PrefabApp
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")
@@ -319,9 +203,7 @@ def system_status() -> PrefabApp:
"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():
@@ -338,13 +220,16 @@ def system_status() -> PrefabApp:
return PrefabApp(view=view)
```
-## Conditional Content
+## Reactive Displays
-[`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.
+These patterns use state and `Rx()` for client-side interactivity — no server calls needed.
+
+### Feature Toggles
```python
-from prefab_ui.components import Column, Heading, Switch, Separator, Alert, If
from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column, Heading, Switch, Alert, If, Separator
+from prefab_ui.rx import Rx
from fastmcp import FastMCP
mcp = FastMCP("Flags")
@@ -355,47 +240,41 @@ 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")
-
+ Switch(name="beta", label="Beta Features")
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")):
Alert(
title="Beta features active",
description="Experimental features are now visible.",
variant="warning",
)
- return PrefabApp(view=view, state={"dark_mode": False, "beta_features": False})
+ return PrefabApp(view=view, state={"dark_mode": False, "beta": False})
```
-## Tabs
-
-[Tabs](https://prefab.prefect.io/docs/components/containers/tabs) organize content into switchable views. Switching is client-side — no server round-trip.
+### Tabs
```python
+from prefab_ui.app import PrefabApp
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:
+def project_overview() -> 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"},
@@ -408,13 +287,11 @@ def project_overview(project_id: str) -> PrefabApp:
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(
@@ -426,24 +303,22 @@ def project_overview(project_id: str) -> 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"]})
```
-## Accordion
-
-[Accordion](https://prefab.prefect.io/docs/components/containers/accordion) collapses sections to save space. `multiple=True` lets users expand several items at once:
+### Accordion
```python
+from prefab_ui.app import PrefabApp
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")
@@ -461,7 +336,6 @@ def api_health() -> PrefabApp:
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"]):
@@ -477,7 +351,81 @@ def api_health() -> PrefabApp:
return PrefabApp(view=view)
```
+## Interactive Patterns
+
+These patterns call server tools. For context on `FastMCPApp`, `@app.tool()`, and `CallTool`, see [FastMCPApp](/apps/interactive-apps).
+
+### Contact Form
+
+```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, Muted, Row, Select, SelectOption, Separator, Text, Textarea,
+)
+from prefab_ui.rx import RESULT
+from fastmcp import FastMCP, FastMCPApp
+
+app = FastMCPApp("Contacts")
+
+contacts_db: list[dict] = [
+ {"name": "Zaphod Beeblebrox", "email": "zaphod@galaxy.gov", "category": "Partner"},
+]
+
+
+@app.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})
+ return list(contacts_db)
+
+
+@app.ui()
+def contact_form() -> PrefabApp:
+ """Contact list with an add form."""
+ 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()
+
+ with Form(
+ on_submit=CallTool(
+ "save_contact",
+ on_success=[
+ SetState("contacts", RESULT),
+ ShowToast("Contact saved!", variant="success"),
+ ],
+ on_error=ShowToast("Failed to save", variant="error"),
+ )
+ ):
+ Input(name="name", label="Full Name", required=True)
+ Input(name="email", label="Email", input_type="email", required=True)
+ with Select(name="category", label="Category"):
+ SelectOption("Customer", value="Customer")
+ SelectOption("Vendor", value="Vendor")
+ SelectOption("Partner", value="Partner")
+ SelectOption("Other", value="Other")
+ Textarea(name="notes", label="Notes", placeholder="Optional notes...")
+ Button("Save Contact")
+
+ return PrefabApp(view=view, state={"contacts": list(contacts_db)})
+
+
+mcp = FastMCP("Server", providers=[app])
+```
+
## 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
+- **[FastMCPApp](/apps/interactive-apps)** — Managed tool binding for server-connected UIs
+- **[Development](/apps/development)** — Preview app tools locally with `fastmcp dev apps`
+- **[Prefab UI Docs](https://prefab.prefect.io)** — Full component reference, layout guides, and more
diff --git a/docs/apps/prefab.mdx b/docs/apps/prefab.mdx
index 907670d4b..156767869 100644
--- a/docs/apps/prefab.mdx
+++ b/docs/apps/prefab.mdx
@@ -1,44 +1,33 @@
---
-title: Prefab Apps
-sidebarTitle: Prefab Apps
-description: Build interactive tool UIs in pure Python — no HTML or JavaScript required.
+title: Prefab UI
+sidebarTitle: Prefab UI
+description: The component library behind FastMCP apps — charts, tables, dashboards, forms, and reactive displays.
icon: palette
-tag: SOON
+tag: NEW
---
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 (see below).
-
+
+[Prefab](https://prefab.prefect.io) is in early, active development — breaking changes can occur with any release. FastMCP pins a minimum version of `prefab-ui` for compatibility but does not pin an upper bound. If you are deploying to production, **pin `prefab-ui` to a specific version** in your own dependencies.
+
-[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 UI](https://prefab.prefect.io) is the component library behind all FastMCP app features. You describe layouts, charts, tables, and forms in Python, and Prefab compiles them to interactive UIs that render in the host's conversation.
-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.
+The simplest way to use it: add `app=True` to a tool and return Prefab components. The host renders an interactive UI instead of text. This works for everything from static charts to reactive dashboards with client-side state — no server round-trips needed.
-```bash
-pip install "fastmcp[apps]"
-```
+For apps that need server interaction (forms, search, CRUD), see [FastMCPApp](/apps/interactive-apps) which adds managed tool binding on top of Prefab UI. For LLM-generated UIs, see [Generative UI](/apps/generative).
-
-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.
+## Getting Started
-```toml
-# pyproject.toml
-dependencies = [
- "fastmcp[apps]",
- "prefab-ui==0.8.0", # pin to a known working version
-]
-```
-
-
-Here's the simplest possible Prefab App — a tool that returns a bar chart:
+Here's 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 Column, Heading
+from prefab_ui.components.charts import BarChart, ChartSeries
from fastmcp import FastMCP
mcp = FastMCP("Dashboard")
@@ -65,67 +54,240 @@ def revenue_chart(year: int) -> PrefabApp:
return PrefabApp(view=view)
```
-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.
+The `app=True` flag tells FastMCP this tool returns a UI. When a host calls the tool, the user sees an interactive chart instead of a JSON blob. The [Patterns](/apps/patterns) page has more examples.
-## What You Return
+## Layout and Components
-### Components
-
-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:
+Prefab uses Python's `with` statement to express nesting. Containers like `Column`, `Row`, and `Grid` collect their children automatically:
```python
-from prefab_ui.components import Column, Heading, Badge
-from fastmcp import FastMCP
+from prefab_ui.components import (
+ Column, Row, Grid, Heading, Text, Muted, Badge,
+ Card, CardContent, Separator,
+)
-mcp = FastMCP("Status")
-
-
-@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
+with Column(gap=4, css_class="p-6") as view:
+ Heading("Team Status")
+ Separator()
+ with Grid(columns=2, gap=4):
+ with Card():
+ with CardContent():
+ Text("API Gateway", css_class="font-medium")
+ Badge("healthy", variant="success")
+ with Card():
+ with CardContent():
+ Text("Cache", css_class="font-medium")
+ Badge("degraded", variant="destructive")
```
-Want a chart? Return a chart. Want a table? Return a table. FastMCP handles the wiring.
-
-### PrefabApp
-
-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:
+You can also use Python loops to generate components at build time:
+
+```python
+services = [
+ {"name": "API", "status": "healthy", "ok": True},
+ {"name": "Cache", "status": "degraded", "ok": False},
+]
+
+with Grid(columns=2, gap=4):
+ for svc in services:
+ with Card():
+ with CardContent():
+ Text(svc["name"])
+ Badge(
+ svc["status"],
+ variant="success" if svc["ok"] else "destructive",
+ )
+```
+
+Build-time loops produce static content — the data is baked into the component tree at construction time. For dynamic iteration over state that changes at render time, use `ForEach` (covered below).
+
+The full component library — layout containers, data display, charts, forms, overlays — is documented in the [Prefab component reference](https://prefab.prefect.io/docs/components).
+
+## State and Reactivity
+
+Display tools can be interactive without calling the server. The key is **state** — a client-side key-value store that lives in the browser. Components read from state, actions mutate it, and the UI re-renders automatically.
+
+### Declaring State
+
+Pass a `state` dict to `PrefabApp` to declare initial state, then use `Rx("key")` to create reactive references:
```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 prefab_ui.components import Column, Heading, Switch, Alert, If
+from prefab_ui.rx import Rx
from fastmcp import FastMCP
-mcp = FastMCP("Demo")
+mcp = FastMCP("Flags")
@mcp.tool(app=True)
-def toggle_demo() -> PrefabApp:
- """Interactive toggle with state."""
+def feature_flags() -> PrefabApp:
+ """Toggle feature flags with live preview."""
with Column(gap=4, css_class="p-6") as view:
- Button("Toggle", on_click=ToggleState("show"))
- with If("{{ show }}"):
- Badge("Visible!", variant="success")
+ Heading("Feature Flags")
+ Switch(name="dark_mode", label="Dark Mode")
+ Switch(name="beta", label="Beta Features")
- return PrefabApp(view=view, state={"show": False})
+ with If(Rx("dark_mode")):
+ Alert(title="Dark mode enabled")
+ with If(Rx("beta")):
+ Alert(title="Beta features active", variant="warning")
+
+ return PrefabApp(view=view, state={"dark_mode": False, "beta": False})
```
-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.
+Three things to notice here:
-### ToolResult
+The `state` dict on `PrefabApp` declares the keys and their starting values. `Rx("dark_mode")` creates a reactive reference that compiles to `{{ dark_mode }}` in the wire protocol.
-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.
+Interactive components with a `name` prop automatically bind to state. The `Switch(name="dark_mode")` syncs its on/off value to the `dark_mode` state key on every toggle — no event wiring needed.
-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:
+`If(Rx("dark_mode"))` shows its children only when the state key is truthy. When the switch flips, the condition re-evaluates instantly in the browser.
+
+### Reactive References with Rx
+
+The `Rx` class is how you reference state in component props:
+
+```python
+from prefab_ui.rx import Rx
+
+count = Rx("count")
+```
+
+Rx objects support arithmetic, comparisons, and formatting — they compile to expressions the renderer evaluates at render time:
```python
-from prefab_ui.components import Column, Heading, BarChart, ChartSeries
from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column, Text, Slider
+from prefab_ui.rx import Rx
+from fastmcp import FastMCP
+
+mcp = FastMCP("Calculator")
+
+
+@mcp.tool(app=True)
+def tip_calculator() -> PrefabApp:
+ """Calculate tip with a slider."""
+ tip_pct = Rx("tip_pct")
+ bill = Rx("bill")
+
+ tip_amount = tip_pct / 100 * bill
+ total = bill + tip_amount
+
+ with Column(gap=4, css_class="p-6") as view:
+ Slider(name="bill", label="Bill Amount", min=0, max=500, step=0.5)
+ Slider(name="tip_pct", label="Tip %", min=0, max=50)
+ Text(f"Tip: {tip_amount.currency()}")
+ Text(f"Total: {total.currency()}")
+
+ return PrefabApp(view=view, state={"bill": 50.00, "tip_pct": 18})
+```
+
+`Rx("tip_pct") / 100 * Rx("bill")` builds a compound expression — it doesn't do the math in Python. The renderer evaluates it live as the sliders move. The `.currency()` pipe formats the result as currency.
+
+#### Pipes
+
+Rx objects support formatting pipes that transform values at render time:
+
+```python
+from prefab_ui.rx import Rx
+
+price = Rx("price")
+ratio = Rx("ratio")
+name = Rx("name")
+
+price.currency() # $42.50
+price.currency("EUR") # EUR format
+ratio.percent() # 85%
+name.upper() # ALICE
+name.truncate(10) # alice (or truncated if longer)
+```
+
+Number pipes include `currency`, `percent`, `number`, `compact`, `round`, and `abs`. String pipes include `upper`, `lower`, and `truncate`. See the [Prefab expression docs](https://prefab.prefect.io/docs/concepts/expressions) for the full list.
+
+#### Conditionals
+
+The `.then()` method creates ternary expressions:
+
+```python
+from prefab_ui.rx import Rx
+
+connected = Rx("connected")
+
+Badge(
+ connected.then("Online", "Offline"),
+ variant=connected.then("success", "destructive"),
+)
+```
+
+### Dynamic Iteration with ForEach
+
+Python `for` loops generate static content at build time. When you need to iterate over state that can change — a list that grows, items that get filtered — use `ForEach`:
+
+```python
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column, Heading, ForEach, Row, Text, Badge
+from fastmcp import FastMCP
+
+mcp = FastMCP("Directory")
+
+
+@mcp.tool(app=True)
+def team_list() -> PrefabApp:
+ """Show the current team."""
+ members = [
+ {"name": "Alice", "role": "Engineering"},
+ {"name": "Bob", "role": "Design"},
+ ]
+
+ with Column(gap=4, css_class="p-6") as view:
+ Heading("Team")
+ with ForEach("members") as member:
+ with Row(gap=2, align="center"):
+ Text(member.name, css_class="font-medium")
+ Badge(member.role)
+
+ return PrefabApp(view=view, state={"members": members})
+```
+
+`ForEach("members")` iterates over the `members` state key. The `as member` gives you an Rx proxy scoped to each item, so `member.name` resolves to `{{ $item.name }}` in the wire protocol. If the `members` state changes (e.g., through an action), the list re-renders automatically.
+
+### Conditional Rendering
+
+`If`, `Elif`, and `Else` control what's visible based on state:
+
+```python
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column, Select, SelectOption, If, Elif, Else, Text
+from prefab_ui.rx import Rx
+
+tier = Rx("tier")
+
+with Column(gap=4) as view:
+ with Select(name="tier", label="Plan"):
+ SelectOption("Free", value="free")
+ SelectOption("Pro", value="pro")
+ SelectOption("Enterprise", value="enterprise")
+ with If(tier == "enterprise"):
+ Text("Full access to all features")
+ with Elif(tier == "pro"):
+ Text("Advanced features unlocked")
+ with Else():
+ Text("Basic features only")
+
+# Pass state={"tier": "free"} to PrefabApp when returning
+```
+
+Changes are instant — switching the dropdown re-evaluates the conditions in the browser.
+
+## Giving the LLM Context
+
+By default, Prefab sends `"[Rendered Prefab UI]"` as the text content for the LLM. If the model needs to reason about the data, wrap your return in a `ToolResult` with a meaningful summary:
+
+```python
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column, Heading
+from prefab_ui.components.charts import BarChart, ChartSeries
from fastmcp import FastMCP
from fastmcp.tools import ToolResult
@@ -148,11 +310,28 @@ 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 LLM sees the summary string.
-## Type Inference
+## Advanced
-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:
+
+`app=True` auto-wires the Prefab renderer with default CSP settings. If your app loads external resources — embedding iframes, fetching from APIs, loading scripts — use `PrefabAppConfig` to 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`. It auto-sets the renderer URI and merges the renderer's CSP with any additional domains you provide.
+
+
+
+If your return type annotation is a Prefab type — `PrefabApp`, `Component`, or unions containing them — FastMCP enables app rendering automatically, even without `app=True`:
```python
@mcp.tool
@@ -160,24 +339,14 @@ 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`).
+Explicit `app=True` is recommended for clarity.
+
-## 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:
+
+Prefab tools and [custom HTML tools](/apps/low-level) coexist on the same server:
```python
-from fastmcp.server.apps import AppConfig
+from fastmcp.apps import AppConfig
@mcp.tool(app=True)
def team_directory() -> PrefabApp:
@@ -187,9 +356,11 @@ def team_directory() -> PrefabApp:
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/interactive-apps)** — Managed tool binding for apps with heavy server interaction
+- **[Patterns](/apps/patterns)** — Charts, tables, dashboards, and other common examples
+- **[Development](/apps/development)** — Preview app tools locally with `fastmcp dev apps`
+- **[Prefab UI Docs](https://prefab.prefect.io)** — Full component reference, advanced state patterns, and more
diff --git a/docs/apps/providers/approval.mdx b/docs/apps/providers/approval.mdx
new file mode 100644
index 000000000..15b683d15
--- /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..71672a95c
--- /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..13cf2402e
--- /dev/null
+++ b/docs/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/apps/providers/form.mdx b/docs/apps/providers/form.mdx
new file mode 100644
index 000000000..ca598f33f
--- /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/providers/generative.mdx b/docs/apps/providers/generative.mdx
new file mode 100644
index 000000000..a0795c939
--- /dev/null
+++ b/docs/apps/providers/generative.mdx
@@ -0,0 +1,74 @@
+---
+title: Generative UI
+sidebarTitle: Generative UI
+description: Let the LLM generate custom UIs at runtime
+icon: wand-magic-sparkles
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+`GenerativeUI` lets the LLM write Prefab Python code at runtime and render it as a streaming interactive UI. Instead of calling pre-built tools with fixed interfaces, the model creates tailored visualizations for whatever data it's working with.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.apps.generative import GenerativeUI
+
+mcp = FastMCP("My Server")
+mcp.add_provider(GenerativeUI())
+```
+
+This registers:
+
+| Component | Type | Purpose |
+|-----------|------|---------|
+| `generate_prefab_ui` | Tool | Accepts Python code, executes in Pyodide sandbox, renders result |
+| `search_prefab_components` | Tool | Lets the LLM discover available Prefab components |
+| Generative renderer | Resource | `ui://` resource with browser-side Pyodide for streaming |
+
+The LLM writes real Python — loops, f-strings, computation — using Prefab's component library (charts, tables, forms, cards, layout primitives). As the model generates tokens, the host streams partial code to the renderer via `ontoolinputpartial`, so the user watches the UI build up in real time.
+
+## Configuration
+
+```python
+GenerativeUI(
+ tool_name="generate_prefab_ui", # Rename the generation tool
+ components_tool_name="search_prefab_components", # Rename the search tool
+ include_components_tool=True, # Set False to omit the search tool
+)
+```
+
+## What the LLM Sees
+
+The tool description includes code examples that teach the LLM the Prefab patterns. The LLM calls `generate_prefab_ui` with a `code` argument containing Prefab Python, and optionally a `data` argument to pass in real data from the conversation:
+
+```python
+# The LLM generates something like:
+generate_prefab_ui(
+ code="""
+from prefab_ui.components import Column, Heading
+from prefab_ui.components.charts import BarChart, ChartSeries
+from prefab_ui.app import PrefabApp
+
+with PrefabApp() as app:
+ with Column(gap=4):
+ Heading("Revenue")
+ BarChart(data=data, series=[ChartSeries(data_key="revenue")], x_axis="quarter")
+""",
+ data={"data": [{"quarter": "Q1", "revenue": 42000}, ...]}
+)
+```
+
+The component search tool lets the LLM discover what's available before writing code — `search_prefab_components("Chart")` returns matching components with import paths.
+
+## Requirements
+
+Requires `fastmcp[apps]` (installs `prefab-ui`). The Pyodide sandbox for server-side validation requires Deno, which installs automatically on first use. The streaming renderer loads Pyodide from CDN in the browser — CSP is configured automatically.
+
+The sandbox includes the Python standard library and Prefab. External packages (NumPy, pandas, etc.) are not available.
+
+## Learn More
+
+The full **[Generative UI guide](/apps/generative)** covers the streaming mechanics in detail, how to pass data, the component search tool, and sandbox limitations.
diff --git a/docs/apps/quickstart.mdx b/docs/apps/quickstart.mdx
new file mode 100644
index 000000000..91265f884
--- /dev/null
+++ b/docs/apps/quickstart.mdx
@@ -0,0 +1,208 @@
+---
+title: Quickstart
+sidebarTitle: Quickstart
+description: Build your first MCP app in under a minute.
+icon: rocket
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+MCP tools normally return text. FastMCP apps return interactive UIs rendered directly in the conversation: charts, tables, forms, dashboards. The easiest way to build one is with [Prefab UI](https://prefab.prefect.io), a Python component library designed for exactly this. You describe the UI in Python; Prefab compiles it to something the host can render.
+
+This tutorial builds a working app from scratch. Here's what you'll have in about a minute:
+
+
+
+
+
+## Setup
+
+Install FastMCP with the `apps` extra, which pulls in Prefab UI:
+
+```bash
+pip install "fastmcp[apps]"
+```
+
+## A Tool That Returns a UI
+
+When your tool has something to *show* (a table of results, a chart, a status dashboard) you can return an interactive UI instead of text. Build the visualization with Prefab components, return it from your tool, and set `app=True` so FastMCP knows to render it. The user sees a live, interactive widget right in the conversation instead of a wall of JSON.
+
+Create `server.py`:
+
+```python server.py expandable
+from collections import Counter
+
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column, Grid, Heading, DataTable, DataTableColumn
+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"):
+ Heading("Team Directory")
+ 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
+```
+
+That `app=True` is doing a lot behind the scenes. It tells FastMCP to set up everything the MCP Apps protocol requires: the renderer resource, the content security policy, the metadata that tells the host "this tool returns a UI." Without it, you'd wire all of that up by hand. With it, you just return Prefab components and FastMCP handles the rest. The host (Claude Desktop, Goose, etc.) loads the result in a sandboxed iframe where the user can sort columns, search, and interact, all client-side with no round-trips to your server.
+
+The Prefab code itself reads top-to-bottom like a document. `PrefabApp()` is the root container and everything inside its `with` block becomes the app's UI. `Column` arranges children vertically. `Heading` renders a title. `DataTable` takes rows of data and column definitions, and gives you sorting and search for free. The `with` blocks establish parent-child relationships: nesting components inside each other builds the layout tree.
+
+## Running It
+
+FastMCP includes a dev server that renders your app tools in a browser, no MCP host needed:
+
+```bash
+fastmcp dev apps server.py
+```
+
+This opens `http://localhost:8080` where you can pick a tool and see the rendered UI. Try sorting the table columns and typing in the search box.
+
+## Making It Interactive
+
+The table above is a static snapshot that renders once from the data your Python code provides. But Prefab apps can also respond to user interaction in real time, without any server round-trips.
+
+The key concept is **state**: a client-side key-value store that components read from and write to. When the user interacts with a component, it updates state. Other components that reference that state re-render instantly. See the [Prefab state docs](https://prefab.prefect.io/docs/concepts/state) for the full guide.
+
+Here's the same directory, but now clicking a row shows that person's details in a card:
+
+
+
+
+
+```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 (
+ Card, CardContent, CardHeader, Column, Grid, H3, Heading, Muted,
+ Row, DataTable, DataTableColumn, Badge, 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},
+ {"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()
+]
+
+
+@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"):
+ Heading("Team Directory")
+ 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 here:
+
+**`SetState` + `on_row_click`** is the interaction. When the user clicks a table row, `SetState("selected", Rx("$event"))` writes the clicked row's data into the `selected` state key. `$event` is a special variable that contains the event payload (in this case, the row dict).
+
+**`Rx("selected.name")`** reads from state reactively. It doesn't hold a Python value. It compiles to a browser-side expression that re-evaluates live whenever `selected` changes. So `Text(Rx("selected.name"))` always shows the name of whoever was last clicked.
+
+**`If(STATE.selected)`** conditionally renders the detail card only when something has been selected. Before any click, `selected` is `None` and the card is hidden.
+
+The `state` dict on `PrefabApp` sets initial values when the app loads. Run `fastmcp dev apps server.py` again and try clicking a row.
+
+## Next Steps
+
+You've built a tool that returns an interactive, reactive UI. This pattern covers a huge range of use cases: build a visualization in Prefab, return it from a tool, and the user gets dashboards, charts, data tables, and status displays right in the conversation.
+
+When you need the UI to talk back to your server (forms that save data, buttons that trigger actions, search that queries a database) you promote the tool to a **[FastMCPApp](/apps/interactive-apps)**. That gives you managed backend tools, automatic visibility control, and stable routing so your UI's button clicks reach the right server-side code.
+
+- **[Prefab UI](/apps/prefab)** covers the full component library: charts, forms, badges, progress bars, and the [reactive state system](https://prefab.prefect.io/docs/concepts/state) in depth.
+- **[FastMCPApp](/apps/interactive-apps)** is the next step when your UI needs to interact with backend logic.
+- **[App Providers](/apps/providers/approval)** are ready-made capabilities you can add with a single `add_provider()` call.
diff --git a/docs/changelog.mdx b/docs/changelog.mdx
index ae9e94fe9..d55783e9f 100644
--- a/docs/changelog.mdx
+++ b/docs/changelog.mdx
@@ -5,6 +5,86 @@ rss: true
tag: NEW
---
+
+
+**[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 +743,21 @@ Breaking changes are minimal: for most servers, updating the import statement is
+
+
+**[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)**
diff --git a/docs/cli/install-mcp.mdx b/docs/cli/install-mcp.mdx
index bf1b60b36..0171b7854 100644
--- a/docs/cli/install-mcp.mdx
+++ b/docs/cli/install-mcp.mdx
@@ -65,6 +65,7 @@ See [Server Configuration](/deployment/server-configuration) for the full config
| 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
@@ -92,6 +93,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..54783bef0 100644
--- a/docs/cli/overview.mdx
+++ b/docs/cli/overview.mdx
@@ -18,6 +18,7 @@ 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 |
diff --git a/docs/cli/running.mdx b/docs/cli/running.mdx
index dd976d561..b0cad0a0b 100644
--- a/docs/cli/running.mdx
+++ b/docs/cli/running.mdx
@@ -96,6 +96,31 @@ 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.
diff --git a/docs/clients/transports.mdx b/docs/clients/transports.mdx
index 7e3eb03db..efcda3366 100644
--- a/docs/clients/transports.mdx
+++ b/docs/clients/transports.mdx
@@ -124,6 +124,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 [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.
diff --git a/docs/deployment/http.mdx b/docs/deployment/http.mdx
index e961de9d1..d2078db1b 100644
--- a/docs/deployment/http.mdx
+++ b/docs/deployment/http.mdx
@@ -115,6 +115,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
@@ -625,18 +629,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()`**
diff --git a/docs/docs.json b/docs/docs.json
index 69b173496..747899efa 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -12,7 +12,7 @@
"decoration": "gradient"
},
"banner": {
- "content": "Deploy FastMCP servers for free on [Prefect Horizon](https://www.prefect.io/horizon)"
+ "content": "Meet [Prefect Horizon](https://prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_banner&utm_content=sitewide_banner), the enterprise MCP gateway built by the team behind FastMCP"
},
"colors": {
"dark": "#f72585",
@@ -140,8 +140,7 @@
"servers/providers/proxy",
"servers/providers/skills",
"servers/providers/custom"
- ],
- "tag": "NEW"
+ ]
},
{
"collapsed": true,
@@ -156,25 +155,30 @@
"servers/transforms/tool-search",
"servers/transforms/resources-as-tools",
"servers/transforms/prompts-as-tools"
- ],
- "tag": "NEW"
+ ]
},
{
"collapsed": true,
- "group": "Authentication",
- "icon": "key",
+ "group": "Auth",
+ "icon": "shield-check",
"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"
+ {
+ "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",
@@ -192,9 +196,44 @@
"group": "Apps",
"pages": [
"apps/overview",
- "apps/prefab",
- "apps/patterns",
- "apps/low-level"
+ "apps/quickstart",
+ "apps/examples",
+ {
+ "collapsed": true,
+ "group": "Building Apps",
+ "icon": "hammer",
+ "pages": [
+ "apps/prefab",
+ "apps/interactive-apps",
+ "apps/generative",
+ "apps/patterns"
+ ],
+ "tag": "NEW"
+ },
+ {
+ "collapsed": true,
+ "group": "Providers",
+ "icon": "layer-group",
+ "pages": [
+ "apps/providers/approval",
+ "apps/providers/choice",
+ "apps/providers/file-upload",
+ "apps/providers/form",
+ "apps/providers/generative"
+ ],
+ "tag": "NEW"
+ },
+ {
+ "collapsed": true,
+ "group": "Advanced",
+ "icon": "gear",
+ "pages": [
+ "apps/development",
+ "apps/architecture",
+ "apps/low-level"
+ ],
+ "tag": "NEW"
+ }
]
},
{
@@ -315,6 +354,7 @@
{
"group": "More",
"pages": [
+ "more/settings",
{
"collapsed": true,
"group": "Upgrading",
@@ -362,10 +402,25 @@
"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.cli",
"pages": [
"python-sdk/fastmcp-cli-__init__",
+ "python-sdk/fastmcp-cli-apps_dev",
"python-sdk/fastmcp-cli-auth",
"python-sdk/fastmcp-cli-cimd",
"python-sdk/fastmcp-cli-cli",
@@ -475,16 +530,16 @@
"group": "fastmcp.prompts",
"pages": [
"python-sdk/fastmcp-prompts-__init__",
- "python-sdk/fastmcp-prompts-function_prompt",
- "python-sdk/fastmcp-prompts-prompt"
+ "python-sdk/fastmcp-prompts-base",
+ "python-sdk/fastmcp-prompts-function_prompt"
]
},
{
"group": "fastmcp.resources",
"pages": [
"python-sdk/fastmcp-resources-__init__",
+ "python-sdk/fastmcp-resources-base",
"python-sdk/fastmcp-resources-function_resource",
- "python-sdk/fastmcp-resources-resource",
"python-sdk/fastmcp-resources-template",
"python-sdk/fastmcp-resources-types"
]
@@ -493,6 +548,7 @@
"group": "fastmcp.server",
"pages": [
"python-sdk/fastmcp-server-__init__",
+ "python-sdk/fastmcp-server-app",
"python-sdk/fastmcp-server-apps",
{
"group": "auth",
@@ -521,6 +577,7 @@
"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-clerk",
"python-sdk/fastmcp-server-auth-providers-debug",
"python-sdk/fastmcp-server-auth-providers-descope",
"python-sdk/fastmcp-server-auth-providers-discord",
@@ -684,9 +741,9 @@
"group": "fastmcp.tools",
"pages": [
"python-sdk/fastmcp-tools-__init__",
+ "python-sdk/fastmcp-tools-base",
"python-sdk/fastmcp-tools-function_parsing",
"python-sdk/fastmcp-tools-function_tool",
- "python-sdk/fastmcp-tools-tool",
"python-sdk/fastmcp-tools-tool_transform"
]
},
@@ -734,6 +791,7 @@
}
]
},
+ "python-sdk/fastmcp-utilities-mime",
{
"group": "openapi",
"pages": [
@@ -750,6 +808,7 @@
"python-sdk/fastmcp-utilities-skills",
"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",
@@ -1050,4 +1109,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/quickstart.mdx b/docs/getting-started/quickstart.mdx
index 3f240cec1..5f3f56b38 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.
+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).
@@ -117,6 +117,34 @@ Note that:
- 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) 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.
diff --git a/docs/getting-started/upgrading/from-fastmcp-2.mdx b/docs/getting-started/upgrading/from-fastmcp-2.mdx
index 47baa5655..f4795fe15 100644
--- a/docs/getting-started/upgrading/from-fastmcp-2.mdx
+++ b/docs/getting-started/upgrading/from-fastmcp-2.mdx
@@ -77,7 +77,7 @@ BREAKING CHANGES (will crash at import or runtime):
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 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.
@@ -126,7 +126,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.
diff --git a/docs/getting-started/welcome.mdx b/docs/getting-started/welcome.mdx
index 28932284f..a0c17720b 100644
--- a/docs/getting-started/welcome.mdx
+++ b/docs/getting-started/welcome.mdx
@@ -80,10 +80,16 @@ FastMCP has three pillars:
**[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). When you're ready to deploy, [Prefect Horizon](https://www.prefect.io/horizon) offers free hosting for FastMCP users.
+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 how teams build MCP servers. **[Prefect Horizon](https://www.prefect.io/horizon)** is how enterprises run them in production. Register any MCP server behind a managed gateway with SSO, tool-level RBAC, audit logs, and observability. Deploy FastMCP servers and go from PR to preview in 60 seconds, then remix tools from across your registry into use-case-specific, permissioned endpoints. Horizon is everything we've learned about MCP at scale from building the world's most popular MCP framework. Free for individuals, built for teams.
+
+[Deploy FastMCP with Horizon →](https://www.prefect.io/horizon)
+
**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.
diff --git a/docs/more/settings.mdx b/docs/more/settings.mdx
new file mode 100644
index 000000000..54bb73f3a
--- /dev/null
+++ b/docs/more/settings.mdx
@@ -0,0 +1,96 @@
+---
+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_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/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..04578add6
--- /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..461a55c52
--- /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..4f693f898
--- /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..d7c5edf2f
--- /dev/null
+++ b/docs/python-sdk/fastmcp-apps-config.mdx
@@ -0,0 +1,90 @@
+---
+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"]``.
+
+
+## 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..9705e335b
--- /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..edf6a72c9
--- /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..336d4f353
--- /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-apps_dev.mdx b/docs/python-sdk/fastmcp-cli-apps_dev.mdx
new file mode 100644
index 000000000..2f38bbc5b
--- /dev/null
+++ b/docs/python-sdk/fastmcp-cli-apps_dev.mdx
@@ -0,0 +1,47 @@
+---
+title: apps_dev
+sidebarTitle: apps_dev
+---
+
+# `fastmcp.cli.apps_dev`
+
+
+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.
+
+
+## Functions
+
+### `run_dev_apps`
+
+```python
+run_dev_apps(server_spec: str) -> None
+```
+
+
+Start the full dev environment for a FastMCPApp server.
+
+Starts the user's MCP server on *mcp_port*, starts the Prefab dev UI
+on *dev_port* (with an /mcp proxy to the user's server), then opens
+the browser.
+
diff --git a/docs/python-sdk/fastmcp-cli-cli.mdx b/docs/python-sdk/fastmcp-cli-cli.mdx
index 60804a298..b8bf7e0de 100644
--- a/docs/python-sdk/fastmcp-cli-cli.mdx
+++ b/docs/python-sdk/fastmcp-cli-cli.mdx
@@ -50,7 +50,23 @@ Run an MCP server with the MCP Inspector for development.
- `server_spec`: Python file to run, optionally with \:object suffix, or None to auto-detect fastmcp.json
-### `run`
+### `apps`
+
+```python
+apps(server_spec: str) -> None
+```
+
+
+Preview a FastMCPApp UI in the browser.
+
+Starts the MCP server from SERVER_SPEC on --mcp-port, launches a local
+dev UI on --dev-port with a tool picker and AppBridge host, then opens
+the browser automatically.
+
+Requires fastmcp[apps] to be installed (prefab-ui).
+
+
+### `run`
```python
run(server_spec: str | None = None, *server_args: str) -> None
@@ -75,7 +91,7 @@ fastmcp run server.py -- --config config.json --debug
- `server_spec`: Python file, object specification (file\:obj), config file, URL, or None to auto-detect
-### `inspect`
+### `inspect`
```python
inspect(server_spec: str | None = None) -> None
@@ -106,7 +122,7 @@ fastmcp inspect # auto-detect fastmcp.json
- `server_spec`: Python file to inspect, optionally with \:object suffix, or fastmcp.json
-### `prepare`
+### `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
diff --git a/docs/python-sdk/fastmcp-cli-client.mdx b/docs/python-sdk/fastmcp-cli-client.mdx
index 726663bfb..78dad8b29 100644
--- a/docs/python-sdk/fastmcp-cli-client.mdx
+++ b/docs/python-sdk/fastmcp-cli-client.mdx
@@ -10,7 +10,7 @@ Client-side CLI commands for querying and invoking MCP servers.
## Functions
-### `resolve_server_spec`
+### `resolve_server_spec`
```python
resolve_server_spec(server_spec: str | None) -> str | dict[str, Any] | ClientTransport
@@ -32,7 +32,7 @@ When ``command`` is provided, the string is shell-split into a
``StdioTransport(command, args)``.
-### `coerce_value`
+### `coerce_value`
```python
coerce_value(raw: str, schema: dict[str, Any]) -> Any
@@ -42,7 +42,7 @@ coerce_value(raw: str, schema: dict[str, Any]) -> Any
Coerce a string CLI value according to a JSON-Schema type hint.
-### `parse_tool_arguments`
+### `parse_tool_arguments`
```python
parse_tool_arguments(raw_args: tuple[str, ...], input_json: str | None, input_schema: dict[str, Any]) -> dict[str, Any]
@@ -56,7 +56,7 @@ A single JSON object argument is treated as the full argument dict.
Values are coerced using the tool's ``inputSchema``.
-### `format_tool_signature`
+### `format_tool_signature`
```python
format_tool_signature(tool: mcp.types.Tool) -> str
@@ -66,7 +66,7 @@ format_tool_signature(tool: mcp.types.Tool) -> str
Build ``name(param: type, ...) -> return_type`` from a tool's JSON schemas.
-### `list_command`
+### `list_command`
```python
list_command(server_spec: Annotated[str | None, cyclopts.Parameter(help='Server URL, Python file, MCPConfig JSON, or .js file')] = None) -> None
@@ -84,7 +84,7 @@ fastmcp list --command 'npx -y @mcp/server' --resources
fastmcp list http://server/mcp --transport sse
-### `call_command`
+### `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
@@ -110,7 +110,7 @@ fastmcp call http://server/mcp create --input-json '{"tags": ["a","b"]}'
```
-### `discover_command`
+### `discover_command`
```python
discover_command() -> None
diff --git a/docs/python-sdk/fastmcp-cli-install-claude_code.mdx b/docs/python-sdk/fastmcp-cli-install-claude_code.mdx
index 0a3393077..675faafc4 100644
--- a/docs/python-sdk/fastmcp-cli-install-claude_code.mdx
+++ b/docs/python-sdk/fastmcp-cli-install-claude_code.mdx
@@ -57,7 +57,7 @@ Install FastMCP server in Claude Code.
- True if installation was successful, False otherwise
-### `claude_code_command`
+### `claude_code_command`
```python
claude_code_command(server_spec: str) -> None
diff --git a/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx b/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx
index 23f7a1b27..2c06c6020 100644
--- a/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx
+++ b/docs/python-sdk/fastmcp-cli-install-claude_desktop.mdx
@@ -13,14 +13,17 @@ Claude Desktop integration for FastMCP install using Cyclopts.
### `get_claude_config_path`
```python
-get_claude_config_path() -> Path | None
+get_claude_config_path(config_path: Path | None = None) -> Path | None
```
Get the Claude config directory based on platform.
+**Args:**
+- `config_path`: Optional custom path to the Claude Desktop config directory
-### `install_claude_desktop`
+
+### `install_claude_desktop`
```python
install_claude_desktop(file: Path, server_object: str | None, name: str) -> bool
@@ -39,12 +42,13 @@ Install FastMCP server in Claude Desktop.
- `python_version`: Optional Python version to use
- `with_requirements`: Optional requirements file to install from
- `project`: Optional project directory to run within
+- `config_path`: Optional custom path to Claude Desktop config directory
**Returns:**
- True if installation was successful, False otherwise
-### `claude_desktop_command`
+### `claude_desktop_command`
```python
claude_desktop_command(server_spec: str) -> None
diff --git a/docs/python-sdk/fastmcp-cli-install-cursor.mdx b/docs/python-sdk/fastmcp-cli-install-cursor.mdx
index a61bca0ff..e6a964eed 100644
--- a/docs/python-sdk/fastmcp-cli-install-cursor.mdx
+++ b/docs/python-sdk/fastmcp-cli-install-cursor.mdx
@@ -68,7 +68,7 @@ Install FastMCP server to workspace-specific Cursor configuration.
- True if installation was successful, False otherwise
-### `install_cursor`
+### `install_cursor`
```python
install_cursor(file: Path, server_object: str | None, name: str) -> bool
@@ -93,7 +93,7 @@ Install FastMCP server in Cursor.
- True if installation was successful, False otherwise
-### `cursor_command`
+### `cursor_command`
```python
cursor_command(server_spec: str) -> None
diff --git a/docs/python-sdk/fastmcp-cli-install-gemini_cli.mdx b/docs/python-sdk/fastmcp-cli-install-gemini_cli.mdx
index 9cb51f0f4..d80716460 100644
--- a/docs/python-sdk/fastmcp-cli-install-gemini_cli.mdx
+++ b/docs/python-sdk/fastmcp-cli-install-gemini_cli.mdx
@@ -54,7 +54,7 @@ Install FastMCP server in Gemini CLI.
- True if installation was successful, False otherwise
-### `gemini_cli_command`
+### `gemini_cli_command`
```python
gemini_cli_command(server_spec: str) -> None
diff --git a/docs/python-sdk/fastmcp-cli-install-shared.mdx b/docs/python-sdk/fastmcp-cli-install-shared.mdx
index a1fe2119c..b51b0a424 100644
--- a/docs/python-sdk/fastmcp-cli-install-shared.mdx
+++ b/docs/python-sdk/fastmcp-cli-install-shared.mdx
@@ -10,7 +10,19 @@ Shared utilities for install commands.
## Functions
-### `parse_env_var`
+### `validate_server_name`
+
+```python
+validate_server_name(name: str) -> str
+```
+
+
+Validate that a server name is safe for use as a subprocess argument.
+
+Raises SystemExit if the name contains shell metacharacters.
+
+
+### `parse_env_var`
```python
parse_env_var(env_var: str) -> tuple[str, str]
@@ -20,7 +32,7 @@ parse_env_var(env_var: str) -> tuple[str, str]
Parse environment variable string in format KEY=VALUE.
-### `process_common_args`
+### `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]
@@ -32,7 +44,7 @@ Process common arguments shared by all install commands.
Handles both fastmcp.json config files and traditional file.py:object syntax.
-### `open_deeplink`
+### `open_deeplink`
```python
open_deeplink(url: str) -> bool
diff --git a/docs/python-sdk/fastmcp-client-auth-oauth.mdx b/docs/python-sdk/fastmcp-client-auth-oauth.mdx
index 455ea1337..85d4e536a 100644
--- a/docs/python-sdk/fastmcp-client-auth-oauth.mdx
+++ b/docs/python-sdk/fastmcp-client-auth-oauth.mdx
@@ -32,37 +32,43 @@ Raised when OAuth client credentials are not found on the server.
**Methods:**
-#### `clear`
+#### `clear`
```python
clear(self) -> None
```
-#### `get_tokens`
+#### `get_tokens`
```python
get_tokens(self) -> OAuthToken | None
```
-#### `set_tokens`
+#### `set_tokens`
```python
set_tokens(self, tokens: OAuthToken) -> None
```
-#### `get_client_info`
+#### `get_token_expiry`
+
+```python
+get_token_expiry(self) -> float | None
+```
+
+#### `get_client_info`
```python
get_client_info(self) -> OAuthClientInformationFull | None
```
-#### `set_client_info`
+#### `set_client_info`
```python
set_client_info(self, client_info: OAuthClientInformationFull) -> None
```
-### `OAuth`
+### `OAuth`
OAuth client provider for MCP servers with browser-based authentication.
@@ -73,7 +79,7 @@ a browser for user authorization and running a local callback server.
**Methods:**
-#### `redirect_handler`
+#### `redirect_handler`
```python
redirect_handler(self, authorization_url: str) -> None
@@ -82,7 +88,7 @@ redirect_handler(self, authorization_url: str) -> None
Open browser for authorization, with pre-flight check for invalid client.
-#### `callback_handler`
+#### `callback_handler`
```python
callback_handler(self) -> tuple[str, str | None]
@@ -91,7 +97,7 @@ callback_handler(self) -> tuple[str, str | None]
Handle OAuth callback and return (auth_code, state).
-#### `async_auth_flow`
+#### `async_auth_flow`
```python
async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]
diff --git a/docs/python-sdk/fastmcp-client-client.mdx b/docs/python-sdk/fastmcp-client-client.mdx
index 6c3ac689a..efbf56f08 100644
--- a/docs/python-sdk/fastmcp-client-client.mdx
+++ b/docs/python-sdk/fastmcp-client-client.mdx
@@ -7,7 +7,7 @@ sidebarTitle: client
## Classes
-### `ClientSessionState`
+### `ClientSessionState`
Holds all session-related state for a Client instance.
@@ -16,13 +16,13 @@ This allows clean separation of configuration (which is copied) from
session state (which should be fresh for each new client instance).
-### `CallToolResult`
+### `CallToolResult`
Parsed result from a tool call.
-### `Client`
+### `Client`
MCP client that delegates connection management to a Transport instance.
@@ -85,7 +85,7 @@ async with client:
**Methods:**
-#### `session`
+#### `session`
```python
session(self) -> ClientSession
@@ -94,7 +94,7 @@ session(self) -> ClientSession
Get the current active session. Raises RuntimeError if not connected.
-#### `initialize_result`
+#### `initialize_result`
```python
initialize_result(self) -> mcp.types.InitializeResult | None
@@ -103,7 +103,7 @@ initialize_result(self) -> mcp.types.InitializeResult | None
Get the result of the initialization request.
-#### `set_roots`
+#### `set_roots`
```python
set_roots(self, roots: RootsList | RootsHandler) -> None
@@ -112,7 +112,7 @@ 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`
+#### `set_sampling_callback`
```python
set_sampling_callback(self, sampling_callback: SamplingHandler, sampling_capabilities: mcp.types.SamplingCapability | None = None) -> None
@@ -121,7 +121,7 @@ set_sampling_callback(self, sampling_callback: SamplingHandler, sampling_capabil
Set the sampling callback for the client.
-#### `set_elicitation_callback`
+#### `set_elicitation_callback`
```python
set_elicitation_callback(self, elicitation_callback: ElicitationHandler) -> None
@@ -130,7 +130,7 @@ set_elicitation_callback(self, elicitation_callback: ElicitationHandler) -> None
Set the elicitation callback for the client.
-#### `is_connected`
+#### `is_connected`
```python
is_connected(self) -> bool
@@ -139,7 +139,7 @@ is_connected(self) -> bool
Check if the client is currently connected.
-#### `new`
+#### `new`
```python
new(self) -> Client[ClientTransportT]
@@ -155,7 +155,7 @@ share state with the original client.
- A new Client instance with the same configuration but disconnected state.
-#### `initialize`
+#### `initialize`
```python
initialize(self, timeout: datetime.timedelta | float | int | None = None) -> mcp.types.InitializeResult
@@ -183,13 +183,13 @@ capabilities, protocol version, and optional instructions.
- `RuntimeError`: If the client is not connected or initialization times out.
-#### `close`
+#### `close`
```python
close(self)
```
-#### `ping`
+#### `ping`
```python
ping(self) -> bool
@@ -198,7 +198,7 @@ ping(self) -> bool
Send a ping request.
-#### `cancel`
+#### `cancel`
```python
cancel(self, request_id: str | int, reason: str | None = None) -> None
@@ -207,7 +207,7 @@ cancel(self, request_id: str | int, reason: str | None = None) -> None
Send a cancellation notification for an in-progress request.
-#### `progress`
+#### `progress`
```python
progress(self, progress_token: str | int, progress: float, total: float | None = None, message: str | None = None) -> None
@@ -216,7 +216,7 @@ progress(self, progress_token: str | int, progress: float, total: float | None =
Send a progress notification.
-#### `set_logging_level`
+#### `set_logging_level`
```python
set_logging_level(self, level: mcp.types.LoggingLevel) -> None
@@ -225,7 +225,7 @@ set_logging_level(self, level: mcp.types.LoggingLevel) -> None
Send a logging/setLevel request.
-#### `send_roots_list_changed`
+#### `send_roots_list_changed`
```python
send_roots_list_changed(self) -> None
@@ -234,7 +234,7 @@ send_roots_list_changed(self) -> None
Send a roots/list_changed notification.
-#### `complete_mcp`
+#### `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
@@ -257,7 +257,7 @@ containing the completion and any additional metadata.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
-#### `complete`
+#### `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
@@ -279,7 +279,7 @@ include with the completion request. Defaults to None.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
-#### `generate_name`
+#### `generate_name`
```python
generate_name(cls, name: str | None = None) -> str
diff --git a/docs/python-sdk/fastmcp-client-mixins-prompts.mdx b/docs/python-sdk/fastmcp-client-mixins-prompts.mdx
index 3931c03db..f91e79a9b 100644
--- a/docs/python-sdk/fastmcp-client-mixins-prompts.mdx
+++ b/docs/python-sdk/fastmcp-client-mixins-prompts.mdx
@@ -10,7 +10,7 @@ Prompt-related methods for FastMCP Client.
## Classes
-### `ClientPromptsMixin`
+### `ClientPromptsMixin`
Mixin providing prompt-related methods for Client.
@@ -18,7 +18,7 @@ Mixin providing prompt-related methods for Client.
**Methods:**
-#### `list_prompts_mcp`
+#### `list_prompts_mcp`
```python
list_prompts_mcp(self: Client) -> mcp.types.ListPromptsResult
@@ -38,10 +38,10 @@ containing the list of prompts and any additional metadata.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
-#### `list_prompts`
+#### `list_prompts`
```python
-list_prompts(self: Client) -> list[mcp.types.Prompt]
+list_prompts(self: Client, max_pages: int = AUTO_PAGINATION_MAX_PAGES) -> list[mcp.types.Prompt]
```
Retrieve all prompts available on the server.
@@ -50,15 +50,18 @@ 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.
+**Args:**
+- `max_pages`: Maximum number of pages to fetch before raising. Defaults to 250.
+
**Returns:**
- list\[mcp.types.Prompt]: A list of all Prompt objects.
**Raises:**
-- `RuntimeError`: If called while the client is not connected.
+- `RuntimeError`: If the page limit is reached before pagination completes.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
-#### `get_prompt_mcp`
+#### `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
@@ -80,19 +83,19 @@ containing the prompt messages and any additional metadata.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
-#### `get_prompt`
+#### `get_prompt`
```python
get_prompt(self: Client, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult
```
-#### `get_prompt`
+#### `get_prompt`
```python
get_prompt(self: Client, name: str, arguments: dict[str, Any] | None = None) -> PromptTask
```
-#### `get_prompt`
+#### `get_prompt`
```python
get_prompt(self: Client, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.GetPromptResult | PromptTask
diff --git a/docs/python-sdk/fastmcp-client-mixins-resources.mdx b/docs/python-sdk/fastmcp-client-mixins-resources.mdx
index 655101ac3..70f07c7e7 100644
--- a/docs/python-sdk/fastmcp-client-mixins-resources.mdx
+++ b/docs/python-sdk/fastmcp-client-mixins-resources.mdx
@@ -10,7 +10,7 @@ Resource-related methods for FastMCP Client.
## Classes
-### `ClientResourcesMixin`
+### `ClientResourcesMixin`
Mixin providing resource-related methods for Client.
@@ -18,7 +18,7 @@ Mixin providing resource-related methods for Client.
**Methods:**
-#### `list_resources_mcp`
+#### `list_resources_mcp`
```python
list_resources_mcp(self: Client) -> mcp.types.ListResourcesResult
@@ -38,10 +38,10 @@ containing the list of resources and any additional metadata.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
-#### `list_resources`
+#### `list_resources`
```python
-list_resources(self: Client) -> list[mcp.types.Resource]
+list_resources(self: Client, max_pages: int = AUTO_PAGINATION_MAX_PAGES) -> list[mcp.types.Resource]
```
Retrieve all resources available on the server.
@@ -50,15 +50,18 @@ 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.
+**Args:**
+- `max_pages`: Maximum number of pages to fetch before raising. Defaults to 250.
+
**Returns:**
- list\[mcp.types.Resource]: A list of all Resource objects.
**Raises:**
-- `RuntimeError`: If called while the client is not connected.
+- `RuntimeError`: If the page limit is reached before pagination completes.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
-#### `list_resource_templates_mcp`
+#### `list_resource_templates_mcp`
```python
list_resource_templates_mcp(self: Client) -> mcp.types.ListResourceTemplatesResult
@@ -78,10 +81,10 @@ containing the list of resource templates and any additional metadata.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
-#### `list_resource_templates`
+#### `list_resource_templates`
```python
-list_resource_templates(self: Client) -> list[mcp.types.ResourceTemplate]
+list_resource_templates(self: Client, max_pages: int = AUTO_PAGINATION_MAX_PAGES) -> list[mcp.types.ResourceTemplate]
```
Retrieve all resource templates available on the server.
@@ -91,15 +94,18 @@ 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.
+**Args:**
+- `max_pages`: Maximum number of pages to fetch before raising. Defaults to 250.
+
**Returns:**
- list\[mcp.types.ResourceTemplate]: A list of all ResourceTemplate objects.
**Raises:**
-- `RuntimeError`: If called while the client is not connected.
+- `RuntimeError`: If the page limit is reached before pagination completes.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
-#### `read_resource_mcp`
+#### `read_resource_mcp`
```python
read_resource_mcp(self: Client, uri: AnyUrl | str, meta: dict[str, Any] | None = None) -> mcp.types.ReadResourceResult
@@ -120,19 +126,19 @@ containing the resource contents and any additional metadata.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
-#### `read_resource`
+#### `read_resource`
```python
read_resource(self: Client, uri: AnyUrl | str) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]
```
-#### `read_resource`
+#### `read_resource`
```python
read_resource(self: Client, uri: AnyUrl | str) -> ResourceTask
```
-#### `read_resource`
+#### `read_resource`
```python
read_resource(self: Client, uri: AnyUrl | str) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents] | ResourceTask
diff --git a/docs/python-sdk/fastmcp-client-mixins-tools.mdx b/docs/python-sdk/fastmcp-client-mixins-tools.mdx
index f048ed070..8711bfeb8 100644
--- a/docs/python-sdk/fastmcp-client-mixins-tools.mdx
+++ b/docs/python-sdk/fastmcp-client-mixins-tools.mdx
@@ -10,7 +10,7 @@ Tool-related methods for FastMCP Client.
## Classes
-### `ClientToolsMixin`
+### `ClientToolsMixin`
Mixin providing tool-related methods for Client.
@@ -18,7 +18,7 @@ Mixin providing tool-related methods for Client.
**Methods:**
-#### `list_tools_mcp`
+#### `list_tools_mcp`
```python
list_tools_mcp(self: Client) -> mcp.types.ListToolsResult
@@ -38,10 +38,10 @@ containing the list of tools and any additional metadata.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
-#### `list_tools`
+#### `list_tools`
```python
-list_tools(self: Client) -> list[mcp.types.Tool]
+list_tools(self: Client, max_pages: int = AUTO_PAGINATION_MAX_PAGES) -> list[mcp.types.Tool]
```
Retrieve all tools available on the server.
@@ -50,15 +50,18 @@ 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.
+**Args:**
+- `max_pages`: Maximum number of pages to fetch before raising. Defaults to 250.
+
**Returns:**
- list\[mcp.types.Tool]: A list of all Tool objects.
**Raises:**
-- `RuntimeError`: If called while the client is not connected.
+- `RuntimeError`: If the page limit is reached before pagination completes.
- `McpError`: If the request results in a TimeoutError | JSONRPCError
-#### `call_tool_mcp`
+#### `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
@@ -88,19 +91,19 @@ containing the tool result and any additional metadata.
- `McpError`: If the tool call requests results in a TimeoutError | JSONRPCError
-#### `call_tool`
+#### `call_tool`
```python
call_tool(self: Client, name: str, arguments: dict[str, Any] | None = None) -> CallToolResult
```
-#### `call_tool`
+#### `call_tool`
```python
call_tool(self: Client, name: str, arguments: dict[str, Any] | None = None) -> ToolTask
```
-#### `call_tool`
+#### `call_tool`
```python
call_tool(self: Client, name: str, arguments: dict[str, Any] | None = None) -> CallToolResult | ToolTask
diff --git a/docs/python-sdk/fastmcp-client-sampling-handlers-anthropic.mdx b/docs/python-sdk/fastmcp-client-sampling-handlers-anthropic.mdx
index 976367c28..ff48e7a31 100644
--- a/docs/python-sdk/fastmcp-client-sampling-handlers-anthropic.mdx
+++ b/docs/python-sdk/fastmcp-client-sampling-handlers-anthropic.mdx
@@ -10,7 +10,7 @@ Anthropic sampling handler for FastMCP.
## Classes
-### `AnthropicSamplingHandler`
+### `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
index 9681c3a4a..d55619c72 100644
--- a/docs/python-sdk/fastmcp-client-sampling-handlers-google_genai.mdx
+++ b/docs/python-sdk/fastmcp-client-sampling-handlers-google_genai.mdx
@@ -10,7 +10,7 @@ Google GenAI sampling handler with tool support for FastMCP 3.0.
## Classes
-### `GoogleGenaiSamplingHandler`
+### `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
index b291bbaa5..2d7976e0f 100644
--- a/docs/python-sdk/fastmcp-client-sampling-handlers-openai.mdx
+++ b/docs/python-sdk/fastmcp-client-sampling-handlers-openai.mdx
@@ -10,7 +10,7 @@ OpenAI sampling handler for FastMCP.
## Classes
-### `OpenAISamplingHandler`
+### `OpenAISamplingHandler`
Sampling handler that uses the OpenAI API.
diff --git a/docs/python-sdk/fastmcp-client-transports-config.mdx b/docs/python-sdk/fastmcp-client-transports-config.mdx
index 7ad10e0df..3881c69e1 100644
--- a/docs/python-sdk/fastmcp-client-transports-config.mdx
+++ b/docs/python-sdk/fastmcp-client-transports-config.mdx
@@ -7,7 +7,7 @@ sidebarTitle: config
## Classes
-### `MCPConfigTransport`
+### `MCPConfigTransport`
Transport for connecting to one or more MCP servers defined in an MCPConfig.
@@ -59,13 +59,13 @@ async with client:
**Methods:**
-#### `connect_session`
+#### `connect_session`
```python
connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
```
-#### `close`
+#### `close`
```python
close(self)
diff --git a/docs/python-sdk/fastmcp-client-transports-http.mdx b/docs/python-sdk/fastmcp-client-transports-http.mdx
index a3375240e..a0db1401d 100644
--- a/docs/python-sdk/fastmcp-client-transports-http.mdx
+++ b/docs/python-sdk/fastmcp-client-transports-http.mdx
@@ -10,7 +10,7 @@ Streamable HTTP transport for FastMCP Client.
## Classes
-### `StreamableHttpTransport`
+### `StreamableHttpTransport`
Transport implementation that connects to an MCP server via Streamable HTTP Requests.
@@ -18,19 +18,19 @@ Transport implementation that connects to an MCP server via Streamable HTTP Requ
**Methods:**
-#### `connect_session`
+#### `connect_session`
```python
connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession]
```
-#### `get_session_id`
+#### `get_session_id`
```python
get_session_id(self) -> str | None
```
-#### `close`
+#### `close`
```python
close(self)
diff --git a/docs/python-sdk/fastmcp-client-transports-sse.mdx b/docs/python-sdk/fastmcp-client-transports-sse.mdx
index 59c145401..a65dace46 100644
--- a/docs/python-sdk/fastmcp-client-transports-sse.mdx
+++ b/docs/python-sdk/fastmcp-client-transports-sse.mdx
@@ -10,7 +10,7 @@ Server-Sent Events (SSE) transport for FastMCP Client.
## Classes
-### `SSETransport`
+### `SSETransport`
Transport implementation that connects to an MCP server via Server-Sent Events.
@@ -18,7 +18,7 @@ Transport implementation that connects to an MCP server via Server-Sent Events.
**Methods:**
-#### `connect_session`
+#### `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
index eb7d98eb2..ac317bfc3 100644
--- a/docs/python-sdk/fastmcp-client-transports-stdio.mdx
+++ b/docs/python-sdk/fastmcp-client-transports-stdio.mdx
@@ -30,49 +30,49 @@ connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[
connect(self, **session_kwargs: Unpack[SessionKwargs]) -> ClientSession | None
```
-#### `disconnect`
+#### `disconnect`
```python
disconnect(self)
```
-#### `close`
+#### `close`
```python
close(self)
```
-### `PythonStdioTransport`
+### `PythonStdioTransport`
Transport for running Python scripts.
-### `FastMCPStdioTransport`
+### `FastMCPStdioTransport`
Transport for running FastMCP servers using the FastMCP CLI.
-### `NodeStdioTransport`
+### `NodeStdioTransport`
Transport for running Node.js scripts.
-### `UvStdioTransport`
+### `UvStdioTransport`
Transport for running commands via the uv tool.
-### `UvxStdioTransport`
+### `UvxStdioTransport`
Transport for running commands via the uvx tool.
-### `NpxStdioTransport`
+### `NpxStdioTransport`
Transport for running commands via the npx tool.
diff --git a/docs/python-sdk/fastmcp-exceptions.mdx b/docs/python-sdk/fastmcp-exceptions.mdx
index d8f5a6871..3494751f7 100644
--- a/docs/python-sdk/fastmcp-exceptions.mdx
+++ b/docs/python-sdk/fastmcp-exceptions.mdx
@@ -10,61 +10,71 @@ Custom exceptions for FastMCP.
## Classes
-### `FastMCPError`
+### `FastMCPDeprecationWarning`
+
+
+Deprecation warning for FastMCP APIs.
+
+Subclass of DeprecationWarning so that standard warning filters
+still apply, but FastMCP can selectively enable its own warnings
+without affecting other libraries in the process.
+
+
+### `FastMCPError`
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`
+### `AuthorizationError`
Error when authorization check fails.
diff --git a/docs/python-sdk/fastmcp-experimental-transforms-code_mode.mdx b/docs/python-sdk/fastmcp-experimental-transforms-code_mode.mdx
index 0553029eb..9dc66b06a 100644
--- a/docs/python-sdk/fastmcp-experimental-transforms-code_mode.mdx
+++ b/docs/python-sdk/fastmcp-experimental-transforms-code_mode.mdx
@@ -47,7 +47,7 @@ leave that limit uncapped.
run(self, code: str) -> Any
```
-### `Search`
+### `Search`
Discovery tool factory that searches the catalog by query.
@@ -64,7 +64,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 +78,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 +93,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 +106,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 +123,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-prompts-prompt.mdx b/docs/python-sdk/fastmcp-prompts-base.mdx
similarity index 64%
rename from docs/python-sdk/fastmcp-prompts-prompt.mdx
rename to docs/python-sdk/fastmcp-prompts-base.mdx
index 2fe759342..1c57fbea2 100644
--- a/docs/python-sdk/fastmcp-prompts-prompt.mdx
+++ b/docs/python-sdk/fastmcp-prompts-base.mdx
@@ -1,16 +1,16 @@
---
-title: prompt
-sidebarTitle: prompt
+title: base
+sidebarTitle: base
---
-# `fastmcp.prompts.prompt`
+# `fastmcp.prompts.base`
Base classes for FastMCP prompts.
## Classes
-### `Message`
+### `Message`
Wrapper for prompt message with auto-serialization.
@@ -21,7 +21,7 @@ Accepts any content - strings pass through, other types
**Methods:**
-#### `to_mcp_prompt_message`
+#### `to_mcp_prompt_message`
```python
to_mcp_prompt_message(self) -> PromptMessage
@@ -30,13 +30,13 @@ to_mcp_prompt_message(self) -> PromptMessage
Convert to MCP PromptMessage.
-### `PromptArgument`
+### `PromptArgument`
An argument that can be passed to a prompt.
-### `PromptResult`
+### `PromptResult`
Canonical result type for prompt rendering.
@@ -47,7 +47,7 @@ roles, and metadata at both the message and result level.
**Methods:**
-#### `to_mcp_prompt_result`
+#### `to_mcp_prompt_result`
```python
to_mcp_prompt_result(self) -> GetPromptResult
@@ -56,7 +56,7 @@ to_mcp_prompt_result(self) -> GetPromptResult
Convert to MCP GetPromptResult.
-### `Prompt`
+### `Prompt`
A prompt template that can be rendered with parameters.
@@ -64,7 +64,7 @@ A prompt template that can be rendered with parameters.
**Methods:**
-#### `to_mcp_prompt`
+#### `to_mcp_prompt`
```python
to_mcp_prompt(self, **overrides: Any) -> SDKPrompt
@@ -73,7 +73,7 @@ to_mcp_prompt(self, **overrides: Any) -> SDKPrompt
Convert the prompt to an MCP prompt.
-#### `from_function`
+#### `from_function`
```python
from_function(cls, fn: Callable[..., Any]) -> FunctionPrompt
@@ -87,7 +87,7 @@ The function can return:
- PromptResult: used directly
-#### `render`
+#### `render`
```python
render(self, arguments: dict[str, Any] | None = None) -> str | list[Message | str] | PromptResult
@@ -101,7 +101,7 @@ Subclasses must implement this method. Return one of:
- PromptResult: Used directly
-#### `convert_result`
+#### `convert_result`
```python
convert_result(self, raw_value: Any) -> PromptResult
@@ -113,7 +113,7 @@ Convert a raw return value to PromptResult.
- `TypeError`: for unsupported types
-#### `register_with_docket`
+#### `register_with_docket`
```python
register_with_docket(self, docket: Docket) -> None
@@ -122,7 +122,7 @@ register_with_docket(self, docket: Docket) -> None
Register this prompt with docket for background execution.
-#### `add_to_docket`
+#### `add_to_docket`
```python
add_to_docket(self, docket: Docket, arguments: dict[str, Any] | None, **kwargs: Any) -> Execution
@@ -138,7 +138,7 @@ Schedule this prompt for background execution via docket.
- `**kwargs`: Additional kwargs passed to docket.add()
-#### `get_span_attributes`
+#### `get_span_attributes`
```python
get_span_attributes(self) -> dict[str, Any]
diff --git a/docs/python-sdk/fastmcp-prompts-function_prompt.mdx b/docs/python-sdk/fastmcp-prompts-function_prompt.mdx
index a3222afc0..92f369d78 100644
--- a/docs/python-sdk/fastmcp-prompts-function_prompt.mdx
+++ b/docs/python-sdk/fastmcp-prompts-function_prompt.mdx
@@ -10,7 +10,7 @@ Standalone @prompt decorator for FastMCP.
## Functions
-### `prompt`
+### `prompt`
```python
prompt(name_or_fn: str | Callable[..., Any] | None = None) -> Any
@@ -25,19 +25,19 @@ using mcp.add_prompt().
## Classes
-### `DecoratedPrompt`
+### `DecoratedPrompt`
Protocol for functions decorated with @prompt.
-### `PromptMeta`
+### `PromptMeta`
Metadata attached to functions by the @prompt decorator.
-### `FunctionPrompt`
+### `FunctionPrompt`
A prompt that is a function.
@@ -45,7 +45,7 @@ A prompt that is a function.
**Methods:**
-#### `from_function`
+#### `from_function`
```python
from_function(cls, fn: Callable[..., Any]) -> FunctionPrompt
@@ -66,7 +66,7 @@ The function can return:
- PromptResult: used directly
-#### `render`
+#### `render`
```python
render(self, arguments: dict[str, Any] | None = None) -> PromptResult
@@ -75,7 +75,7 @@ render(self, arguments: dict[str, Any] | None = None) -> PromptResult
Render the prompt with arguments.
-#### `register_with_docket`
+#### `register_with_docket`
```python
register_with_docket(self, docket: Docket) -> None
@@ -87,7 +87,7 @@ FunctionPrompt registers the underlying function, which has the user's
Depends parameters for docket to resolve.
-#### `add_to_docket`
+#### `add_to_docket`
```python
add_to_docket(self, docket: Docket, arguments: dict[str, Any] | None, **kwargs: Any) -> Execution
diff --git a/docs/python-sdk/fastmcp-resources-resource.mdx b/docs/python-sdk/fastmcp-resources-base.mdx
similarity index 70%
rename from docs/python-sdk/fastmcp-resources-resource.mdx
rename to docs/python-sdk/fastmcp-resources-base.mdx
index 029b48700..aab4a1dd7 100644
--- a/docs/python-sdk/fastmcp-resources-resource.mdx
+++ b/docs/python-sdk/fastmcp-resources-base.mdx
@@ -1,16 +1,16 @@
---
-title: resource
-sidebarTitle: resource
+title: base
+sidebarTitle: base
---
-# `fastmcp.resources.resource`
+# `fastmcp.resources.base`
Base classes and interfaces for FastMCP resources.
## Classes
-### `ResourceContent`
+### `ResourceContent`
Wrapper for resource content with optional MIME type and metadata.
@@ -21,7 +21,7 @@ other types (dict, list, BaseModel, etc.) are automatically JSON-serialized.
**Methods:**
-#### `to_mcp_resource_contents`
+#### `to_mcp_resource_contents`
```python
to_mcp_resource_contents(self, uri: AnyUrl | str) -> mcp.types.TextResourceContents | mcp.types.BlobResourceContents
@@ -36,7 +36,7 @@ Convert to MCP resource contents type.
- TextResourceContents for str content, BlobResourceContents for bytes
-### `ResourceResult`
+### `ResourceResult`
Canonical result type for resource reads.
@@ -47,7 +47,7 @@ per-item MIME types, and metadata at both the item and result level.
**Methods:**
-#### `to_mcp_result`
+#### `to_mcp_result`
```python
to_mcp_result(self, uri: AnyUrl | str) -> mcp.types.ReadResourceResult
@@ -62,7 +62,7 @@ Convert to MCP ReadResourceResult.
- MCP ReadResourceResult with converted contents
-### `Resource`
+### `Resource`
Base class for all resources.
@@ -70,13 +70,13 @@ Base class for all resources.
**Methods:**
-#### `from_function`
+#### `from_function`
```python
from_function(cls, fn: Callable[..., Any], uri: str | AnyUrl) -> FunctionResource
```
-#### `set_default_mime_type`
+#### `set_default_mime_type`
```python
set_default_mime_type(cls, mime_type: str | None) -> str
@@ -85,7 +85,7 @@ set_default_mime_type(cls, mime_type: str | None) -> str
Set default MIME type if not provided.
-#### `set_default_name`
+#### `set_default_name`
```python
set_default_name(self) -> Self
@@ -94,7 +94,7 @@ set_default_name(self) -> Self
Set default name from URI if not provided.
-#### `read`
+#### `read`
```python
read(self) -> str | bytes | ResourceResult
@@ -108,7 +108,7 @@ Subclasses implement this to return resource data. Supported return types:
- ResourceResult: Full control over contents and result-level meta
-#### `convert_result`
+#### `convert_result`
```python
convert_result(self, raw_value: Any) -> ResourceResult
@@ -131,7 +131,7 @@ MCP Apps CSP/permissions) is propagated to each content item so
that hosts can read it from the ``resources/read`` response.
-#### `to_mcp_resource`
+#### `to_mcp_resource`
```python
to_mcp_resource(self, **overrides: Any) -> SDKResource
@@ -140,7 +140,7 @@ to_mcp_resource(self, **overrides: Any) -> SDKResource
Convert the resource to an SDKResource.
-#### `key`
+#### `key`
```python
key(self) -> str
@@ -149,7 +149,7 @@ key(self) -> str
The globally unique lookup key for this resource.
-#### `register_with_docket`
+#### `register_with_docket`
```python
register_with_docket(self, docket: Docket) -> None
@@ -158,7 +158,7 @@ register_with_docket(self, docket: Docket) -> None
Register this resource with docket for background execution.
-#### `add_to_docket`
+#### `add_to_docket`
```python
add_to_docket(self, docket: Docket, **kwargs: Any) -> Execution
@@ -173,7 +173,7 @@ Schedule this resource for background execution via docket.
- `**kwargs`: Additional kwargs passed to docket.add()
-#### `get_span_attributes`
+#### `get_span_attributes`
```python
get_span_attributes(self) -> dict[str, Any]
diff --git a/docs/python-sdk/fastmcp-resources-function_resource.mdx b/docs/python-sdk/fastmcp-resources-function_resource.mdx
index 3a7d346e1..4977f0d58 100644
--- a/docs/python-sdk/fastmcp-resources-function_resource.mdx
+++ b/docs/python-sdk/fastmcp-resources-function_resource.mdx
@@ -10,7 +10,7 @@ Standalone @resource decorator for FastMCP.
## Functions
-### `resource`
+### `resource`
```python
resource(uri: str) -> Callable[[F], F]
@@ -25,19 +25,19 @@ using mcp.add_resource().
## Classes
-### `DecoratedResource`
+### `DecoratedResource`
Protocol for functions decorated with @resource.
-### `ResourceMeta`
+### `ResourceMeta`
Metadata attached to functions by the @resource decorator.
-### `FunctionResource`
+### `FunctionResource`
A resource that defers data loading by wrapping a function.
@@ -54,7 +54,7 @@ The function can return:
**Methods:**
-#### `from_function`
+#### `from_function`
```python
from_function(cls, fn: Callable[..., Any], uri: str | AnyUrl | None = None) -> FunctionResource
@@ -71,7 +71,7 @@ individual parameters must not be passed.
Cannot be used together with metadata parameter.
-#### `read`
+#### `read`
```python
read(self) -> str | bytes | ResourceResult
@@ -80,7 +80,7 @@ read(self) -> str | bytes | ResourceResult
Read the resource by calling the wrapped function.
-#### `register_with_docket`
+#### `register_with_docket`
```python
register_with_docket(self, docket: Docket) -> None
diff --git a/docs/python-sdk/fastmcp-resources-template.mdx b/docs/python-sdk/fastmcp-resources-template.mdx
index 89e51c22f..06d8ef8ed 100644
--- a/docs/python-sdk/fastmcp-resources-template.mdx
+++ b/docs/python-sdk/fastmcp-resources-template.mdx
@@ -10,7 +10,7 @@ Resource template functionality.
## Functions
-### `extract_query_params`
+### `extract_query_params`
```python
extract_query_params(uri_template: str) -> set[str]
@@ -20,10 +20,10 @@ extract_query_params(uri_template: str) -> set[str]
Extract query parameter names from RFC 6570 `{?param1,param2}` syntax.
-### `build_regex`
+### `build_regex`
```python
-build_regex(template: str) -> re.Pattern
+build_regex(template: str) -> re.Pattern[str] | None
```
@@ -34,8 +34,11 @@ Supports:
- `{var*}` - wildcard path parameter (captures multiple segments)
- `{?var1,var2}` - query parameters (ignored in path matching)
+Returns None if the template produces an invalid regex (e.g. parameter
+names with hyphens, leading digits, or duplicates from a remote server).
-### `match_uri_template`
+
+### `match_uri_template`
```python
match_uri_template(uri: str, uri_template: str) -> dict[str, str] | None
@@ -51,7 +54,7 @@ Supports RFC 6570 URI templates:
## Classes
-### `ResourceTemplate`
+### `ResourceTemplate`
A template for dynamically creating resources.
@@ -59,13 +62,13 @@ A template for dynamically creating resources.
**Methods:**
-#### `from_function`
+#### `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`
+#### `set_default_mime_type`
```python
set_default_mime_type(cls, mime_type: str | None) -> str
@@ -74,7 +77,7 @@ set_default_mime_type(cls, mime_type: str | None) -> str
Set default MIME type if not provided.
-#### `matches`
+#### `matches`
```python
matches(self, uri: str) -> dict[str, Any] | None
@@ -83,7 +86,7 @@ matches(self, uri: str) -> dict[str, Any] | None
Check if URI matches template and extract parameters.
-#### `read`
+#### `read`
```python
read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult
@@ -92,7 +95,7 @@ read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult
Read the resource content.
-#### `convert_result`
+#### `convert_result`
```python
convert_result(self, raw_value: Any) -> ResourceResult
@@ -108,7 +111,7 @@ Handles ResourceResult passthrough and converts raw values using
ResourceResult's normalization.
-#### `create_resource`
+#### `create_resource`
```python
create_resource(self, uri: str, params: dict[str, Any]) -> Resource
@@ -120,7 +123,7 @@ The base implementation does not support background tasks.
Use FunctionResourceTemplate for task support.
-#### `to_mcp_template`
+#### `to_mcp_template`
```python
to_mcp_template(self, **overrides: Any) -> SDKResourceTemplate
@@ -129,7 +132,7 @@ to_mcp_template(self, **overrides: Any) -> SDKResourceTemplate
Convert the resource template to an SDKResourceTemplate.
-#### `from_mcp_template`
+#### `from_mcp_template`
```python
from_mcp_template(cls, mcp_template: SDKResourceTemplate) -> ResourceTemplate
@@ -138,7 +141,7 @@ from_mcp_template(cls, mcp_template: SDKResourceTemplate) -> ResourceTemplate
Creates a FastMCP ResourceTemplate from a raw MCP ResourceTemplate object.
-#### `key`
+#### `key`
```python
key(self) -> str
@@ -147,7 +150,7 @@ key(self) -> str
The globally unique lookup key for this template.
-#### `register_with_docket`
+#### `register_with_docket`
```python
register_with_docket(self, docket: Docket) -> None
@@ -156,7 +159,7 @@ register_with_docket(self, docket: Docket) -> None
Register this template with docket for background execution.
-#### `add_to_docket`
+#### `add_to_docket`
```python
add_to_docket(self, docket: Docket, params: dict[str, Any], **kwargs: Any) -> Execution
@@ -172,13 +175,13 @@ Schedule this template for background execution via docket.
- `**kwargs`: Additional kwargs passed to docket.add()
-#### `get_span_attributes`
+#### `get_span_attributes`
```python
get_span_attributes(self) -> dict[str, Any]
```
-### `FunctionResourceTemplate`
+### `FunctionResourceTemplate`
A template for dynamically creating resources.
@@ -186,7 +189,7 @@ A template for dynamically creating resources.
**Methods:**
-#### `create_resource`
+#### `create_resource`
```python
create_resource(self, uri: str, params: dict[str, Any]) -> Resource
@@ -195,7 +198,7 @@ create_resource(self, uri: str, params: dict[str, Any]) -> Resource
Create a resource from the template with the given parameters.
-#### `read`
+#### `read`
```python
read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult
@@ -204,7 +207,7 @@ read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult
Read the resource content.
-#### `register_with_docket`
+#### `register_with_docket`
```python
register_with_docket(self, docket: Docket) -> None
@@ -216,7 +219,7 @@ FunctionResourceTemplate registers the underlying function, which has the
user's Depends parameters for docket to resolve.
-#### `add_to_docket`
+#### `add_to_docket`
```python
add_to_docket(self, docket: Docket, params: dict[str, Any], **kwargs: Any) -> Execution
@@ -234,7 +237,7 @@ FunctionResourceTemplate splats the params dict since .fn expects **kwargs.
- `**kwargs`: Additional kwargs passed to docket.add()
-#### `from_function`
+#### `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
diff --git a/docs/python-sdk/fastmcp-resources-types.mdx b/docs/python-sdk/fastmcp-resources-types.mdx
index d19b24eb2..5a951f0dc 100644
--- a/docs/python-sdk/fastmcp-resources-types.mdx
+++ b/docs/python-sdk/fastmcp-resources-types.mdx
@@ -54,7 +54,7 @@ Set is_binary=True to read file as binary data instead of text.
**Methods:**
-#### `validate_absolute_path`
+#### `validate_absolute_path`
```python
validate_absolute_path(cls, path: Path) -> Path
@@ -63,7 +63,7 @@ validate_absolute_path(cls, path: Path) -> Path
Ensure path is absolute.
-#### `set_binary_from_mime_type`
+#### `set_binary_from_mime_type`
```python
set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool
@@ -72,7 +72,7 @@ set_binary_from_mime_type(cls, is_binary: bool, info: ValidationInfo) -> bool
Set is_binary based on mime_type if not explicitly set.
-#### `read`
+#### `read`
```python
read(self) -> ResourceResult
@@ -81,7 +81,7 @@ read(self) -> ResourceResult
Read the file content.
-### `HttpResource`
+### `HttpResource`
A resource that reads from an HTTP endpoint.
@@ -89,7 +89,7 @@ A resource that reads from an HTTP endpoint.
**Methods:**
-#### `read`
+#### `read`
```python
read(self) -> ResourceResult
@@ -98,7 +98,7 @@ read(self) -> ResourceResult
Read the HTTP content.
-### `DirectoryResource`
+### `DirectoryResource`
A resource that lists files in a directory.
@@ -106,7 +106,7 @@ A resource that lists files in a directory.
**Methods:**
-#### `validate_absolute_path`
+#### `validate_absolute_path`
```python
validate_absolute_path(cls, path: Path) -> Path
@@ -115,7 +115,7 @@ validate_absolute_path(cls, path: Path) -> Path
Ensure path is absolute.
-#### `list_files`
+#### `list_files`
```python
list_files(self) -> list[Path]
@@ -124,7 +124,7 @@ list_files(self) -> list[Path]
List files in the directory.
-#### `read`
+#### `read`
```python
read(self) -> ResourceResult
diff --git a/docs/python-sdk/fastmcp-server-app.mdx b/docs/python-sdk/fastmcp-server-app.mdx
new file mode 100644
index 000000000..7f99ecb53
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-app.mdx
@@ -0,0 +1,13 @@
+---
+title: app
+sidebarTitle: app
+---
+
+# `fastmcp.server.app`
+
+
+Backward-compatible re-exports from fastmcp.apps.app.
+
+.. deprecated:: 3.2.0
+ Import from ``fastmcp.apps.app`` or ``fastmcp`` instead.
+
diff --git a/docs/python-sdk/fastmcp-server-apps.mdx b/docs/python-sdk/fastmcp-server-apps.mdx
index 00e53eafe..df7d64d44 100644
--- a/docs/python-sdk/fastmcp-server-apps.mdx
+++ b/docs/python-sdk/fastmcp-server-apps.mdx
@@ -6,81 +6,8 @@ sidebarTitle: apps
# `fastmcp.server.apps`
-MCP Apps support — extension negotiation and typed UI metadata models.
+Backward-compatible re-exports from fastmcp.apps.
-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).
+.. deprecated:: 3.2.0
+ Import from ``fastmcp.apps`` instead.
diff --git a/docs/python-sdk/fastmcp-server-auth-auth.mdx b/docs/python-sdk/fastmcp-server-auth-auth.mdx
index 2186df875..def0830fd 100644
--- a/docs/python-sdk/fastmcp-server-auth-auth.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-auth.mdx
@@ -183,7 +183,7 @@ Get HTTP application-level middleware for this auth provider.
- List of Starlette Middleware instances to apply to the HTTP app
-### `TokenVerifier`
+### `TokenVerifier`
Base class for token verifiers (Resource Servers).
@@ -194,7 +194,7 @@ Token verifiers typically don't provide authentication routes by default.
**Methods:**
-#### `scopes_supported`
+#### `scopes_supported`
```python
scopes_supported(self) -> list[str]
@@ -208,7 +208,7 @@ where tokens contain short-form scopes but clients request full URI
scopes).
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -217,7 +217,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify a bearer token and return access info if valid.
-### `RemoteAuthProvider`
+### `RemoteAuthProvider`
Authentication provider for resource servers that verify tokens from known authorization servers.
@@ -234,7 +234,7 @@ the authorization servers that issue valid tokens.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -243,7 +243,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify token using the configured token verifier.
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
@@ -254,7 +254,7 @@ Get routes for this provider.
Creates protected resource metadata routes (RFC 9728).
-### `MultiAuth`
+### `MultiAuth`
Composes an optional auth server with additional token verifiers.
@@ -270,7 +270,7 @@ come from the server; verifiers contribute only token verification.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -283,7 +283,7 @@ it is logged and treated as a non-match so that remaining sources
still get a chance to verify the token.
-#### `set_mcp_path`
+#### `set_mcp_path`
```python
set_mcp_path(self, mcp_path: str | None) -> None
@@ -292,7 +292,7 @@ set_mcp_path(self, mcp_path: str | None) -> None
Propagate MCP path to the server and all verifiers.
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
@@ -301,7 +301,7 @@ get_routes(self, mcp_path: str | None = None) -> list[Route]
Delegate route creation to the server.
-#### `get_well_known_routes`
+#### `get_well_known_routes`
```python
get_well_known_routes(self, mcp_path: str | None = None) -> list[Route]
@@ -313,7 +313,7 @@ This ensures that server-specific well-known route logic (e.g.,
OAuthProvider's RFC 8414 path-aware discovery) is preserved.
-### `OAuthProvider`
+### `OAuthProvider`
OAuth Authorization Server provider.
@@ -324,7 +324,7 @@ authorization flows, token issuance, and token verification.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -342,7 +342,7 @@ to our existing load_access_token method.
- AccessToken object if valid, None if invalid or expired
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
@@ -358,7 +358,7 @@ This method creates the full set of OAuth routes including:
- List of OAuth routes
-#### `get_well_known_routes`
+#### `get_well_known_routes`
```python
get_well_known_routes(self, mcp_path: str | None = None) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-auth-cimd.mdx b/docs/python-sdk/fastmcp-server-auth-cimd.mdx
index dda5d28c6..7cb0dabdc 100644
--- a/docs/python-sdk/fastmcp-server-auth-cimd.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-cimd.mdx
@@ -129,6 +129,9 @@ validate_redirect_uri(self, doc: CIMDDocument, redirect_uri: str) -> bool
Validate that a redirect_uri is allowed by the CIMD document.
+Uses component-level matching (scheme, host, port, path) which correctly
+handles RFC 8252 §7.3 loopback port flexibility and wildcard patterns.
+
**Args:**
- `doc`: The CIMD document
- `redirect_uri`: The redirect URI to validate
@@ -137,7 +140,7 @@ Validate that a redirect_uri is allowed by the CIMD document.
- True if valid, False otherwise
-### `CIMDAssertionValidator`
+### `CIMDAssertionValidator`
Validates JWT assertions for private_key_jwt CIMD clients.
@@ -153,7 +156,7 @@ JTI replay protection uses TTL-based caching to ensure proper security:
**Methods:**
-#### `validate_assertion`
+#### `validate_assertion`
```python
validate_assertion(self, assertion: str, client_id: str, token_endpoint: str, cimd_doc: CIMDDocument) -> bool
@@ -174,7 +177,7 @@ Validate JWT assertion from client.
- `ValueError`: If validation fails
-### `CIMDClientManager`
+### `CIMDClientManager`
Manages all CIMD client operations for OAuth proxy.
@@ -191,7 +194,7 @@ single, focused manager class.
**Methods:**
-#### `is_cimd_client_id`
+#### `is_cimd_client_id`
```python
is_cimd_client_id(self, client_id: str) -> bool
@@ -206,7 +209,7 @@ Check if client_id is a CIMD URL.
- True if client_id is an HTTPS URL (CIMD format)
-#### `get_client`
+#### `get_client`
```python
get_client(self, client_id_url: str)
@@ -221,7 +224,7 @@ Fetch CIMD document and create synthetic OAuth client.
- OAuthProxyClient with CIMD document attached, or None if fetch fails
-#### `validate_private_key_jwt`
+#### `validate_private_key_jwt`
```python
validate_private_key_jwt(self, assertion: str, client, token_endpoint: str) -> bool
diff --git a/docs/python-sdk/fastmcp-server-auth-jwt_issuer.mdx b/docs/python-sdk/fastmcp-server-auth-jwt_issuer.mdx
index b30f3090b..9ca0d77ad 100644
--- a/docs/python-sdk/fastmcp-server-auth-jwt_issuer.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-jwt_issuer.mdx
@@ -15,7 +15,7 @@ This maintains proper OAuth 2.0 token audience boundaries.
## Functions
-### `derive_jwt_key`
+### `derive_jwt_key`
```python
derive_jwt_key() -> bytes
@@ -27,7 +27,7 @@ Derive JWT signing key from a high-entropy or low-entropy key material and serve
## Classes
-### `JWTIssuer`
+### `JWTIssuer`
Issues and validates FastMCP-signed JWT tokens using HS256.
@@ -39,7 +39,7 @@ a key derived from the upstream client secret.
**Methods:**
-#### `issue_access_token`
+#### `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
@@ -62,7 +62,7 @@ which contains actual user identity and authorization data.
- Signed JWT token
-#### `issue_refresh_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
@@ -85,18 +85,20 @@ token which contains actual user identity and authorization data.
- Signed JWT token
-#### `verify_token`
+#### `verify_token`
```python
-verify_token(self, token: str) -> dict[str, Any]
+verify_token(self, token: str, expected_token_use: str = 'access') -> dict[str, Any]
```
Verify and decode a FastMCP token.
-Validates JWT signature, expiration, issuer, and audience.
+Validates JWT signature, expiration, issuer, audience, and token type.
**Args:**
- `token`: JWT token to verify
+- `expected_token_use`: Expected token type ("access" or "refresh").
+Defaults to "access", which rejects refresh tokens.
**Returns:**
- Decoded token payload
diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-proxy.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy-proxy.mdx
index dd1400086..2f7742f26 100644
--- a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-proxy.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-oauth_proxy-proxy.mdx
@@ -26,7 +26,7 @@ production use with enterprise identity providers.
## Classes
-### `OAuthProxy`
+### `OAuthProxy`
OAuth provider that presents a DCR-compliant interface while proxying to non-DCR IDPs.
@@ -140,7 +140,7 @@ Handles provider-specific requirements:
**Methods:**
-#### `set_mcp_path`
+#### `set_mcp_path`
```python
set_mcp_path(self, mcp_path: str | None) -> None
@@ -157,7 +157,7 @@ this specific MCP endpoint.
- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
-#### `jwt_issuer`
+#### `jwt_issuer`
```python
jwt_issuer(self) -> JWTIssuer
@@ -169,7 +169,7 @@ 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`
+#### `get_client`
```python
get_client(self, client_id: str) -> OAuthClientInformationFull | None
@@ -182,7 +182,7 @@ 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`
+#### `register_client`
```python
register_client(self, client_info: OAuthClientInformationFull) -> None
@@ -196,7 +196,7 @@ 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`
+#### `authorize`
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
@@ -214,7 +214,7 @@ If consent is disabled (require_authorization_consent=False), skip the consent s
and redirect directly to the upstream IdP.
-#### `load_authorization_code`
+#### `load_authorization_code`
```python
load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None
@@ -226,7 +226,7 @@ Look up our client code and return authorization code object
with PKCE challenge for validation.
-#### `exchange_authorization_code`
+#### `exchange_authorization_code`
```python
exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken
@@ -244,7 +244,7 @@ Implements the token factory pattern:
PKCE validation is handled by the MCP framework before this method is called.
-#### `load_refresh_token`
+#### `load_refresh_token`
```python
load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None
@@ -256,7 +256,7 @@ Looks up by token hash and reconstructs the RefreshToken object.
Validates that the token belongs to the requesting client.
-#### `exchange_refresh_token`
+#### `exchange_refresh_token`
```python
exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken
@@ -273,7 +273,7 @@ Implements two-tier refresh:
6. Keep same FastMCP refresh token (unless upstream rotates)
-#### `load_access_token`
+#### `load_access_token`
```python
load_access_token(self, token: str) -> AccessToken | None
@@ -286,13 +286,14 @@ This implements the token swap pattern:
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
+5. If upstream validation fails, attempt transparent refresh
+6. 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`
+#### `revoke_token`
```python
revoke_token(self, token: AccessToken | RefreshToken) -> None
@@ -305,7 +306,7 @@ For all tokens, attempts upstream revocation if endpoint is configured.
Access token JTI mappings expire via TTL.
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx b/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
index 183380ed9..a9160db17 100644
--- a/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
@@ -19,7 +19,7 @@ This implementation is based on:
## Classes
-### `OIDCConfiguration`
+### `OIDCConfiguration`
OIDC Configuration.
@@ -27,7 +27,7 @@ OIDC Configuration.
**Methods:**
-#### `get_oidc_configuration`
+#### `get_oidc_configuration`
```python
get_oidc_configuration(cls, config_url: AnyHttpUrl) -> Self
@@ -41,7 +41,7 @@ Get the OIDC configuration for the specified config URL.
- `timeout_seconds`: HTTP request timeout in seconds
-### `OIDCProxy`
+### `OIDCProxy`
OAuth provider that wraps OAuthProxy to provide configuration via an OIDC configuration URL.
@@ -52,7 +52,7 @@ that is OIDC compliant.
**Methods:**
-#### `get_oidc_configuration`
+#### `get_oidc_configuration`
```python
get_oidc_configuration(self, config_url: AnyHttpUrl, strict: bool | None, timeout_seconds: int | None) -> OIDCConfiguration
@@ -66,7 +66,7 @@ Gets the OIDC configuration for the specified configuration URL.
- `timeout_seconds`: HTTP request timeout in seconds
-#### `get_token_verifier`
+#### `get_token_verifier`
```python
get_token_verifier(self) -> TokenVerifier
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-auth0.mdx b/docs/python-sdk/fastmcp-server-auth-providers-auth0.mdx
index 140ddd193..350d3f2e6 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-auth0.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-auth0.mdx
@@ -31,7 +31,7 @@ Example:
## Classes
-### `Auth0Provider`
+### `Auth0Provider`
An Auth0 provider implementation for FastMCP.
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-aws.mdx b/docs/python-sdk/fastmcp-server-auth-providers-aws.mdx
index 5803d4f62..c8d9ea795 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-aws.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-aws.mdx
@@ -31,7 +31,7 @@ Example:
## Classes
-### `AWSCognitoTokenVerifier`
+### `AWSCognitoTokenVerifier`
Token verifier that filters claims to Cognito-specific subset.
@@ -39,7 +39,7 @@ Token verifier that filters claims to Cognito-specific subset.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -48,7 +48,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify token and filter claims to Cognito-specific subset.
-### `AWSCognitoProvider`
+### `AWSCognitoProvider`
Complete AWS Cognito OAuth provider for FastMCP.
@@ -66,10 +66,10 @@ Features:
**Methods:**
-#### `get_token_verifier`
+#### `get_token_verifier`
```python
-get_token_verifier(self) -> TokenVerifier
+get_token_verifier(self) -> AWSCognitoTokenVerifier
```
Creates a Cognito-specific token verifier with claim filtering.
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
index 4d3140c4a..6301e4c28 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
@@ -14,7 +14,7 @@ using the OAuth Proxy pattern for non-DCR OAuth flows.
## Functions
-### `EntraOBOToken`
+### `EntraOBOToken`
```python
EntraOBOToken(scopes: list[str]) -> str
@@ -43,7 +43,7 @@ or OBO exchange fails
## Classes
-### `AzureProvider`
+### `AzureProvider`
Azure (Microsoft Entra) OAuth provider for FastMCP.
@@ -78,7 +78,7 @@ Setup:
**Methods:**
-#### `authorize`
+#### `authorize`
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
@@ -98,7 +98,7 @@ scopes to determine the resource/audience instead of a separate parameter.
- Authorization URL to redirect the user to Azure AD
-#### `get_obo_credential`
+#### `get_obo_credential`
```python
get_obo_credential(self, user_assertion: str) -> OnBehalfOfCredential
@@ -120,7 +120,7 @@ calls multiple tools with the same scopes.
- `ImportError`: If azure-identity is not installed (requires fastmcp[azure]).
-#### `close_obo_credentials`
+#### `close_obo_credentials`
```python
close_obo_credentials(self) -> None
@@ -129,7 +129,7 @@ close_obo_credentials(self) -> None
Close all cached OBO credentials.
-### `AzureJWTVerifier`
+### `AzureJWTVerifier`
JWT verifier pre-configured for Azure AD / Microsoft Entra ID.
@@ -166,7 +166,7 @@ Example::
**Methods:**
-#### `scopes_supported`
+#### `scopes_supported`
```python
scopes_supported(self) -> list[str]
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-clerk.mdx b/docs/python-sdk/fastmcp-server-auth-providers-clerk.mdx
new file mode 100644
index 000000000..5add0fff2
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-auth-providers-clerk.mdx
@@ -0,0 +1,94 @@
+---
+title: clerk
+sidebarTitle: clerk
+---
+
+# `fastmcp.server.auth.providers.clerk`
+
+
+Clerk OAuth provider for FastMCP.
+
+This module provides a complete Clerk OAuth integration that's ready to use
+with a Clerk domain, client ID, and client secret. It handles all the complexity
+of Clerk's OAuth/OIDC flow, token validation, and user management.
+
+Clerk uses standard OIDC endpoints derived from the instance domain
+(e.g., ``https://.clerk.accounts.dev``). Token verification is
+performed via the introspection endpoint (RFC 7662) for security-critical
+checks (active status, audience, scopes), followed by the userinfo endpoint
+for profile enrichment. Userinfo failure is non-fatal.
+
+Example:
+ ```python
+ from fastmcp import FastMCP
+ from fastmcp.server.auth.providers.clerk import ClerkProvider
+
+ auth = ClerkProvider(
+ domain="saving-primate-16.clerk.accounts.dev",
+ client_id="your-clerk-client-id",
+ client_secret="your-clerk-client-secret",
+ base_url="https://my-server.com",
+ )
+
+ mcp = FastMCP("My Protected Server", auth=auth)
+ ```
+
+
+## Classes
+
+### `ClerkTokenVerifier`
+
+
+Token verifier for Clerk OAuth tokens.
+
+Clerk issues standard OIDC tokens. Verification uses the introspection
+endpoint (RFC 7662) as the primary security gate — it confirms the token
+is active and provides metadata (scopes, expiry, audience). The userinfo
+endpoint is called second for profile enrichment (name, email, picture)
+and its failure is non-fatal.
+
+When a ``client_id`` is configured, the audience from introspection is
+validated against it. When ``required_scopes`` are configured,
+introspection must return the token's scopes — the verifier will not
+assume scopes when introspection is unavailable.
+
+
+**Methods:**
+
+#### `verify_token`
+
+```python
+verify_token(self, token: str) -> AccessToken | None
+```
+
+Verify a Clerk OAuth token via introspection and userinfo.
+
+Calls the introspection endpoint first to validate the token and
+retrieve auth metadata (active status, scopes, expiry, audience).
+If the token passes security checks, the userinfo endpoint is called
+for profile enrichment. Userinfo failure is non-fatal.
+
+When a ``client_id`` is configured, the token's audience must match it.
+When ``required_scopes`` are configured, introspection must confirm
+them; tokens are rejected if scope information is unavailable.
+
+
+### `ClerkProvider`
+
+
+Complete Clerk OAuth provider for FastMCP.
+
+This provider makes it trivial to add Clerk OAuth protection to any
+FastMCP server. Provide your Clerk instance domain, OAuth app credentials,
+and a base URL, and you're ready to go.
+
+Clerk uses standard OIDC endpoints derived from the instance domain.
+All endpoint URLs are constructed automatically from the domain parameter.
+
+Features:
+- Transparent OAuth proxy to Clerk
+- Automatic token validation via Clerk's userinfo & introspection APIs
+- User information extraction from Clerk's OIDC claims
+- PKCE support (S256)
+- Minimal configuration required
+
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-descope.mdx b/docs/python-sdk/fastmcp-server-auth-providers-descope.mdx
index 3436aa5c3..45dce934e 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-descope.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-descope.mdx
@@ -43,7 +43,7 @@ https://docs.descope.com/identity-federation/inbound-apps/creating-inbound-apps#
**Methods:**
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-discord.mdx b/docs/python-sdk/fastmcp-server-auth-providers-discord.mdx
index 61b024b63..57d3b743b 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-discord.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-discord.mdx
@@ -29,7 +29,7 @@ Example:
## Classes
-### `DiscordTokenVerifier`
+### `DiscordTokenVerifier`
Token verifier for Discord OAuth tokens.
@@ -40,7 +40,7 @@ by calling Discord's tokeninfo API to check if they're valid and get user info.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -49,7 +49,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify Discord OAuth token by calling Discord's tokeninfo API.
-### `DiscordProvider`
+### `DiscordProvider`
Complete Discord OAuth provider for FastMCP.
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-github.mdx b/docs/python-sdk/fastmcp-server-auth-providers-github.mdx
index 66a808136..48d8e24da 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-github.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-github.mdx
@@ -29,7 +29,7 @@ Example:
## Classes
-### `GitHubTokenVerifier`
+### `GitHubTokenVerifier`
Token verifier for GitHub OAuth tokens.
@@ -37,10 +37,14 @@ 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.
+Caching is disabled by default. Set ``cache_ttl_seconds`` to a positive
+integer to cache successful verification results and avoid repeated
+GitHub API calls for the same token.
+
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -49,7 +53,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify GitHub OAuth token by calling GitHub API.
-### `GitHubProvider`
+### `GitHubProvider`
Complete GitHub OAuth provider for FastMCP.
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-google.mdx b/docs/python-sdk/fastmcp-server-auth-providers-google.mdx
index 880488438..05f2400b0 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-google.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-google.mdx
@@ -29,27 +29,35 @@ Example:
## Classes
-### `GoogleTokenVerifier`
+### `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.
+Google OAuth tokens are opaque (not JWTs), so we verify them by calling
+Google's tokeninfo endpoint with the access token as a query parameter.
+This returns the OAuth app ID (``aud``), granted scopes, and expiry time.
+User profile data (name, picture, etc.) is fetched separately from the
+v2 userinfo endpoint when the token is valid.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
```
-Verify Google OAuth token by calling Google's tokeninfo API.
+Verify a Google OAuth token using the tokeninfo endpoint.
+
+Calls ``https://oauth2.googleapis.com/tokeninfo?access_token=TOKEN``
+to validate the token and retrieve the OAuth app ID (``aud``), granted
+scopes, and expiry time. On success, fetches user profile data from
+the v2 userinfo endpoint to populate name, picture, and locale claims.
-### `GoogleProvider`
+### `GoogleProvider`
Complete Google OAuth provider for FastMCP.
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-introspection.mdx b/docs/python-sdk/fastmcp-server-auth-providers-introspection.mdx
index 811737e34..8666cc726 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-introspection.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-introspection.mdx
@@ -31,7 +31,7 @@ Example:
## Classes
-### `IntrospectionTokenVerifier`
+### `IntrospectionTokenVerifier`
OAuth 2.0 Token Introspection verifier (RFC 7662).
@@ -59,7 +59,7 @@ introspection endpoint (e.g., ``cache_ttl_seconds=300`` for 5 minutes).
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx b/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx
index 6ba9054c2..b049a4d64 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx
@@ -60,7 +60,7 @@ Generate a test JWT token for testing purposes.
- `kid`: Key ID to include in header
-### `JWTVerifier`
+### `JWTVerifier`
JWT token verifier supporting both asymmetric (RSA/ECDSA) and symmetric (HMAC) algorithms.
@@ -82,7 +82,7 @@ Use this when:
**Methods:**
-#### `load_access_token`
+#### `load_access_token`
```python
load_access_token(self, token: str) -> AccessToken | None
@@ -97,7 +97,7 @@ Validate a JWT bearer token and return an AccessToken when the token is valid.
- 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`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -115,7 +115,7 @@ to our existing load_access_token method.
- AccessToken object if valid, None if invalid or expired
-### `StaticTokenVerifier`
+### `StaticTokenVerifier`
Simple static token verifier for testing and development.
@@ -136,7 +136,7 @@ WARNING: Never use this in production - tokens are stored in plain text!
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-oci.mdx b/docs/python-sdk/fastmcp-server-auth-providers-oci.mdx
index b88c7c49e..9f1be4fc1 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-oci.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-oci.mdx
@@ -87,7 +87,7 @@ Example:
## Classes
-### `OCIProvider`
+### `OCIProvider`
An OCI IAM Domain provider implementation for FastMCP.
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-propelauth.mdx b/docs/python-sdk/fastmcp-server-auth-providers-propelauth.mdx
index 3b31b00d8..df066f733 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-propelauth.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-propelauth.mdx
@@ -43,7 +43,7 @@ https://docs.propelauth.com/mcp-authentication/overview
**Methods:**
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
@@ -59,7 +59,7 @@ and creates an authorization server metadata route that forwards to PropelAuth's
This is used to advertise the resource URL in metadata.
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx b/docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx
index 1aa125c6c..cefe81c23 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-scalekit.mdx
@@ -44,7 +44,7 @@ https://docs.scalekit.com/mcp/overview/
**Methods:**
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-supabase.mdx b/docs/python-sdk/fastmcp-server-auth-providers-supabase.mdx
index c44deecae..45f576a8a 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-supabase.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-supabase.mdx
@@ -29,7 +29,7 @@ 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)
+ - Configure your JWT algorithm in Supabase Auth settings (RS256 or ES256)
- Asymmetric keys (RS256/ES256) are recommended for production
2. JWT Verification:
@@ -50,7 +50,7 @@ https://supabase.com/docs/guides/auth/jwts
**Methods:**
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
index cb263d9ec..f93a9f0c6 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
@@ -18,7 +18,7 @@ Choose based on your WorkOS setup and authentication requirements.
## Classes
-### `WorkOSTokenVerifier`
+### `WorkOSTokenVerifier`
Token verifier for WorkOS OAuth tokens.
@@ -29,7 +29,7 @@ the /oauth2/userinfo endpoint to check validity and get user info.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -38,7 +38,7 @@ verify_token(self, token: str) -> AccessToken | None
Verify WorkOS OAuth token by calling userinfo endpoint.
-### `WorkOSProvider`
+### `WorkOSProvider`
Complete WorkOS OAuth provider for FastMCP.
@@ -59,7 +59,7 @@ Setup Requirements:
4. Note your Client ID and Client Secret
-### `AuthKitProvider`
+### `AuthKitProvider`
AuthKit metadata provider for DCR (Dynamic Client Registration).
@@ -85,7 +85,7 @@ https://workos.com/docs/authkit/mcp/integrating/token-verification
**Methods:**
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
diff --git a/docs/python-sdk/fastmcp-server-auth-redirect_validation.mdx b/docs/python-sdk/fastmcp-server-auth-redirect_validation.mdx
index 65155a160..af57dd145 100644
--- a/docs/python-sdk/fastmcp-server-auth-redirect_validation.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-redirect_validation.mdx
@@ -14,7 +14,7 @@ protecting against userinfo-based bypass attacks like http://localhost@evil.com.
## Functions
-### `matches_allowed_pattern`
+### `matches_allowed_pattern`
```python
matches_allowed_pattern(uri: str, pattern: str) -> bool
@@ -43,7 +43,7 @@ naive string matching (e.g., http://localhost@evil.com).
- True if the URI matches the pattern
-### `validate_redirect_uri`
+### `validate_redirect_uri`
```python
validate_redirect_uri(redirect_uri: str | AnyUrl | None, allowed_patterns: list[str] | None) -> bool
diff --git a/docs/python-sdk/fastmcp-server-context.mdx b/docs/python-sdk/fastmcp-server-context.mdx
index 58a916faf..78371e393 100644
--- a/docs/python-sdk/fastmcp-server-context.mdx
+++ b/docs/python-sdk/fastmcp-server-context.mdx
@@ -319,7 +319,7 @@ request 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:
diff --git a/docs/python-sdk/fastmcp-server-dependencies.mdx b/docs/python-sdk/fastmcp-server-dependencies.mdx
index 60439e182..80b9f8421 100644
--- a/docs/python-sdk/fastmcp-server-dependencies.mdx
+++ b/docs/python-sdk/fastmcp-server-dependencies.mdx
@@ -15,7 +15,7 @@ CurrentWorker) and background task execution require fastmcp[tasks].
## Functions
-### `get_task_context`
+### `get_task_context`
```python
get_task_context() -> TaskContextInfo | None
@@ -31,7 +31,7 @@ Returns None if not running in a task context (e.g., foreground execution).
- TaskContextInfo with task_id and session_id, or None if not in a task.
-### `register_task_session`
+### `register_task_session`
```python
register_task_session(session_id: str, session: ServerSession) -> None
@@ -49,7 +49,7 @@ client disconnects.
- `session`: The ServerSession instance
-### `get_task_session`
+### `get_task_session`
```python
get_task_session(session_id: str) -> ServerSession | None
@@ -65,7 +65,24 @@ Get a registered session by ID if still alive.
- The ServerSession if found and alive, None otherwise
-### `is_docket_available`
+### `register_task_server`
+
+```python
+register_task_server(task_id: str, server: FastMCP) -> None
+```
+
+
+Register the server for a background task.
+
+Called at task-submission time (inside the child server's call_tool
+context) so that background workers can resolve CurrentFastMCP() and
+ctx.fastmcp to the child server for mounted tasks.
+
+The map is bounded to avoid unbounded growth in long-lived servers.
+Evicted entries fall back to the ContextVar (parent server).
+
+
+### `is_docket_available`
```python
is_docket_available() -> bool
@@ -75,7 +92,7 @@ is_docket_available() -> bool
Check if pydocket is installed.
-### `require_docket`
+### `require_docket`
```python
require_docket(feature: str) -> None
@@ -89,7 +106,7 @@ Raise ImportError with install instructions if docket not available.
"CurrentDocket()"). Will be included in the error message.
-### `transform_context_annotations`
+### `transform_context_annotations`
```python
transform_context_annotations(fn: Callable[..., Any]) -> Callable[..., Any]
@@ -115,7 +132,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 +142,7 @@ get_context() -> Context
Get the current FastMCP Context instance directly.
-### `get_server`
+### `get_server`
```python
get_server() -> FastMCP
@@ -134,6 +151,10 @@ get_server() -> FastMCP
Get the current FastMCP server instance directly.
+In a background-task worker, checks the task-server map first so that
+mounted-child tasks resolve to the child server (not the parent that
+started the worker).
+
**Returns:**
- The active FastMCP server
@@ -141,7 +162,7 @@ Get the current FastMCP server instance directly.
- `RuntimeError`: If no server in context
-### `get_http_request`
+### `get_http_request`
```python
get_http_request() -> Request
@@ -151,9 +172,11 @@ get_http_request() -> Request
Get the current HTTP request.
Tries MCP SDK's request_ctx first, then falls back to FastMCP's HTTP context.
+In background tasks, returns a synthetic request populated with the
+snapshotted headers from the originating HTTP request.
-### `get_http_headers`
+### `get_http_headers`
```python
get_http_headers(include_all: bool = False, include: set[str] | None = None) -> dict[str, str]
@@ -174,7 +197,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
@@ -193,7 +216,7 @@ token snapshot stored in Redis at task submission time.
- 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]
@@ -218,7 +241,7 @@ Handles:
- 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 +267,7 @@ time, so all injection goes through the unified DI system.
which will be filtered out)
-### `CurrentContext`
+### `CurrentContext`
```python
CurrentContext() -> Context
@@ -263,7 +286,7 @@ current MCP operation (tool/resource/prompt call).
- `RuntimeError`: If no active context found (during resolution)
-### `OptionalCurrentContext`
+### `OptionalCurrentContext`
```python
OptionalCurrentContext() -> Context | None
@@ -273,7 +296,7 @@ OptionalCurrentContext() -> Context | None
Get the current FastMCP Context, or None when no context is active.
-### `CurrentDocket`
+### `CurrentDocket`
```python
CurrentDocket() -> Docket
@@ -293,7 +316,7 @@ automatically creates for background task scheduling.
- `ImportError`: If fastmcp[tasks] not installed
-### `CurrentWorker`
+### `CurrentWorker`
```python
CurrentWorker() -> Worker
@@ -313,7 +336,7 @@ automatically creates for background task processing.
- `ImportError`: If fastmcp[tasks] not installed
-### `CurrentFastMCP`
+### `CurrentFastMCP`
```python
CurrentFastMCP() -> FastMCP
@@ -331,7 +354,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 +374,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 +392,7 @@ transport.
- A dependency that resolves to a dictionary of header name -> value
-### `CurrentAccessToken`
+### `CurrentAccessToken`
```python
CurrentAccessToken() -> AccessToken
@@ -388,7 +411,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,7 +436,7 @@ without needing the full token object.
## Classes
-### `TaskContextInfo`
+### `TaskContextInfo`
Information about the current background task context.
@@ -422,7 +445,7 @@ Returned by ``get_task_context()`` when running inside a Docket worker.
Contains identifiers needed to communicate with the MCP session.
-### `ProgressLike`
+### `ProgressLike`
Protocol for progress tracking interface.
@@ -433,7 +456,7 @@ and Docket's Progress (worker context).
**Methods:**
-#### `current`
+#### `current`
```python
current(self) -> int | None
@@ -442,7 +465,7 @@ current(self) -> int | None
Current progress value.
-#### `total`
+#### `total`
```python
total(self) -> int
@@ -451,7 +474,7 @@ total(self) -> int
Total/target progress value.
-#### `message`
+#### `message`
```python
message(self) -> str | None
@@ -460,7 +483,7 @@ message(self) -> str | None
Current progress message.
-#### `set_total`
+#### `set_total`
```python
set_total(self, total: int) -> None
@@ -469,7 +492,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 +501,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 +510,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 +522,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 +549,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 +558,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,7 +567,7 @@ 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.
@@ -561,7 +584,7 @@ is installed.
**Methods:**
-#### `current`
+#### `current`
```python
current(self) -> int | None
@@ -570,7 +593,7 @@ current(self) -> int | None
Current progress value.
-#### `total`
+#### `total`
```python
total(self) -> int
@@ -579,7 +602,7 @@ total(self) -> int
Total/target progress value.
-#### `message`
+#### `message`
```python
message(self) -> str | None
@@ -588,7 +611,7 @@ message(self) -> str | None
Current progress message.
-#### `set_total`
+#### `set_total`
```python
set_total(self, total: int) -> None
@@ -597,7 +620,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 +629,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-http.mdx b/docs/python-sdk/fastmcp-server-http.mdx
index 9b2fe758d..9b4c309af 100644
--- a/docs/python-sdk/fastmcp-server-http.mdx
+++ b/docs/python-sdk/fastmcp-server-http.mdx
@@ -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,7 +54,7 @@ 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
diff --git a/docs/python-sdk/fastmcp-server-low_level.mdx b/docs/python-sdk/fastmcp-server-low_level.mdx
index 78acc7225..7456a5940 100644
--- a/docs/python-sdk/fastmcp-server-low_level.mdx
+++ b/docs/python-sdk/fastmcp-server-low_level.mdx
@@ -15,7 +15,7 @@ ServerSession that routes initialization requests through FastMCP middleware.
**Methods:**
-#### `fastmcp`
+#### `fastmcp`
```python
fastmcp(self) -> FastMCP
@@ -24,7 +24,7 @@ fastmcp(self) -> FastMCP
Get the FastMCP instance.
-#### `client_supports_extension`
+#### `client_supports_extension`
```python
client_supports_extension(self, extension_id: str) -> bool
@@ -36,11 +36,11 @@ Inspects the ``extensions`` extra field on ``ClientCapabilities``
sent by the client during initialization.
-### `LowLevelServer`
+### `LowLevelServer`
**Methods:**
-#### `fastmcp`
+#### `fastmcp`
```python
fastmcp(self) -> FastMCP
@@ -49,13 +49,13 @@ fastmcp(self) -> FastMCP
Get the FastMCP instance.
-#### `create_initialization_options`
+#### `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`
+#### `get_capabilities`
```python
get_capabilities(self, notification_options: NotificationOptions, experimental_capabilities: dict[str, dict[str, Any]]) -> mcp.types.ServerCapabilities
@@ -68,7 +68,7 @@ capabilities.experimental.tasks, which is required by the MCP spec and
enables proper task detection by clients like VS Code Copilot 1.107+.
-#### `run`
+#### `run`
```python
run(self, read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], write_stream: MemoryObjectSendStream[SessionMessage], initialization_options: InitializationOptions, raise_exceptions: bool = False, stateless: bool = False)
@@ -77,7 +77,7 @@ run(self, read_stream: MemoryObjectReceiveStream[SessionMessage | Exception], wr
Overrides the run method to use the MiddlewareServerSession.
-#### `read_resource`
+#### `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]]]
@@ -92,7 +92,7 @@ This decorator can be removed once the MCP SDK adds native CreateTaskResult supp
for resources.
-#### `get_prompt`
+#### `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]]]
diff --git a/docs/python-sdk/fastmcp-server-middleware-caching.mdx b/docs/python-sdk/fastmcp-server-middleware-caching.mdx
index 66b86999d..fbf9bba15 100644
--- a/docs/python-sdk/fastmcp-server-middleware-caching.mdx
+++ b/docs/python-sdk/fastmcp-server-middleware-caching.mdx
@@ -10,13 +10,13 @@ A middleware for response caching.
## Classes
-### `CachableResourceContent`
+### `CachableResourceContent`
A wrapper for ResourceContent that can be cached.
-### `CachableResourceResult`
+### `CachableResourceResult`
A wrapper for ResourceResult that can be cached.
@@ -24,47 +24,47 @@ A wrapper for ResourceResult that can be cached.
**Methods:**
-#### `get_size`
+#### `get_size`
```python
get_size(self) -> int
```
-#### `wrap`
+#### `wrap`
```python
wrap(cls, value: ResourceResult) -> Self
```
-#### `unwrap`
+#### `unwrap`
```python
unwrap(self) -> ResourceResult
```
-### `CachableToolResult`
+### `CachableToolResult`
**Methods:**
-#### `wrap`
+#### `wrap`
```python
wrap(cls, value: ToolResult) -> Self
```
-#### `unwrap`
+#### `unwrap`
```python
unwrap(self) -> ToolResult
```
-### `CachableMessage`
+### `CachableMessage`
A wrapper for Message that can be cached.
-### `CachablePromptResult`
+### `CachablePromptResult`
A wrapper for PromptResult that can be cached.
@@ -72,69 +72,69 @@ A wrapper for PromptResult that can be cached.
**Methods:**
-#### `get_size`
+#### `get_size`
```python
get_size(self) -> int
```
-#### `wrap`
+#### `wrap`
```python
wrap(cls, value: PromptResult) -> Self
```
-#### `unwrap`
+#### `unwrap`
```python
unwrap(self) -> PromptResult
```
-### `SharedMethodSettings`
+### `SharedMethodSettings`
Shared config for a cache method.
-### `ListToolsSettings`
+### `ListToolsSettings`
Configuration options for Tool-related caching.
-### `ListResourcesSettings`
+### `ListResourcesSettings`
Configuration options for Resource-related caching.
-### `ListPromptsSettings`
+### `ListPromptsSettings`
Configuration options for Prompt-related caching.
-### `CallToolSettings`
+### `CallToolSettings`
Configuration options for Tool-related caching.
-### `ReadResourceSettings`
+### `ReadResourceSettings`
Configuration options for Resource-related caching.
-### `GetPromptSettings`
+### `GetPromptSettings`
Configuration options for Prompt-related caching.
-### `ResponseCachingStatistics`
+### `ResponseCachingStatistics`
-### `ResponseCachingMiddleware`
+### `ResponseCachingMiddleware`
The response caching middleware offers a simple way to cache responses to mcp methods. The Middleware
@@ -151,7 +151,7 @@ Notes:
**Methods:**
-#### `on_list_tools`
+#### `on_list_tools`
```python
on_list_tools(self, context: MiddlewareContext[mcp.types.ListToolsRequest], call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]
@@ -161,7 +161,7 @@ List tools from the cache, if caching is enabled, and the result is in the cache
otherwise call the next middleware and store the result in the cache if caching is enabled.
-#### `on_list_resources`
+#### `on_list_resources`
```python
on_list_resources(self, context: MiddlewareContext[mcp.types.ListResourcesRequest], call_next: CallNext[mcp.types.ListResourcesRequest, Sequence[Resource]]) -> Sequence[Resource]
@@ -171,7 +171,7 @@ List resources from the cache, if caching is enabled, and the result is in the c
otherwise call the next middleware and store the result in the cache if caching is enabled.
-#### `on_list_prompts`
+#### `on_list_prompts`
```python
on_list_prompts(self, context: MiddlewareContext[mcp.types.ListPromptsRequest], call_next: CallNext[mcp.types.ListPromptsRequest, Sequence[Prompt]]) -> Sequence[Prompt]
@@ -181,7 +181,7 @@ List prompts from the cache, if caching is enabled, and the result is in the cac
otherwise call the next middleware and store the result in the cache if caching is enabled.
-#### `on_call_tool`
+#### `on_call_tool`
```python
on_call_tool(self, context: MiddlewareContext[mcp.types.CallToolRequestParams], call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult]) -> ToolResult
@@ -191,7 +191,7 @@ Call a tool from the cache, if caching is enabled, and the result is in the cach
otherwise call the next middleware and store the result in the cache if caching is enabled.
-#### `on_read_resource`
+#### `on_read_resource`
```python
on_read_resource(self, context: MiddlewareContext[mcp.types.ReadResourceRequestParams], call_next: CallNext[mcp.types.ReadResourceRequestParams, ResourceResult]) -> ResourceResult
@@ -201,7 +201,7 @@ Read a resource from the cache, if caching is enabled, and the result is in the
otherwise call the next middleware and store the result in the cache if caching is enabled.
-#### `on_get_prompt`
+#### `on_get_prompt`
```python
on_get_prompt(self, context: MiddlewareContext[mcp.types.GetPromptRequestParams], call_next: CallNext[mcp.types.GetPromptRequestParams, PromptResult]) -> PromptResult
@@ -211,7 +211,7 @@ Get a prompt from the cache, if caching is enabled, and the result is in the cac
otherwise call the next middleware and store the result in the cache if caching is enabled.
-#### `statistics`
+#### `statistics`
```python
statistics(self) -> ResponseCachingStatistics
diff --git a/docs/python-sdk/fastmcp-server-middleware-tool_injection.mdx b/docs/python-sdk/fastmcp-server-middleware-tool_injection.mdx
index 6c9c01346..549150689 100644
--- a/docs/python-sdk/fastmcp-server-middleware-tool_injection.mdx
+++ b/docs/python-sdk/fastmcp-server-middleware-tool_injection.mdx
@@ -10,7 +10,7 @@ A middleware for injecting tools into the MCP server context.
## Functions
-### `list_prompts`
+### `list_prompts`
```python
list_prompts(context: Context) -> list[Prompt]
@@ -20,7 +20,7 @@ list_prompts(context: Context) -> list[Prompt]
List prompts available on the server.
-### `get_prompt`
+### `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
@@ -30,7 +30,7 @@ get_prompt(context: Context, name: Annotated[str, 'The name of the prompt to ren
Render a prompt available on the server.
-### `list_resources`
+### `list_resources`
```python
list_resources(context: Context) -> list[mcp.types.Resource]
@@ -40,7 +40,7 @@ list_resources(context: Context) -> list[mcp.types.Resource]
List resources available on the server.
-### `read_resource`
+### `read_resource`
```python
read_resource(context: Context, uri: Annotated[AnyUrl | str, 'The URI of the resource to read.']) -> ResourceResult
@@ -52,7 +52,7 @@ Read a resource available on the server.
## Classes
-### `ToolInjectionMiddleware`
+### `ToolInjectionMiddleware`
A middleware for injecting tools into the context.
@@ -60,7 +60,7 @@ A middleware for injecting tools into the context.
**Methods:**
-#### `on_list_tools`
+#### `on_list_tools`
```python
on_list_tools(self, context: MiddlewareContext[mcp.types.ListToolsRequest], call_next: CallNext[mcp.types.ListToolsRequest, Sequence[Tool]]) -> Sequence[Tool]
@@ -69,7 +69,7 @@ on_list_tools(self, context: MiddlewareContext[mcp.types.ListToolsRequest], call
Inject tools into the response.
-#### `on_call_tool`
+#### `on_call_tool`
```python
on_call_tool(self, context: MiddlewareContext[mcp.types.CallToolRequestParams], call_next: CallNext[mcp.types.CallToolRequestParams, ToolResult]) -> ToolResult
@@ -78,14 +78,20 @@ on_call_tool(self, context: MiddlewareContext[mcp.types.CallToolRequestParams],
Intercept tool calls to injected tools.
-### `PromptToolMiddleware`
+### `PromptToolMiddleware`
A middleware for injecting prompts as tools into the context.
+.. deprecated::
+ Use ``fastmcp.server.transforms.PromptsAsTools`` instead.
-### `ResourceToolMiddleware`
+
+### `ResourceToolMiddleware`
A middleware for injecting resources as tools into the context.
+.. deprecated::
+ Use ``fastmcp.server.transforms.ResourcesAsTools`` instead.
+
diff --git a/docs/python-sdk/fastmcp-server-mixins-lifespan.mdx b/docs/python-sdk/fastmcp-server-mixins-lifespan.mdx
index 9d0eec22e..fe786652c 100644
--- a/docs/python-sdk/fastmcp-server-mixins-lifespan.mdx
+++ b/docs/python-sdk/fastmcp-server-mixins-lifespan.mdx
@@ -10,7 +10,7 @@ Lifespan and Docket task infrastructure for FastMCP Server.
## Classes
-### `LifespanMixin`
+### `LifespanMixin`
Mixin providing lifespan and Docket task infrastructure for FastMCP.
@@ -18,7 +18,7 @@ Mixin providing lifespan and Docket task infrastructure for FastMCP.
**Methods:**
-#### `docket`
+#### `docket`
```python
docket(self: FastMCP) -> Docket | None
diff --git a/docs/python-sdk/fastmcp-server-mixins-mcp_operations.mdx b/docs/python-sdk/fastmcp-server-mixins-mcp_operations.mdx
index 0b6bef529..6fbd2855a 100644
--- a/docs/python-sdk/fastmcp-server-mixins-mcp_operations.mdx
+++ b/docs/python-sdk/fastmcp-server-mixins-mcp_operations.mdx
@@ -10,7 +10,7 @@ MCP protocol handler setup and wire-format handlers for FastMCP Server.
## Classes
-### `MCPOperationsMixin`
+### `MCPOperationsMixin`
Mixin providing MCP protocol handler setup and wire-format handlers.
diff --git a/docs/python-sdk/fastmcp-server-mixins-transport.mdx b/docs/python-sdk/fastmcp-server-mixins-transport.mdx
index c5cc4e2fd..ad4e0b60e 100644
--- a/docs/python-sdk/fastmcp-server-mixins-transport.mdx
+++ b/docs/python-sdk/fastmcp-server-mixins-transport.mdx
@@ -10,7 +10,7 @@ Transport-related methods for FastMCP Server.
## Classes
-### `TransportMixin`
+### `TransportMixin`
Mixin providing transport-related methods for FastMCP.
@@ -20,7 +20,7 @@ Includes HTTP/stdio/SSE transport handling and custom HTTP routes.
**Methods:**
-#### `run_async`
+#### `run_async`
```python
run_async(self: FastMCP, transport: Transport | None = None, show_banner: bool | None = None, **transport_kwargs: Any) -> None
@@ -34,7 +34,7 @@ Run the FastMCP server asynchronously.
FASTMCP_SHOW_SERVER_BANNER setting (default\: True).
-#### `run`
+#### `run`
```python
run(self: FastMCP, transport: Transport | None = None, show_banner: bool | None = None, **transport_kwargs: Any) -> None
@@ -48,7 +48,7 @@ Run the FastMCP server. Note this is a synchronous function.
FASTMCP_SHOW_SERVER_BANNER setting (default\: True).
-#### `custom_route`
+#### `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]]]
@@ -69,7 +69,7 @@ Starlette's reverse URL lookup feature)
- `include_in_schema`: Whether to include in OpenAPI schema, defaults to True
-#### `run_stdio_async`
+#### `run_stdio_async`
```python
run_stdio_async(self: FastMCP, show_banner: bool = True, log_level: str | None = None, stateless: bool = False) -> None
@@ -83,7 +83,7 @@ Run the server using stdio transport.
- `stateless`: Whether to run in stateless mode (no session initialization)
-#### `run_http_async`
+#### `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
@@ -104,7 +104,7 @@ Run the server using HTTP transport.
- `stateless`: Alias for stateless_http for CLI consistency
-#### `http_app`
+#### `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
diff --git a/docs/python-sdk/fastmcp-server-openapi-server.mdx b/docs/python-sdk/fastmcp-server-openapi-server.mdx
index 4f751e090..374eac2ba 100644
--- a/docs/python-sdk/fastmcp-server-openapi-server.mdx
+++ b/docs/python-sdk/fastmcp-server-openapi-server.mdx
@@ -21,7 +21,7 @@ This class is deprecated. Use FastMCP with OpenAPIProvider instead:
## Classes
-### `FastMCPOpenAPI`
+### `FastMCPOpenAPI`
FastMCP server implementation that creates components from an OpenAPI schema.
diff --git a/docs/python-sdk/fastmcp-server-providers-aggregate.mdx b/docs/python-sdk/fastmcp-server-providers-aggregate.mdx
index e0a8103da..ffc3d8dbb 100644
--- a/docs/python-sdk/fastmcp-server-providers-aggregate.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-aggregate.mdx
@@ -64,7 +64,16 @@ FastMCPProvider to ensure middleware is invoked correctly.
- Prompts become "namespace_promptname"
-#### `get_tasks`
+#### `get_app_tool`
+
+```python
+get_app_tool(self, app_name: str, tool_name: str) -> Tool | None
+```
+
+Query all child providers for an app tool.
+
+
+#### `get_tasks`
```python
get_tasks(self) -> Sequence[FastMCPComponent]
@@ -73,7 +82,7 @@ get_tasks(self) -> Sequence[FastMCPComponent]
Get all task-eligible components from all providers.
-#### `lifespan`
+#### `lifespan`
```python
lifespan(self) -> AsyncIterator[None]
diff --git a/docs/python-sdk/fastmcp-server-providers-base.mdx b/docs/python-sdk/fastmcp-server-providers-base.mdx
index 09d02595b..715f52a0c 100644
--- a/docs/python-sdk/fastmcp-server-providers-base.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-base.mdx
@@ -132,7 +132,22 @@ allowing session-level transforms to override provider-level disables.
- The tool if found (may be marked disabled), None if not found.
-#### `list_resources`
+#### `get_app_tool`
+
+```python
+get_app_tool(self, app_name: str, tool_name: str) -> Tool | None
+```
+
+Look up an app-visible tool by original name, bypassing transforms.
+
+Searches for a tool named ``tool_name`` tagged with the given app
+name. Skips the transform chain entirely.
+
+**Returns:**
+- The tool if found and tagged with the given app name, else None.
+
+
+#### `list_resources`
```python
list_resources(self) -> Sequence[Resource]
@@ -143,7 +158,7 @@ List resources with all transforms applied.
Components may be marked as disabled but are NOT filtered here.
-#### `get_resource`
+#### `get_resource`
```python
get_resource(self, uri: str, version: VersionSpec | None = None) -> Resource | None
@@ -162,7 +177,7 @@ Note: This method does NOT filter disabled components. The Server
- The resource if found (may be marked disabled), None if not found.
-#### `list_resource_templates`
+#### `list_resource_templates`
```python
list_resource_templates(self) -> Sequence[ResourceTemplate]
@@ -173,7 +188,7 @@ List resource templates with all transforms applied.
Components may be marked as disabled but are NOT filtered here.
-#### `get_resource_template`
+#### `get_resource_template`
```python
get_resource_template(self, uri: str, version: VersionSpec | None = None) -> ResourceTemplate | None
@@ -192,7 +207,7 @@ Note: This method does NOT filter disabled components. The Server
- The template if found (may be marked disabled), None if not found.
-#### `list_prompts`
+#### `list_prompts`
```python
list_prompts(self) -> Sequence[Prompt]
@@ -203,7 +218,7 @@ List prompts with all transforms applied.
Components may be marked as disabled but are NOT filtered here.
-#### `get_prompt`
+#### `get_prompt`
```python
get_prompt(self, name: str, version: VersionSpec | None = None) -> Prompt | None
@@ -222,7 +237,7 @@ Note: This method does NOT filter disabled components. The Server
- The prompt if found (may be marked disabled), None if not found.
-#### `get_tasks`
+#### `get_tasks`
```python
get_tasks(self) -> Sequence[FastMCPComponent]
@@ -237,7 +252,7 @@ for components with task_config.mode != 'forbidden'.
Used by the server during startup to register functions with Docket.
-#### `lifespan`
+#### `lifespan`
```python
lifespan(self) -> AsyncIterator[None]
@@ -253,7 +268,7 @@ The lifespan scope matches the server's lifespan - code before yield
runs at startup, code after yield runs at shutdown.
-#### `enable`
+#### `enable`
```python
enable(self) -> Self
@@ -281,7 +296,7 @@ VersionSpec(gte="v2")). Unversioned components will not match.
- Self for method chaining.
-#### `disable`
+#### `disable`
```python
disable(self) -> Self
diff --git a/docs/python-sdk/fastmcp-server-providers-fastmcp_provider.mdx b/docs/python-sdk/fastmcp-server-providers-fastmcp_provider.mdx
index 84d60da3d..ec5333e78 100644
--- a/docs/python-sdk/fastmcp-server-providers-fastmcp_provider.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-fastmcp_provider.mdx
@@ -39,7 +39,7 @@ wrap(cls, server: Any, tool: Tool) -> FastMCPProviderTool
Wrap a Tool to delegate execution to the server's middleware.
-#### `run`
+#### `run`
```python
run(self, arguments: dict[str, Any]) -> ToolResult
@@ -51,13 +51,13 @@ 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`
+#### `get_span_attributes`
```python
get_span_attributes(self) -> dict[str, Any]
```
-### `FastMCPProviderResource`
+### `FastMCPProviderResource`
Resource that delegates reading to a wrapped server's read_resource().
@@ -68,7 +68,7 @@ When `read()` is called, this resource invokes the wrapped server's
**Methods:**
-#### `wrap`
+#### `wrap`
```python
wrap(cls, server: Any, resource: Resource) -> FastMCPProviderResource
@@ -77,13 +77,13 @@ wrap(cls, server: Any, resource: Resource) -> FastMCPProviderResource
Wrap a Resource to delegate reading to the server's middleware.
-#### `get_span_attributes`
+#### `get_span_attributes`
```python
get_span_attributes(self) -> dict[str, Any]
```
-### `FastMCPProviderPrompt`
+### `FastMCPProviderPrompt`
Prompt that delegates rendering to a wrapped server's render_prompt().
@@ -94,7 +94,7 @@ When `render()` is called, this prompt invokes the wrapped server's
**Methods:**
-#### `wrap`
+#### `wrap`
```python
wrap(cls, server: Any, prompt: Prompt) -> FastMCPProviderPrompt
@@ -103,7 +103,7 @@ wrap(cls, server: Any, prompt: Prompt) -> FastMCPProviderPrompt
Wrap a Prompt to delegate rendering to the server's middleware.
-#### `render`
+#### `render`
```python
render(self, arguments: dict[str, Any] | None = None) -> PromptResult
@@ -115,13 +115,13 @@ This is called when the prompt is used within a transformed context
or other contexts where task_meta is not available.
-#### `get_span_attributes`
+#### `get_span_attributes`
```python
get_span_attributes(self) -> dict[str, Any]
```
-### `FastMCPProviderResourceTemplate`
+### `FastMCPProviderResourceTemplate`
Resource template that creates FastMCPProviderResources.
@@ -133,7 +133,7 @@ when read.
**Methods:**
-#### `wrap`
+#### `wrap`
```python
wrap(cls, server: Any, template: ResourceTemplate) -> FastMCPProviderResourceTemplate
@@ -142,7 +142,7 @@ wrap(cls, server: Any, template: ResourceTemplate) -> FastMCPProviderResourceTem
Wrap a ResourceTemplate to create FastMCPProviderResources.
-#### `create_resource`
+#### `create_resource`
```python
create_resource(self, uri: str, params: dict[str, Any]) -> Resource
@@ -155,7 +155,7 @@ We use `_original_uri_template` with `params` to construct the internal
URI that the nested server understands.
-#### `read`
+#### `read`
```python
read(self, arguments: dict[str, Any]) -> str | bytes | ResourceResult
@@ -167,7 +167,7 @@ Reads the resource via the wrapped server and returns the ResourceResult.
This method is called by Docket during background task execution.
-#### `register_with_docket`
+#### `register_with_docket`
```python
register_with_docket(self, docket: Docket) -> None
@@ -176,7 +176,7 @@ register_with_docket(self, docket: Docket) -> None
No-op: the child's actual template is registered via get_tasks().
-#### `add_to_docket`
+#### `add_to_docket`
```python
add_to_docket(self, docket: Docket, params: dict[str, Any], **kwargs: Any) -> Execution
@@ -188,13 +188,13 @@ The child's FunctionResourceTemplate.fn is registered (via get_tasks),
and it expects splatted **kwargs, so we splat params here.
-#### `get_span_attributes`
+#### `get_span_attributes`
```python
get_span_attributes(self) -> dict[str, Any]
```
-### `FastMCPProvider`
+### `FastMCPProvider`
Provider that wraps a FastMCP server.
@@ -210,7 +210,16 @@ This ensures middleware runs when components are executed.
**Methods:**
-#### `get_tasks`
+#### `get_app_tool`
+
+```python
+get_app_tool(self, app_name: str, tool_name: str) -> Tool | None
+```
+
+Delegate to nested server's get_app_tool, wrapping for middleware.
+
+
+#### `get_tasks`
```python
get_tasks(self) -> Sequence[FastMCPComponent]
@@ -224,7 +233,7 @@ server's transforms applied, then applies this provider's transforms
for correct registration keys.
-#### `lifespan`
+#### `lifespan`
```python
lifespan(self) -> AsyncIterator[None]
diff --git a/docs/python-sdk/fastmcp-server-providers-filesystem_discovery.mdx b/docs/python-sdk/fastmcp-server-providers-filesystem_discovery.mdx
index 060398177..2a4595b89 100644
--- a/docs/python-sdk/fastmcp-server-providers-filesystem_discovery.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-filesystem_discovery.mdx
@@ -16,7 +16,7 @@ This module provides functions to:
## Functions
-### `discover_files`
+### `discover_files`
```python
discover_files(root: Path) -> list[Path]
@@ -34,10 +34,10 @@ Excludes __init__.py files (they're for package structure, not components).
- List of .py file paths, sorted for deterministic order.
-### `import_module_from_file`
+### `import_module_from_file`
```python
-import_module_from_file(file_path: Path) -> ModuleType
+import_module_from_file(file_path: Path, provider_root: Path | None = None) -> ModuleType
```
@@ -47,8 +47,13 @@ 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.
+sys.path is modified only for the duration of the import and restored
+immediately after, so no permanent pollution occurs.
+
**Args:**
- `file_path`: Path to the Python file.
+- `provider_root`: The provider's root directory. Prevents package root
+discovery from walking above this boundary into ancestor packages.
**Returns:**
- The imported module.
@@ -57,7 +62,7 @@ imports directly using spec_from_file_location.
- `ImportError`: If the module cannot be imported.
-### `extract_components`
+### `extract_components`
```python
extract_components(module: ModuleType) -> list[FastMCPComponent]
@@ -77,7 +82,7 @@ or functions decorated with @tool/@resource/@prompt that have __fastmcp__ metada
- List of component objects (Tool, Resource, ResourceTemplate, Prompt).
-### `discover_and_import`
+### `discover_and_import`
```python
discover_and_import(root: Path) -> DiscoveryResult
@@ -97,7 +102,7 @@ This is the main entry point for filesystem-based discovery.
## Classes
-### `DiscoveryResult`
+### `DiscoveryResult`
Result of filesystem discovery.
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
index efeae3661..802adb6e0 100644
--- a/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-tools.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-local_provider-decorators-tools.mdx
@@ -14,7 +14,7 @@ registration functionality to LocalProvider.
## Classes
-### `ToolDecoratorMixin`
+### `ToolDecoratorMixin`
Mixin class providing tool decorator functionality for LocalProvider.
@@ -26,7 +26,7 @@ This mixin contains all methods related to:
**Methods:**
-#### `add_tool`
+#### `add_tool`
```python
add_tool(self: LocalProvider, tool: Tool | Callable[..., Any]) -> Tool
@@ -37,19 +37,19 @@ Add a tool to this provider's storage.
Accepts either a Tool object or a decorated function with __fastmcp__ metadata.
-#### `tool`
+#### `tool`
```python
tool(self: LocalProvider, name_or_fn: F) -> F
```
-#### `tool`
+#### `tool`
```python
tool(self: LocalProvider, name_or_fn: str | None = None) -> Callable[[F], F]
```
-#### `tool`
+#### `tool`
```python
tool(self: LocalProvider, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool | partial[Callable[[AnyFunction], FunctionTool] | FunctionTool]
diff --git a/docs/python-sdk/fastmcp-server-providers-openapi-components.mdx b/docs/python-sdk/fastmcp-server-providers-openapi-components.mdx
index 94f0b7b6a..f16ecaaeb 100644
--- a/docs/python-sdk/fastmcp-server-providers-openapi-components.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-openapi-components.mdx
@@ -10,7 +10,7 @@ OpenAPI component classes: Tool, Resource, and ResourceTemplate.
## Classes
-### `OpenAPITool`
+### `OpenAPITool`
Tool implementation for OpenAPI endpoints.
@@ -18,7 +18,7 @@ Tool implementation for OpenAPI endpoints.
**Methods:**
-#### `run`
+#### `run`
```python
run(self, arguments: dict[str, Any]) -> ToolResult
@@ -27,7 +27,7 @@ run(self, arguments: dict[str, Any]) -> ToolResult
Execute the HTTP request using RequestDirector.
-### `OpenAPIResource`
+### `OpenAPIResource`
Resource implementation for OpenAPI endpoints.
@@ -35,7 +35,7 @@ Resource implementation for OpenAPI endpoints.
**Methods:**
-#### `read`
+#### `read`
```python
read(self) -> ResourceResult
@@ -44,7 +44,7 @@ read(self) -> ResourceResult
Fetch the resource data by making an HTTP request.
-### `OpenAPIResourceTemplate`
+### `OpenAPIResourceTemplate`
Resource template implementation for OpenAPI endpoints.
@@ -52,7 +52,7 @@ Resource template implementation for OpenAPI endpoints.
**Methods:**
-#### `create_resource`
+#### `create_resource`
```python
create_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> Resource
diff --git a/docs/python-sdk/fastmcp-server-providers-proxy.mdx b/docs/python-sdk/fastmcp-server-providers-proxy.mdx
index 4c64d8566..d612a9f37 100644
--- a/docs/python-sdk/fastmcp-server-providers-proxy.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-proxy.mdx
@@ -15,7 +15,7 @@ classes that forward execution to remote servers.
## Functions
-### `default_proxy_roots_handler`
+### `default_proxy_roots_handler`
```python
default_proxy_roots_handler(context: RequestContext[ClientSession, LifespanContextT]) -> RootsList
@@ -25,7 +25,7 @@ default_proxy_roots_handler(context: RequestContext[ClientSession, LifespanConte
Forward list roots request from remote server to proxy's connected clients.
-### `default_proxy_sampling_handler`
+### `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
@@ -35,7 +35,7 @@ default_proxy_sampling_handler(messages: list[mcp.types.SamplingMessage], params
Forward sampling request from remote server to proxy's connected clients.
-### `default_proxy_elicitation_handler`
+### `default_proxy_elicitation_handler`
```python
default_proxy_elicitation_handler(message: str, response_type: type, params: mcp.types.ElicitRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> ElicitResult
@@ -45,7 +45,7 @@ default_proxy_elicitation_handler(message: str, response_type: type, params: mcp
Forward elicitation request from remote server to proxy's connected clients.
-### `default_proxy_log_handler`
+### `default_proxy_log_handler`
```python
default_proxy_log_handler(message: LogMessage) -> None
@@ -55,7 +55,7 @@ default_proxy_log_handler(message: LogMessage) -> None
Forward log notification from remote server to proxy's connected clients.
-### `default_proxy_progress_handler`
+### `default_proxy_progress_handler`
```python
default_proxy_progress_handler(progress: float, total: float | None, message: str | None) -> None
@@ -67,7 +67,7 @@ Forward progress notification from remote server to proxy's connected clients.
## Classes
-### `ProxyTool`
+### `ProxyTool`
A Tool that represents and executes a tool on a remote server.
@@ -75,7 +75,7 @@ A Tool that represents and executes a tool on a remote server.
**Methods:**
-#### `model_copy`
+#### `model_copy`
```python
model_copy(self, **kwargs: Any) -> ProxyTool
@@ -84,7 +84,7 @@ model_copy(self, **kwargs: Any) -> ProxyTool
Override to preserve _backend_name when name changes.
-#### `from_mcp_tool`
+#### `from_mcp_tool`
```python
from_mcp_tool(cls, client_factory: ClientFactoryT, mcp_tool: mcp.types.Tool) -> ProxyTool
@@ -93,7 +93,7 @@ from_mcp_tool(cls, client_factory: ClientFactoryT, mcp_tool: mcp.types.Tool) ->
Factory method to create a ProxyTool from a raw MCP tool schema.
-#### `run`
+#### `run`
```python
run(self, arguments: dict[str, Any], context: Context | None = None) -> ToolResult
@@ -102,13 +102,13 @@ run(self, arguments: dict[str, Any], context: Context | None = None) -> ToolResu
Executes the tool by making a call through the client.
-#### `get_span_attributes`
+#### `get_span_attributes`
```python
get_span_attributes(self) -> dict[str, Any]
```
-### `ProxyResource`
+### `ProxyResource`
A Resource that represents and reads a resource from a remote server.
@@ -116,7 +116,7 @@ A Resource that represents and reads a resource from a remote server.
**Methods:**
-#### `model_copy`
+#### `model_copy`
```python
model_copy(self, **kwargs: Any) -> ProxyResource
@@ -125,7 +125,7 @@ model_copy(self, **kwargs: Any) -> ProxyResource
Override to preserve _backend_uri when uri changes.
-#### `from_mcp_resource`
+#### `from_mcp_resource`
```python
from_mcp_resource(cls, client_factory: ClientFactoryT, mcp_resource: mcp.types.Resource) -> ProxyResource
@@ -134,7 +134,7 @@ from_mcp_resource(cls, client_factory: ClientFactoryT, mcp_resource: mcp.types.R
Factory method to create a ProxyResource from a raw MCP resource schema.
-#### `read`
+#### `read`
```python
read(self) -> ResourceResult
@@ -143,13 +143,13 @@ read(self) -> ResourceResult
Read the resource content from the remote server.
-#### `get_span_attributes`
+#### `get_span_attributes`
```python
get_span_attributes(self) -> dict[str, Any]
```
-### `ProxyTemplate`
+### `ProxyTemplate`
A ResourceTemplate that represents and creates resources from a remote server template.
@@ -157,7 +157,7 @@ A ResourceTemplate that represents and creates resources from a remote server te
**Methods:**
-#### `model_copy`
+#### `model_copy`
```python
model_copy(self, **kwargs: Any) -> ProxyTemplate
@@ -166,7 +166,7 @@ model_copy(self, **kwargs: Any) -> ProxyTemplate
Override to preserve _backend_uri_template when uri_template changes.
-#### `from_mcp_template`
+#### `from_mcp_template`
```python
from_mcp_template(cls, client_factory: ClientFactoryT, mcp_template: mcp.types.ResourceTemplate) -> ProxyTemplate
@@ -175,7 +175,7 @@ from_mcp_template(cls, client_factory: ClientFactoryT, mcp_template: mcp.types.R
Factory method to create a ProxyTemplate from a raw MCP template schema.
-#### `create_resource`
+#### `create_resource`
```python
create_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> ProxyResource
@@ -184,13 +184,13 @@ create_resource(self, uri: str, params: dict[str, Any], context: Context | None
Create a resource from the template by calling the remote server.
-#### `get_span_attributes`
+#### `get_span_attributes`
```python
get_span_attributes(self) -> dict[str, Any]
```
-### `ProxyPrompt`
+### `ProxyPrompt`
A Prompt that represents and renders a prompt from a remote server.
@@ -198,7 +198,7 @@ A Prompt that represents and renders a prompt from a remote server.
**Methods:**
-#### `model_copy`
+#### `model_copy`
```python
model_copy(self, **kwargs: Any) -> ProxyPrompt
@@ -207,7 +207,7 @@ model_copy(self, **kwargs: Any) -> ProxyPrompt
Override to preserve _backend_name when name changes.
-#### `from_mcp_prompt`
+#### `from_mcp_prompt`
```python
from_mcp_prompt(cls, client_factory: ClientFactoryT, mcp_prompt: mcp.types.Prompt) -> ProxyPrompt
@@ -216,7 +216,7 @@ from_mcp_prompt(cls, client_factory: ClientFactoryT, mcp_prompt: mcp.types.Promp
Factory method to create a ProxyPrompt from a raw MCP prompt schema.
-#### `render`
+#### `render`
```python
render(self, arguments: dict[str, Any]) -> PromptResult
@@ -225,13 +225,13 @@ render(self, arguments: dict[str, Any]) -> PromptResult
Render the prompt by making a call through the client.
-#### `get_span_attributes`
+#### `get_span_attributes`
```python
get_span_attributes(self) -> dict[str, Any]
```
-### `ProxyProvider`
+### `ProxyProvider`
Provider that proxies to a remote MCP server via a client factory.
@@ -242,10 +242,20 @@ 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.
+Component lists (tools, resources, templates, prompts) are cached so that
+individual lookups (e.g. during ``call_tool``) can resolve from the cache
+instead of opening a new backend connection. The cache stores the
+backend's raw component metadata and is shared across all sessions;
+per-session visibility and auth filtering are applied after cache lookup
+by the server layer. The cache is refreshed whenever a ``list_*`` call
+is made, and entries expire after ``cache_ttl`` seconds (default 300).
+Set ``cache_ttl=0`` to disable caching. Disabling is recommended for
+backends whose component lists change dynamically.
+
**Methods:**
-#### `get_tasks`
+#### `get_tasks`
```python
get_tasks(self) -> Sequence[FastMCPComponent]
@@ -258,7 +268,7 @@ server lifespan initialization, which would open the client before any
context is set. All Proxy* components have task_config.mode="forbidden".
-### `FastMCPProxy`
+### `FastMCPProxy`
A FastMCP server that acts as a proxy to a remote MCP-compliant server.
@@ -267,7 +277,7 @@ This is a convenience wrapper that creates a FastMCP server with a
ProxyProvider. For more control, use FastMCP with add_provider(ProxyProvider(...)).
-### `ProxyClient`
+### `ProxyClient`
A proxy client that forwards advanced interactions between a remote MCP server and the proxy's connected clients.
@@ -275,7 +285,7 @@ A proxy client that forwards advanced interactions between a remote MCP server a
Supports forwarding roots, sampling, elicitation, logging, and progress.
-### `StatefulProxyClient`
+### `StatefulProxyClient`
A proxy client that provides a stateful client factory for the proxy server.
@@ -296,7 +306,7 @@ it to detect (and correct) staleness.
**Methods:**
-#### `clear`
+#### `clear`
```python
clear(self)
@@ -305,7 +315,7 @@ clear(self)
Clear all cached clients and force disconnect them.
-#### `new_stateful`
+#### `new_stateful`
```python
new_stateful(self) -> Client[ClientTransportT]
diff --git a/docs/python-sdk/fastmcp-server-sampling-run.mdx b/docs/python-sdk/fastmcp-server-sampling-run.mdx
index c09ad42d5..ea6d46104 100644
--- a/docs/python-sdk/fastmcp-server-sampling-run.mdx
+++ b/docs/python-sdk/fastmcp-server-sampling-run.mdx
@@ -44,7 +44,7 @@ sampling_handler is set via determine_handler_mode(). The checks below are
safeguards against internal misuse.
-### `execute_tools`
+### `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]
@@ -71,7 +71,7 @@ regardless of this setting.
- List of tool result content blocks in the same order as tool_calls.
-### `prepare_messages`
+### `prepare_messages`
```python
prepare_messages(messages: str | Sequence[str | SamplingMessage]) -> list[SamplingMessage]
@@ -81,7 +81,7 @@ prepare_messages(messages: str | Sequence[str | SamplingMessage]) -> list[Sampli
Convert various message formats to a list of SamplingMessage objects.
-### `prepare_tools`
+### `prepare_tools`
```python
prepare_tools(tools: Sequence[SamplingTool | FunctionTool | TransformedTool | Callable[..., Any]] | None) -> list[SamplingTool] | None
@@ -102,7 +102,7 @@ TransformedTool, or plain callable functions.
- List of SamplingTool instances, or None if tools is None.
-### `extract_tool_calls`
+### `extract_tool_calls`
```python
extract_tool_calls(response: CreateMessageResult | CreateMessageResultWithTools) -> list[ToolUseContent]
@@ -112,7 +112,7 @@ extract_tool_calls(response: CreateMessageResult | CreateMessageResultWithTools)
Extract tool calls from a response.
-### `create_final_response_tool`
+### `create_final_response_tool`
```python
create_final_response_tool(result_type: type) -> SamplingTool
@@ -125,7 +125,7 @@ This tool is used to capture structured responses from the LLM.
The tool's schema is derived from the result_type.
-### `sample_step_impl`
+### `sample_step_impl`
```python
sample_step_impl(context: Context, messages: str | Sequence[str | SamplingMessage]) -> SampleStep
@@ -138,7 +138,7 @@ 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`
+### `sample_impl`
```python
sample_impl(context: Context, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT]
diff --git a/docs/python-sdk/fastmcp-server-sampling-sampling_tool.mdx b/docs/python-sdk/fastmcp-server-sampling-sampling_tool.mdx
index cac91d36e..2a3590003 100644
--- a/docs/python-sdk/fastmcp-server-sampling-sampling_tool.mdx
+++ b/docs/python-sdk/fastmcp-server-sampling-sampling_tool.mdx
@@ -10,7 +10,7 @@ SamplingTool for use during LLM sampling requests.
## Classes
-### `SamplingTool`
+### `SamplingTool`
A tool that can be used during LLM sampling.
@@ -37,7 +37,7 @@ Create a SamplingTool explicitly when you need custom name/description:
**Methods:**
-#### `run`
+#### `run`
```python
run(self, arguments: dict[str, Any] | None = None) -> Any
@@ -52,7 +52,7 @@ Execute the tool with the given arguments.
- The result of executing the tool function.
-#### `from_function`
+#### `from_function`
```python
from_function(cls, fn: Callable[..., Any]) -> SamplingTool
@@ -79,7 +79,7 @@ concurrently. Defaults to False.
- `ValueError`: If the function is a lambda without a name override.
-#### `from_callable_tool`
+#### `from_callable_tool`
```python
from_callable_tool(cls, tool: FunctionTool | TransformedTool) -> SamplingTool
diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx
index dbd0ad15a..14fcf04ca 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,7 +26,7 @@ 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
@@ -54,53 +54,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]
```
-#### `local_provider`
+#### `local_provider`
```python
local_provider(self) -> LocalProvider
@@ -115,13 +115,13 @@ 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_provider`
```python
add_provider(self, provider: Provider) -> None
@@ -141,7 +141,7 @@ always take precedence over providers.
- Prompts become "namespace_promptname"
-#### `get_tasks`
+#### `get_tasks`
```python
get_tasks(self) -> Sequence[FastMCPComponent]
@@ -153,7 +153,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,7 +168,7 @@ They transform tools, resources, and prompts from ALL providers.
- `transform`: The transform to add.
-#### `add_tool_transformation`
+#### `add_tool_transformation`
```python
add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfig) -> None
@@ -180,7 +180,7 @@ Add a tool transformation.
Use ``add_transform(ToolTransform({...}))`` instead.
-#### `remove_tool_transformation`
+#### `remove_tool_transformation`
```python
remove_tool_transformation(self, _tool_name: str) -> None
@@ -192,7 +192,7 @@ Remove a tool transformation.
Tool transformations are now immutable. Use enable/disable controls instead.
-#### `list_tools`
+#### `list_tools`
```python
list_tools(self) -> Sequence[Tool]
@@ -205,7 +205,7 @@ 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
@@ -217,6 +217,9 @@ Overrides Provider.get_tool() to add visibility filtering 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 +228,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 +241,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 +252,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 +263,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 +276,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 +287,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 +298,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 +311,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 +322,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,19 +333,19 @@ 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`
+#### `call_tool`
```python
call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult
```
-#### `call_tool`
+#### `call_tool`
```python
call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> ToolResult | mcp.types.CreateTaskResult
@@ -363,19 +375,19 @@ return ToolResult.
- `ValidationError`: If arguments fail validation
-#### `read_resource`
+#### `read_resource`
```python
read_resource(self, uri: str) -> ResourceResult
```
-#### `read_resource`
+#### `read_resource`
```python
read_resource(self, uri: str) -> mcp.types.CreateTaskResult
```
-#### `read_resource`
+#### `read_resource`
```python
read_resource(self, uri: str) -> ResourceResult | mcp.types.CreateTaskResult
@@ -404,19 +416,19 @@ return ResourceResult.
- `ResourceError`: If resource read fails
-#### `render_prompt`
+#### `render_prompt`
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult
```
-#### `render_prompt`
+#### `render_prompt`
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> mcp.types.CreateTaskResult
```
-#### `render_prompt`
+#### `render_prompt`
```python
render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> PromptResult | mcp.types.CreateTaskResult
@@ -446,7 +458,7 @@ return PromptResult.
- `PromptError`: If prompt rendering fails
-#### `add_tool`
+#### `add_tool`
```python
add_tool(self, tool: Tool | Callable[..., Any]) -> Tool
@@ -464,7 +476,7 @@ with the Context type annotation. See the @tool decorator for examples.
- The tool instance that was added to the server.
-#### `remove_tool`
+#### `remove_tool`
```python
remove_tool(self, name: str, version: str | None = None) -> None
@@ -483,19 +495,19 @@ Remove tool(s) from the server.
- `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]
@@ -551,7 +563,7 @@ server.tool(my_function, name="custom_name")
```
-#### `add_resource`
+#### `add_resource`
```python
add_resource(self, resource: Resource | Callable[..., Any]) -> Resource | ResourceTemplate
@@ -566,7 +578,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 +593,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 +652,7 @@ async def get_weather(city: str) -> str:
```
-#### `add_prompt`
+#### `add_prompt`
```python
add_prompt(self, prompt: Prompt | Callable[..., Any]) -> Prompt
@@ -655,19 +667,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,7 +756,7 @@ Decorator to register a prompt.
```
-#### `mount`
+#### `mount`
```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
@@ -791,7 +803,7 @@ mounted server.
- `prefix`: Deprecated. Use namespace instead.
-#### `import_server`
+#### `import_server`
```python
import_server(self, server: FastMCP[LifespanResultT], prefix: str | None = None) -> None
@@ -832,7 +844,7 @@ templates, and prompts are imported with their original names.
objects are imported with their original names.
-#### `from_openapi`
+#### `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
@@ -861,7 +873,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
@@ -885,7 +897,7 @@ Use this to configure timeout and other client settings.
- A FastMCP server with an OpenAPIProvider attached.
-#### `as_proxy`
+#### `as_proxy`
```python
as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy
@@ -903,7 +915,7 @@ instance or any value accepted as the `transport` argument of
`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-tasks-config.mdx b/docs/python-sdk/fastmcp-server-tasks-config.mdx
index 0dc2bf4ba..a014e1ac4 100644
--- a/docs/python-sdk/fastmcp-server-tasks-config.mdx
+++ b/docs/python-sdk/fastmcp-server-tasks-config.mdx
@@ -14,7 +14,7 @@ handle task-augmented execution as specified in SEP-1686.
## Classes
-### `TaskMeta`
+### `TaskMeta`
Metadata for task-augmented execution requests.
@@ -27,7 +27,7 @@ the operation should be submitted as a background task.
- `fn_key`: Docket routing key. Auto-derived from component name if None.
-### `TaskConfig`
+### `TaskConfig`
Configuration for MCP background task execution (SEP-1686).
@@ -44,7 +44,7 @@ Controls how a component handles task-augmented requests:
**Methods:**
-#### `from_bool`
+#### `from_bool`
```python
from_bool(cls, value: bool) -> TaskConfig
@@ -59,7 +59,7 @@ Convert boolean task flag to TaskConfig.
- TaskConfig with appropriate mode.
-#### `supports_tasks`
+#### `supports_tasks`
```python
supports_tasks(self) -> bool
@@ -71,7 +71,7 @@ Check if this component supports task execution.
- True if mode is "optional" or "required", False if "forbidden".
-#### `validate_function`
+#### `validate_function`
```python
validate_function(self, fn: Callable[..., Any], name: str) -> None
diff --git a/docs/python-sdk/fastmcp-server-tasks-handlers.mdx b/docs/python-sdk/fastmcp-server-tasks-handlers.mdx
index 31f228f14..e7b1ed35e 100644
--- a/docs/python-sdk/fastmcp-server-tasks-handlers.mdx
+++ b/docs/python-sdk/fastmcp-server-tasks-handlers.mdx
@@ -13,7 +13,7 @@ Handles queuing tool/prompt/resource executions to Docket as background tasks.
## Functions
-### `submit_to_docket`
+### `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
diff --git a/docs/python-sdk/fastmcp-server-tasks-requests.mdx b/docs/python-sdk/fastmcp-server-tasks-requests.mdx
index 5cde802fc..a8b31a13d 100644
--- a/docs/python-sdk/fastmcp-server-tasks-requests.mdx
+++ b/docs/python-sdk/fastmcp-server-tasks-requests.mdx
@@ -52,7 +52,7 @@ Converts raw task return values to MCP types based on task type.
- MCP result (CallToolResult, GetPromptResult, or ReadResourceResult)
-### `tasks_list_handler`
+### `tasks_list_handler`
```python
tasks_list_handler(server: FastMCP, params: dict[str, Any]) -> ListTasksResult
@@ -71,7 +71,7 @@ Note: With client-side tracking, this returns minimal info.
- Response with tasks list and pagination
-### `tasks_cancel_handler`
+### `tasks_cancel_handler`
```python
tasks_cancel_handler(server: FastMCP, params: dict[str, Any]) -> CancelTaskResult
diff --git a/docs/python-sdk/fastmcp-server-transforms-catalog.mdx b/docs/python-sdk/fastmcp-server-transforms-catalog.mdx
index 1dd2cfe4a..5728d65b1 100644
--- a/docs/python-sdk/fastmcp-server-transforms-catalog.mdx
+++ b/docs/python-sdk/fastmcp-server-transforms-catalog.mdx
@@ -52,7 +52,7 @@ Usage::
## Classes
-### `CatalogTransform`
+### `CatalogTransform`
Transform that needs access to the real component catalog.
@@ -70,31 +70,31 @@ by temporarily setting a bypass flag so that this transform's
**Methods:**
-#### `list_tools`
+#### `list_tools`
```python
list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
```
-#### `list_resources`
+#### `list_resources`
```python
list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]
```
-#### `list_resource_templates`
+#### `list_resource_templates`
```python
list_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]
```
-#### `list_prompts`
+#### `list_prompts`
```python
list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]
```
-#### `transform_tools`
+#### `transform_tools`
```python
transform_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
@@ -110,7 +110,7 @@ to handle re-entrant bypass when ``get_tool_catalog()`` reads the
real catalog.
-#### `transform_resources`
+#### `transform_resources`
```python
transform_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]
@@ -126,7 +126,7 @@ to handle re-entrant bypass when ``get_resource_catalog()`` reads the
real catalog.
-#### `transform_resource_templates`
+#### `transform_resource_templates`
```python
transform_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate]
@@ -142,7 +142,7 @@ uses it to handle re-entrant bypass when
``get_resource_template_catalog()`` reads the real catalog.
-#### `transform_prompts`
+#### `transform_prompts`
```python
transform_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]
@@ -158,7 +158,7 @@ to handle re-entrant bypass when ``get_prompt_catalog()`` reads the
real catalog.
-#### `get_tool_catalog`
+#### `get_tool_catalog`
```python
get_tool_catalog(self, ctx: Context) -> Sequence[Tool]
@@ -166,6 +166,10 @@ get_tool_catalog(self, ctx: Context) -> Sequence[Tool]
Fetch the real tool catalog, bypassing this transform.
+The result is deduplicated by name so that only the highest version
+of each tool is returned — matching what protocol handlers expose
+on the wire.
+
**Args:**
- `ctx`: The current request context.
- `run_middleware`: Whether to run middleware on the inner call.
@@ -173,7 +177,7 @@ Defaults to True because this is typically called from a
tool handler where list_tools middleware has not yet run.
-#### `get_resource_catalog`
+#### `get_resource_catalog`
```python
get_resource_catalog(self, ctx: Context) -> Sequence[Resource]
@@ -188,7 +192,7 @@ Defaults to True because this is typically called from a
tool handler where list_resources middleware has not yet run.
-#### `get_prompt_catalog`
+#### `get_prompt_catalog`
```python
get_prompt_catalog(self, ctx: Context) -> Sequence[Prompt]
@@ -203,7 +207,7 @@ Defaults to True because this is typically called from a
tool handler where list_prompts middleware has not yet run.
-#### `get_resource_template_catalog`
+#### `get_resource_template_catalog`
```python
get_resource_template_catalog(self, ctx: Context) -> Sequence[ResourceTemplate]
diff --git a/docs/python-sdk/fastmcp-server-transforms-prompts_as_tools.mdx b/docs/python-sdk/fastmcp-server-transforms-prompts_as_tools.mdx
index f1656c263..dc3b659d0 100644
--- a/docs/python-sdk/fastmcp-server-transforms-prompts_as_tools.mdx
+++ b/docs/python-sdk/fastmcp-server-transforms-prompts_as_tools.mdx
@@ -11,6 +11,10 @@ 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.
+The generated tools route through `ctx.fastmcp` at runtime, so all server
+middleware (auth, visibility, rate limiting, etc.) applies to prompt
+operations exactly as it would for direct `prompts/get` calls.
+
Example:
```python
from fastmcp import FastMCP
@@ -24,23 +28,26 @@ Example:
## Classes
-### `PromptsAsTools`
+### `PromptsAsTools`
Transform that adds tools for listing and getting prompts.
Generates two tools:
-- `list_prompts`: Lists all prompts from the provider
+- `list_prompts`: Lists all prompts
- `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.
+The generated tools route through the server at runtime, so auth,
+middleware, and visibility apply automatically.
+
+This transform should be applied to a FastMCP server instance, not
+a raw Provider, because the generated tools need the server's
+middleware chain for auth and visibility filtering.
**Methods:**
-#### `list_tools`
+#### `list_tools`
```python
list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
@@ -49,7 +56,7 @@ list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
Add prompt tools to the tool list.
-#### `get_tool`
+#### `get_tool`
```python
get_tool(self, name: str, call_next: GetToolNext) -> Tool | None
diff --git a/docs/python-sdk/fastmcp-server-transforms-resources_as_tools.mdx b/docs/python-sdk/fastmcp-server-transforms-resources_as_tools.mdx
index 3b46e875c..50f0a0c95 100644
--- a/docs/python-sdk/fastmcp-server-transforms-resources_as_tools.mdx
+++ b/docs/python-sdk/fastmcp-server-transforms-resources_as_tools.mdx
@@ -11,6 +11,10 @@ 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.
+The generated tools route through `ctx.fastmcp` at runtime, so all server
+middleware (auth, visibility, rate limiting, etc.) applies to resource
+operations exactly as it would for direct `resources/read` calls.
+
Example:
```python
from fastmcp import FastMCP
@@ -24,23 +28,26 @@ Example:
## Classes
-### `ResourcesAsTools`
+### `ResourcesAsTools`
Transform that adds tools for listing and reading resources.
Generates two tools:
-- `list_resources`: Lists all resources and templates from the provider
+- `list_resources`: Lists all resources and templates
- `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.
+The generated tools route through the server at runtime, so auth,
+middleware, and visibility apply automatically.
+
+This transform should be applied to a FastMCP server instance, not
+a raw Provider, because the generated tools need the server's
+middleware chain for auth and visibility filtering.
**Methods:**
-#### `list_tools`
+#### `list_tools`
```python
list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
@@ -49,7 +56,7 @@ list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]
Add resource tools to the tool list.
-#### `get_tool`
+#### `get_tool`
```python
get_tool(self, name: str, call_next: GetToolNext) -> Tool | None
diff --git a/docs/python-sdk/fastmcp-settings.mdx b/docs/python-sdk/fastmcp-settings.mdx
index eeb1156f8..f9b0cf26a 100644
--- a/docs/python-sdk/fastmcp-settings.mdx
+++ b/docs/python-sdk/fastmcp-settings.mdx
@@ -7,13 +7,13 @@ sidebarTitle: settings
## Classes
-### `DocketSettings`
+### `DocketSettings`
Docket worker configuration.
-### `Settings`
+### `Settings`
FastMCP settings.
@@ -21,7 +21,7 @@ FastMCP settings.
**Methods:**
-#### `get_setting`
+#### `get_setting`
```python
get_setting(self, attr: str) -> Any
@@ -31,7 +31,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 +41,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-tools-tool.mdx b/docs/python-sdk/fastmcp-tools-base.mdx
similarity index 80%
rename from docs/python-sdk/fastmcp-tools-tool.mdx
rename to docs/python-sdk/fastmcp-tools-base.mdx
index 0394bf4a5..4f8d362ea 100644
--- a/docs/python-sdk/fastmcp-tools-tool.mdx
+++ b/docs/python-sdk/fastmcp-tools-base.mdx
@@ -1,13 +1,13 @@
---
-title: tool
-sidebarTitle: tool
+title: base
+sidebarTitle: base
---
-# `fastmcp.tools.tool`
+# `fastmcp.tools.base`
## Functions
-### `default_serializer`
+### `default_serializer`
```python
default_serializer(data: Any) -> str
@@ -15,17 +15,17 @@ default_serializer(data: Any) -> str
## Classes
-### `ToolResult`
+### `ToolResult`
**Methods:**
-#### `to_mcp_result`
+#### `to_mcp_result`
```python
to_mcp_result(self) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]] | CallToolResult
```
-### `Tool`
+### `Tool`
Internal tool registration info.
@@ -33,7 +33,7 @@ Internal tool registration info.
**Methods:**
-#### `to_mcp_tool`
+#### `to_mcp_tool`
```python
to_mcp_tool(self, **overrides: Any) -> MCPTool
@@ -42,7 +42,7 @@ to_mcp_tool(self, **overrides: Any) -> MCPTool
Convert the FastMCP tool to an MCP tool.
-#### `from_function`
+#### `from_function`
```python
from_function(cls, fn: Callable[..., Any]) -> FunctionTool
@@ -51,7 +51,7 @@ from_function(cls, fn: Callable[..., Any]) -> FunctionTool
Create a Tool from a function.
-#### `run`
+#### `run`
```python
run(self, arguments: dict[str, Any]) -> ToolResult
@@ -66,7 +66,7 @@ implemented by subclasses.
(list of ContentBlocks, dict of structured output).
-#### `convert_result`
+#### `convert_result`
```python
convert_result(self, raw_value: Any) -> ToolResult
@@ -78,7 +78,7 @@ Handles ToolResult passthrough and converts raw values using the tool's
attributes (serializer, output_schema) for proper conversion.
-#### `register_with_docket`
+#### `register_with_docket`
```python
register_with_docket(self, docket: Docket) -> None
@@ -87,7 +87,7 @@ register_with_docket(self, docket: Docket) -> None
Register this tool with docket for background execution.
-#### `add_to_docket`
+#### `add_to_docket`
```python
add_to_docket(self, docket: Docket, arguments: dict[str, Any], **kwargs: Any) -> Execution
@@ -103,13 +103,13 @@ Schedule this tool for background execution via docket.
- `**kwargs`: Additional kwargs passed to docket.add()
-#### `from_tool`
+#### `from_tool`
```python
from_tool(cls, tool: Tool | Callable[..., Any]) -> TransformedTool
```
-#### `get_span_attributes`
+#### `get_span_attributes`
```python
get_span_attributes(self) -> dict[str, Any]
diff --git a/docs/python-sdk/fastmcp-tools-function_parsing.mdx b/docs/python-sdk/fastmcp-tools-function_parsing.mdx
index f9cd7f28e..6264ef5d3 100644
--- a/docs/python-sdk/fastmcp-tools-function_parsing.mdx
+++ b/docs/python-sdk/fastmcp-tools-function_parsing.mdx
@@ -10,11 +10,11 @@ Function introspection and schema generation for FastMCP tools.
## Classes
-### `ParsedFunction`
+### `ParsedFunction`
**Methods:**
-#### `from_function`
+#### `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
index d25c6ecf8..d18d659f8 100644
--- a/docs/python-sdk/fastmcp-tools-function_tool.mdx
+++ b/docs/python-sdk/fastmcp-tools-function_tool.mdx
@@ -10,7 +10,7 @@ Standalone @tool decorator for FastMCP.
## Functions
-### `tool`
+### `tool`
```python
tool(name_or_fn: str | Callable[..., Any] | None = None) -> Any
@@ -25,34 +25,23 @@ using mcp.add_tool().
## Classes
-### `DecoratedTool`
+### `DecoratedTool`
Protocol for functions decorated with @tool.
-### `ToolMeta`
+### `ToolMeta`
Metadata attached to functions by the @tool decorator.
-### `FunctionTool`
+### `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`
+#### `from_function`
```python
from_function(cls, fn: Callable[..., Any]) -> FunctionTool
@@ -68,7 +57,7 @@ individual parameters must not be passed.
Cannot be used together with metadata parameter.
-#### `run`
+#### `run`
```python
run(self, arguments: dict[str, Any]) -> ToolResult
@@ -77,7 +66,7 @@ run(self, arguments: dict[str, Any]) -> ToolResult
Run the tool with arguments.
-#### `register_with_docket`
+#### `register_with_docket`
```python
register_with_docket(self, docket: Docket) -> None
@@ -86,10 +75,12 @@ 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.
+Depends parameters for docket to resolve. The function is wrapped to
+eagerly restore HTTP headers from Redis so that get_http_request()
+works even without explicit dependency injection.
-#### `add_to_docket`
+#### `add_to_docket`
```python
add_to_docket(self, docket: Docket, arguments: dict[str, Any], **kwargs: Any) -> Execution
diff --git a/docs/python-sdk/fastmcp-tools-tool_transform.mdx b/docs/python-sdk/fastmcp-tools-tool_transform.mdx
index 84a1b66cf..24f6270aa 100644
--- a/docs/python-sdk/fastmcp-tools-tool_transform.mdx
+++ b/docs/python-sdk/fastmcp-tools-tool_transform.mdx
@@ -7,7 +7,7 @@ sidebarTitle: tool_transform
## Functions
-### `forward`
+### `forward`
```python
forward(**kwargs: Any) -> ToolResult
@@ -36,7 +36,7 @@ tool has args `a` and `b`, and an `transform_args` was provided that maps `x` to
- `TypeError`: If provided arguments don't match the transformed schema.
-### `forward_raw`
+### `forward_raw`
```python
forward_raw(**kwargs: Any) -> ToolResult
@@ -62,7 +62,7 @@ y=2)` will call the parent tool with `x=1` and `y=2`.
- `RuntimeError`: If called outside a transformed tool context.
-### `apply_transformations_to_tools`
+### `apply_transformations_to_tools`
```python
apply_transformations_to_tools(tools: dict[str, Tool], transformations: dict[str, ToolTransformConfig]) -> dict[str, Tool]
@@ -78,7 +78,7 @@ but transformations are keyed by tool name (e.g., "my_tool").
## Classes
-### `ArgTransform`
+### `ArgTransform`
Configuration for transforming a parent tool's argument.
@@ -150,7 +150,7 @@ ArgTransform(name="new_name", description="New desc", default=None, type=int)
```
-### `ArgTransformConfig`
+### `ArgTransformConfig`
A model for requesting a single argument transform.
@@ -158,7 +158,7 @@ A model for requesting a single argument transform.
**Methods:**
-#### `to_arg_transform`
+#### `to_arg_transform`
```python
to_arg_transform(self) -> ArgTransform
@@ -167,7 +167,7 @@ to_arg_transform(self) -> ArgTransform
Convert the argument transform to a FastMCP argument transform.
-### `TransformedTool`
+### `TransformedTool`
A tool that is transformed from another tool.
@@ -191,7 +191,7 @@ validation when forward() is called from custom functions.
**Methods:**
-#### `run`
+#### `run`
```python
run(self, arguments: dict[str, Any]) -> ToolResult
@@ -210,7 +210,7 @@ functions.
- ToolResult object containing content and optional structured output.
-#### `from_tool`
+#### `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
@@ -293,7 +293,7 @@ async def custom_output(**kwargs) -> ToolResult:
```
-### `ToolTransformConfig`
+### `ToolTransformConfig`
Provides a way to transform a tool.
@@ -301,7 +301,7 @@ Provides a way to transform a tool.
**Methods:**
-#### `apply`
+#### `apply`
```python
apply(self, tool: Tool) -> TransformedTool
diff --git a/docs/python-sdk/fastmcp-types.mdx b/docs/python-sdk/fastmcp-types.mdx
new file mode 100644
index 000000000..7aa854d47
--- /dev/null
+++ b/docs/python-sdk/fastmcp-types.mdx
@@ -0,0 +1,25 @@
+---
+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::
+
+ 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-async_utils.mdx b/docs/python-sdk/fastmcp-utilities-async_utils.mdx
index cfd0cd7ab..75c6edd47 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,7 +37,7 @@ 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]
diff --git a/docs/python-sdk/fastmcp-utilities-components.mdx b/docs/python-sdk/fastmcp-utilities-components.mdx
index eba2517c2..e45129a28 100644
--- a/docs/python-sdk/fastmcp-utilities-components.mdx
+++ b/docs/python-sdk/fastmcp-utilities-components.mdx
@@ -24,7 +24,7 @@ namespace for compatibility with older FastMCP servers.
### `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
@@ -65,7 +65,7 @@ Subclasses should override this to use their specific identifier.
Base implementation uses name.
-#### `get_meta`
+#### `get_meta`
```python
get_meta(self) -> dict[str, Any]
@@ -80,7 +80,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 +89,7 @@ enable(self) -> None
Removed in 3.0. Use server.enable(keys=[...]) instead.
-#### `disable`
+#### `disable`
```python
disable(self) -> None
@@ -98,7 +98,7 @@ disable(self) -> None
Removed in 3.0. Use server.disable(keys=[...]) instead.
-#### `copy`
+#### `copy`
```python
copy(self) -> Self
@@ -107,7 +107,7 @@ copy(self) -> Self
Create a copy of the component.
-#### `register_with_docket`
+#### `register_with_docket`
```python
register_with_docket(self, docket: Docket) -> None
@@ -119,7 +119,7 @@ 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`
+#### `add_to_docket`
```python
add_to_docket(self, docket: Docket, *args: Any, **kwargs: Any) -> Execution
@@ -136,7 +136,7 @@ Subclasses override this to handle their specific calling conventions:
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-json_schema.mdx b/docs/python-sdk/fastmcp-utilities-json_schema.mdx
index e08643cb9..86ca44f9e 100644
--- a/docs/python-sdk/fastmcp-utilities-json_schema.mdx
+++ b/docs/python-sdk/fastmcp-utilities-json_schema.mdx
@@ -7,7 +7,7 @@ sidebarTitle: json_schema
## Functions
-### `dereference_refs`
+### `dereference_refs`
```python
dereference_refs(schema: dict[str, Any]) -> dict[str, Any]
@@ -27,6 +27,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 +40,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 +62,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-mcp_server_config-v1-sources-filesystem.mdx b/docs/python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem.mdx
index 3d791f9cd..15bd86966 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..b823d447b
--- /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
index 0d61900c6..1d0687637 100644
--- a/docs/python-sdk/fastmcp-utilities-openapi-director.mdx
+++ b/docs/python-sdk/fastmcp-utilities-openapi-director.mdx
@@ -10,7 +10,7 @@ Request director using openapi-core for stateless HTTP request building.
## Classes
-### `RequestDirector`
+### `RequestDirector`
Builds httpx.Request objects from HTTPRoute and arguments using openapi-core.
@@ -18,7 +18,7 @@ Builds httpx.Request objects from HTTPRoute and arguments using openapi-core.
**Methods:**
-#### `build`
+#### `build`
```python
build(self, route: HTTPRoute, flat_args: dict[str, Any], base_url: str = 'http://localhost') -> httpx.Request
diff --git a/docs/python-sdk/fastmcp-utilities-skills.mdx b/docs/python-sdk/fastmcp-utilities-skills.mdx
index ccb0c83b8..807d07782 100644
--- a/docs/python-sdk/fastmcp-utilities-skills.mdx
+++ b/docs/python-sdk/fastmcp-utilities-skills.mdx
@@ -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]
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..af0a890f8
--- /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..562b6d961 100644
--- a/docs/python-sdk/fastmcp-utilities-types.mdx
+++ b/docs/python-sdk/fastmcp-utilities-types.mdx
@@ -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])
@@ -112,7 +112,7 @@ list[list[str]]
Base model for FastMCP models.
-### `Image`
+### `Image`
Helper class for returning images from tools.
@@ -120,7 +120,7 @@ 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
@@ -129,7 +129,7 @@ to_image_content(self, mime_type: str | None = None, annotations: Annotations |
Convert to MCP ImageContent.
-#### `to_data_uri`
+#### `to_data_uri`
```python
to_data_uri(self, mime_type: str | None = None) -> str
@@ -138,7 +138,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 +146,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
```
-### `File`
+### `File`
Helper class for returning file data from tools.
@@ -160,10 +160,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
```
-### `ContextSamplingFallbackProtocol`
+### `ContextSamplingFallbackProtocol`
diff --git a/docs/python-sdk/fastmcp-utilities-versions.mdx b/docs/python-sdk/fastmcp-utilities-versions.mdx
index f5e44f296..c571c571f 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,7 +38,7 @@ 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
@@ -56,7 +56,7 @@ Use with sorted() or max() to order components by version.
- A sortable VersionKey.
-### `compare_versions`
+### `compare_versions`
```python
compare_versions(a: str | None, b: str | None) -> int
@@ -73,7 +73,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 +90,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 +107,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 +124,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.
@@ -143,7 +163,7 @@ match any spec.
**Methods:**
-#### `matches`
+#### `matches`
```python
matches(self, version: str | None) -> bool
@@ -162,7 +182,7 @@ from version-specific rules.
- True if the version matches the spec.
-#### `intersect`
+#### `intersect`
```python
intersect(self, other: VersionSpec | None) -> VersionSpec
@@ -181,7 +201,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/oauth-proxy.mdx b/docs/servers/auth/oauth-proxy.mdx
index e8e79507c..19f5c2cee 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.
diff --git a/docs/servers/auth/oidc-proxy.mdx b/docs/servers/auth/oidc-proxy.mdx
index 86661bc16..73ab43e73 100644
--- a/docs/servers/auth/oidc-proxy.mdx
+++ b/docs/servers/auth/oidc-proxy.mdx
@@ -70,8 +70,9 @@ 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.
diff --git a/docs/servers/dependency-injection.mdx b/docs/servers/dependency-injection.mdx
index d13f43952..40fc7b65b 100644
--- a/docs/servers/dependency-injection.mdx
+++ b/docs/servers/dependency-injection.mdx
@@ -160,14 +160,20 @@ 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).
+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—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()`:
diff --git a/docs/servers/middleware.mdx b/docs/servers/middleware.mdx
index 40449ae10..1a18d89e3 100644
--- a/docs/servers/middleware.mdx
+++ b/docs/servers/middleware.mdx
@@ -421,11 +421,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),
+ )
))
```
@@ -535,30 +545,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
diff --git a/docs/servers/providers/proxy.mdx b/docs/servers/providers/proxy.mdx
index a2def6892..c870c97f8 100644
--- a/docs/servers/providers/proxy.mdx
+++ b/docs/servers/providers/proxy.mdx
@@ -258,7 +258,50 @@ 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
diff --git a/docs/servers/resources.mdx b/docs/servers/resources.mdx
index 0a8a772aa..77843f252 100644
--- a/docs/servers/resources.mdx
+++ b/docs/servers/resources.mdx
@@ -349,7 +349,7 @@ 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.
+- `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`).
diff --git a/docs/servers/server.mdx b/docs/servers/server.mdx
index ed84b5e48..6b192a3b0 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
-
+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
+ A human-readable name for your server, shown in client applications and logs
- 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,106 @@ 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
+
+
+
+### 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 options
+ Authentication provider for securing HTTP-based transports. See [Authentication](/servers/auth/authentication) for configuration
-
- Server-level setup and teardown logic. See [Lifespans](/servers/lifespan) for composable lifespans
+
+ [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
-
- 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
+
+ [Providers](/servers/providers) 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 [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 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.). 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
+
-## Components
+### Handlers and Storage
-FastMCP servers expose three types of components to clients. Each type serves a distinct purpose in the MCP protocol.
+These parameters provide custom handlers for MCP capabilities and persistent storage for session state.
-### Tools
+
+
+ Custom handler for MCP sampling requests (server-initiated LLM calls). See [Sampling](/servers/sampling) for details
+
-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.
+
+ When `"fallback"`, the sampling handler is used only when no tool-specific handler exists. When `"always"`, this handler is used for all sampling requests
+
-```python
-@mcp.tool
-def multiply(a: float, b: float) -> float:
- """Multiplies two numbers together."""
- return a * b
-```
+
+ 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
+
+
-See [Tools](/servers/tools) for detailed documentation.
-
-### Resources
-
-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"})
@@ -174,8 +241,6 @@ The filtering logic works as follows:
To ensure a component is never exposed, you can set `enabled=False` on the component itself. See the component-specific documentation for details.
-Configure tag-based filtering after creating your server.
-
```python
# Only expose components tagged with "public"
mcp = FastMCP()
@@ -192,38 +257,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 +276,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/storage-backends.mdx b/docs/servers/storage-backends.mdx
index 1bdb19b20..e743b5dab 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
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/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..394bcdb02 100644
--- a/docs/updates.mdx
+++ b/docs/updates.mdx
@@ -5,6 +5,26 @@ icon: "sparkles"
tag: NEW
---
+
+
+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.
+
+
+
+
+
+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.
+
+
+
+
+**[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)**
diff --git a/docs/v2/updates.mdx b/docs/v2/updates.mdx
index 65e212e8d..a59a6fc6a 100644
--- a/docs/v2/updates.mdx
+++ b/docs/v2/updates.mdx
@@ -5,6 +5,16 @@ icon: "sparkles"
tag: NEW
---
+
+
+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.
+
+
+
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..5bc868a14
--- /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 httpx
+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 = httpx.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/quiz/quiz_server.py b/examples/apps/quiz/quiz_server.py
new file mode 100644
index 000000000..7de6f08d7
--- /dev/null
+++ b/examples/apps/quiz/quiz_server.py
@@ -0,0 +1,258 @@
+"""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 fastmcp import FastMCP, FastMCPApp
+
+app = FastMCPApp("Quiz")
+
+DEFAULT_QUESTIONS = [
+ {
+ "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[dict] | 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..fe38c64bc
--- /dev/null
+++ b/examples/apps/sales_dashboard/sales_dashboard_server.py
@@ -0,0 +1,233 @@
+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")
+
+MONTHLY_REVENUE = [
+ {"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/auth/clerk_oauth/README.md b/examples/auth/clerk_oauth/README.md
new file mode 100644
index 000000000..84d2b44b1
--- /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://localhost: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..74b1e4687
--- /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://localhost: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/testing_demo/uv.lock b/examples/testing_demo/uv.lock
index 8f07579f9..74be17426 100644
--- a/examples/testing_demo/uv.lock
+++ b/examples/testing_demo/uv.lock
@@ -2,6 +2,18 @@ version = 1
revision = 3
requires-python = ">=3.10"
+[[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"
version = "0.7.0"
@@ -13,26 +25,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 +48,14 @@ wheels = [
[[package]]
name = "authlib"
-version = "1.6.6"
+version = "1.6.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
-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/af/98/00d3dd826d46959ad8e32af2dbb2398868fd9fd0683c26e56d0789bd0e68/authlib-1.6.9.tar.gz", hash = "sha256:d8f2421e7e5980cc1ddb4e32d3f5fa659cfaf60d8eaf3281ebed192e4ab74f04", size = 165134, upload-time = "2026-03-02T07:44:01.998Z" }
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/53/23/b65f568ed0c22f1efacb744d2db1a33c8068f384b8c9b482b52ebdbc3ef6/authlib-1.6.9-py2.py3-none-any.whl", hash = "sha256:f08b4c14e08f0861dc18a32357b33fbcfd2ea86cfe3fe149484b4d764c4a0ac3", size = 244197, upload-time = "2026-03-02T07:44:00.307Z" },
]
[[package]]
@@ -76,29 +78,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 +214,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 +237,67 @@ wheels = [
[[package]]
name = "cryptography"
-version = "46.0.5"
+version = "46.0.6"
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/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" }
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/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" },
+ { url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" },
+ { url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" },
+ { url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" },
+ { url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" },
+ { url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" },
+ { url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" },
+ { url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" },
+ { url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" },
+ { url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" },
+ { url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" },
+ { url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" },
+ { url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" },
+ { url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" },
+ { url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/84/7ccff00ced5bac74b775ce0beb7d1be4e8637536b522b5df9b73ada42da2/cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead", size = 3475444, upload-time = "2026-03-25T23:34:38.944Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/1f/4c926f50df7749f000f20eede0c896769509895e2648db5da0ed55db711d/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8", size = 4218227, upload-time = "2026-03-25T23:34:40.871Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/65/707be3ffbd5f786028665c3223e86e11c4cda86023adbc56bd72b1b6bab5/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0", size = 4381399, upload-time = "2026-03-25T23:34:42.609Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/6d/73557ed0ef7d73d04d9aba745d2c8e95218213687ee5e76b7d236a5030fc/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b", size = 4217595, upload-time = "2026-03-25T23:34:44.205Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/c5/e1594c4eec66a567c3ac4400008108a415808be2ce13dcb9a9045c92f1a0/cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a", size = 4380912, upload-time = "2026-03-25T23:34:46.328Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/89/843b53614b47f97fe1abc13f9a86efa5ec9e275292c457af1d4a60dc80e0/cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e", size = 3409955, upload-time = "2026-03-25T23:34:48.465Z" },
]
[[package]]
name = "cyclopts"
-version = "4.2.1"
+version = "4.10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
@@ -374,27 +307,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 +341,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 +363,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.1.1"
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/25/83/c95d3bf717698a693eccb43e137a32939d2549876e884e246028bff6ecce/fastmcp-3.1.1.tar.gz", hash = "sha256:db184b5391a31199323766a3abf3a8bfbb8010479f77eca84c0e554f18655c48", size = 17347644, upload-time = "2026-03-14T19:12:20.235Z" }
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/70/ea/570122de7e24f72138d006f799768e14cc1ccf7fcb22b7750b2bd276c711/fastmcp-3.1.1-py3-none-any.whl", hash = "sha256:8132ba069d89f14566b3266919d6d72e2ec23dd45d8944622dca407e9beda7eb", size = 633754, upload-time = "2026-03-14T19:12:22.736Z" },
]
[[package]]
@@ -552,14 +462,14 @@ wheels = [
[[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 +495,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 +526,18 @@ 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 = "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 +545,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 +578,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 +589,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 +608,7 @@ wheels = [
[[package]]
name = "mcp"
-version = "1.25.0"
+version = "1.26.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
@@ -782,9 +626,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/fc/6d/62e76bbb8144d6ed86e202b5edd8a4cb631e7c8130f3f4893c3f90262b10/mcp-1.26.0.tar.gz", hash = "sha256:db6e2ef491eecc1a0d93711a76f28dec2e05999f93afd48795da1c1137142c66", size = 608005, upload-time = "2026-01-24T19:40:32.468Z" }
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/fd/d9/eaa1f80170d2b7c5ba23f3b59f766f3a0bb41155fbc32a69adfa1adaaef9/mcp-1.26.0-py3-none-any.whl", hash = "sha256:904a21c33c25aa98ddbeb47273033c435e595bbacfdb177f4bd87f6dceebe1ca", size = 233615, upload-time = "2026-01-24T19:40:30.652Z" },
]
[[package]]
@@ -819,107 +663,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 +710,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 +734,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 +754,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,40 +884,16 @@ wheels = [
[[package]]
name = "pydantic-settings"
-version = "2.11.0"
+version = "2.13.1"
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/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" }
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/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" },
]
[[package]]
@@ -1177,11 +907,14 @@ wheels = [
[[package]]
name = "pyjwt"
-version = "2.10.1"
+version = "2.12.1"
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/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" }
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/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" },
]
[package.optional-dependencies]
@@ -1200,7 +933,7 @@ wheels = [
[[package]]
name = "pytest"
-version = "8.4.2"
+version = "9.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -1211,41 +944,32 @@ 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/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
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/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
]
[[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]]
@@ -1352,58 +1076,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 +1118,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" },
]
-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 = "0.52.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/c4/68/79977123bb7be889ad680d79a40f339082c1978b5cfcf62c2d8d196873ac/starlette-0.52.1.tar.gz", hash = "sha256:834edd1b0a23167694292e94f597773bc3f89f362be6effee198165a35d62933", size = 2653702, upload-time = "2026-01-18T13:34:11.062Z" }
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/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" },
]
[[package]]
@@ -1627,66 +1298,56 @@ requires-dist = [
[[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 +1372,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/loq.toml b/loq.toml
index 0fffe3d5c..d495ee57f 100644
--- a/loq.toml
+++ b/loq.toml
@@ -4,7 +4,7 @@
default_max_lines = 1000
respect_gitignore = true
-exclude = ["**/uv.lock", ".git/**", "docs/**"]
+exclude = ["**/uv.lock", ".git/**", ".claude/**", "docs/**"]
[[rules]]
path = "tests/**"
diff --git a/pyproject.toml b/pyproject.toml
index 320938b2e..be8794e95 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -52,10 +52,11 @@ classifiers = [
]
[project.optional-dependencies]
-anthropic = ["anthropic>=0.40.0"]
-apps = ["prefab-ui>=0.6.0"]
-azure = ["azure-identity>=1.16.0"]
-code-mode = ["pydantic-monty>=0.0.7"]
+anthropic = ["anthropic>=0.48.0"]
+apps = ["prefab-ui>=0.18.0"]
+# PyJWT floor: transitive via msal; CVE-2026-32597 affects <= 2.11.0
+azure = ["azure-identity>=1.16.0", "PyJWT>=2.12.0"]
+code-mode = ["pydantic-monty==0.0.9"]
gemini = ["google-genai>=1.18.0"]
openai = ["openai>=1.102.0"]
tasks = ["pydocket>=0.18.0"]
@@ -84,7 +85,7 @@ dev = [
"pytest-timeout>=2.4.0",
"pytest-xdist>=3.6.1",
"ruff>=0.12.8",
- "ty>=0.0.20",
+ "ty>=0.0.26",
"prek>=0.2.12",
"loq>=0.1.0a3",
"opentelemetry-exporter-otlp-proto-grpc>=1.39.0",
@@ -138,6 +139,7 @@ env = [
markers = [
"integration: marks tests as integration tests (deselect with '-m \"not integration\"')",
"client_process: marks tests that spawn client processes via stdio transport. These can create issues when run in the same CI environment as other subprocess-based tests.",
+ "conformance: marks MCP conformance tests (require Node.js/npx)",
]
# Automatically mark all tests in integration_tests folder
pythonpath = ["."]
@@ -197,4 +199,4 @@ known-first-party = ["fastmcp"]
[tool.codespell]
-ignore-words-list = "asend,shttp,te"
+ignore-words-list = "asend,shttp,te"
\ No newline at end of file
diff --git a/scripts/auto_close_needs_mre.py b/scripts/auto_close_needs_mre.py
index 15c525976..a9185f584 100644
--- a/scripts/auto_close_needs_mre.py
+++ b/scripts/auto_close_needs_mre.py
@@ -215,9 +215,29 @@ class GitHubClient:
return timeline
- def close_issue(self, issue_number: int, comment: str) -> bool:
- """Close an issue with a comment."""
- # First add the comment
+ def close_issue(self, issue_number: int, comment: str) -> tuple[bool, bool]:
+ """Close an issue with a comment.
+
+ Closes first, then comments — so a failed comment never leaves
+ a misleading "closing" notice on a still-open issue.
+
+ Returns (closed, commented) so the caller can log partial failures.
+ """
+ # Close the issue first
+ issue_url = f"{self.base_url}/issues/{issue_number}"
+ with httpx.Client() as client:
+ response = client.patch(
+ issue_url, headers=self.headers, json={"state": "closed"}
+ )
+
+ if response.status_code != 200:
+ print(
+ f"Failed to close issue #{issue_number}: "
+ f"{response.status_code} {response.text}"
+ )
+ return False, False
+
+ # Then add the comment
comment_url = f"{self.base_url}/issues/{issue_number}/comments"
with httpx.Client() as client:
response = client.post(
@@ -225,17 +245,13 @@ class GitHubClient:
)
if response.status_code != 201:
- print(f"Failed to add comment to issue #{issue_number}")
- return False
+ print(
+ f"Issue #{issue_number} was closed but comment failed: "
+ f"{response.status_code} {response.text}"
+ )
+ return True, False
- # Then close the issue
- issue_url = f"{self.base_url}/issues/{issue_number}"
- with httpx.Client() as client:
- response = client.patch(
- issue_url, headers=self.headers, json={"state": "closed"}
- )
-
- return response.status_code == 200
+ return True, True
def find_label_application_date(
@@ -371,9 +387,16 @@ def main():
"**If this was closed in error**, please leave a comment explaining the situation and we'll reopen it."
)
- if client.close_issue(issue.number, close_message):
- print(f"[SUCCESS] Closed issue #{issue.number} (needs MRE)")
+ closed, commented = client.close_issue(issue.number, close_message)
+ if closed:
closed_count += 1
+ if commented:
+ print(f"[SUCCESS] Closed issue #{issue.number} (needs MRE)")
+ else:
+ print(
+ f"[WARNING] Closed issue #{issue.number} but "
+ f"comment was not posted"
+ )
else:
print(f"[ERROR] Failed to close issue #{issue.number}")
diff --git a/src/fastmcp/__init__.py b/src/fastmcp/__init__.py
index a524b402c..3208b064a 100644
--- a/src/fastmcp/__init__.py
+++ b/src/fastmcp/__init__.py
@@ -10,6 +10,7 @@ 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
settings = Settings()
if settings.log_enabled:
@@ -18,16 +19,15 @@ if settings.log_enabled:
enable_rich_tracebacks=settings.enable_rich_tracebacks,
)
+from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.server.server import FastMCP
from fastmcp.server.context import Context
import fastmcp.server
__version__ = _version("fastmcp")
-
-# ensure deprecation warnings are displayed by default
if settings.deprecation_warnings:
- warnings.simplefilter("default", DeprecationWarning)
+ warnings.simplefilter("default", FastMCPDeprecationWarning)
# --- Lazy imports for performance (see #3292) ---
@@ -40,6 +40,10 @@ def __getattr__(name: str) -> object:
from fastmcp.client import Client
return Client
+ if name == "FastMCPApp":
+ from fastmcp.apps.app import FastMCPApp
+
+ return FastMCPApp
if name == "client":
return importlib.import_module("fastmcp.client")
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -49,5 +53,7 @@ __all__ = [
"Client",
"Context",
"FastMCP",
+ "FastMCPApp",
+ "FastMCPDeprecationWarning",
"settings",
]
diff --git a/src/fastmcp/apps/__init__.py b/src/fastmcp/apps/__init__.py
new file mode 100644
index 000000000..d8fc21696
--- /dev/null
+++ b/src/fastmcp/apps/__init__.py
@@ -0,0 +1,18 @@
+"""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 fastmcp.apps.app import FastMCPApp as FastMCPApp
+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
diff --git a/src/fastmcp/apps/app.py b/src/fastmcp/apps/app.py
new file mode 100644
index 000000000..f4fed7e15
--- /dev/null
+++ b/src/fastmcp/apps/app.py
@@ -0,0 +1,428 @@
+"""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, suppress
+from typing import Any, Literal, TypeVar, overload
+
+from mcp.types import AnyFunction, Icon, ToolAnnotations
+
+from fastmcp.server.auth.authorization import AuthCheck
+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
+
+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 prefixes tool names with the app name.
+
+ When ``app_name`` is set, tool references like ``CallTool("store_files")``
+ or ``CallTool(store_files)`` are resolved to
+ ``ResolvedTool(name="Files___store_files")``. This produces stable
+ identifiers that bypass transforms and work without host ``_meta``
+ forwarding.
+ """
+
+ def _prefix(name: str) -> str:
+ if app_name and "___" not in name:
+ return f"{app_name}___{name}"
+ return 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:
+ if "___" in name:
+ raise ValueError(
+ f"App name {name!r} must not contain '___' "
+ "(reserved as the app tool routing separator)"
+ )
+ super().__init__()
+ self.name = name
+ self._local = 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:
+ 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
+
+ app_config = AppConfig(visibility=visibility)
+ meta: dict[str, Any] = {
+ "ui": app_config_to_meta_dict(app_config),
+ "fastmcp": {"app": self.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.local_provider.decorators.tools import (
+ PREFAB_RENDERER_URI,
+ _ensure_prefab_renderer,
+ )
+
+ try:
+ from prefab_ui.renderer import get_renderer_csp
+
+ from fastmcp.apps.config import ResourceCSP
+
+ csp = get_renderer_csp()
+ app_config = AppConfig(
+ resource_uri=PREFAB_RENDERER_URI,
+ visibility=["model"],
+ csp=ResourceCSP(
+ resource_domains=csp.get("resource_domains"),
+ connect_domains=csp.get("connect_domains"),
+ ),
+ )
+ except ImportError:
+ 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_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)
+
+ # Register the Prefab renderer resource on the internal provider
+ with suppress(ImportError):
+ _ensure_prefab_renderer(self._local)
+
+ 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.
+ """
+ if not isinstance(tool, Tool):
+ tool = Tool._ensure_tool(tool)
+
+ meta = dict(tool.meta) if tool.meta else {}
+ meta.setdefault("fastmcp", {})["app"] = self.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/src/fastmcp/apps/approval.py b/src/fastmcp/apps/approval.py
new file mode 100644
index 000000000..17b124e1f
--- /dev/null
+++ b/src/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/src/fastmcp/apps/choice.py b/src/fastmcp/apps/choice.py
new file mode 100644
index 000000000..aaffef903
--- /dev/null
+++ b/src/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/apps/config.py b/src/fastmcp/apps/config.py
new file mode 100644
index 000000000..c55cd0b13
--- /dev/null
+++ b/src/fastmcp/apps/config.py
@@ -0,0 +1,177 @@
+"""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.
+"""
+
+from __future__ import annotations
+
+from typing import Any, Literal
+
+from pydantic import BaseModel, Field
+
+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"
+
+
+class ResourceCSP(BaseModel):
+ """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.
+ """
+
+ connect_domains: list[str] | None = Field(
+ default=None,
+ alias="connectDomains",
+ description="Origins allowed for fetch/XHR/WebSocket (connect-src)",
+ )
+ resource_domains: list[str] | None = Field(
+ default=None,
+ alias="resourceDomains",
+ description="Origins allowed for scripts, images, styles, fonts (script-src etc.)",
+ )
+ frame_domains: list[str] | None = Field(
+ default=None,
+ alias="frameDomains",
+ description="Origins allowed for nested iframes (frame-src)",
+ )
+ base_uri_domains: list[str] | None = Field(
+ default=None,
+ alias="baseUriDomains",
+ description="Allowed base URIs for the document (base-uri)",
+ )
+
+ model_config = {"populate_by_name": True, "extra": "allow"}
+
+
+class ResourcePermissions(BaseModel):
+ """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.
+ """
+
+ camera: dict[str, Any] | None = Field(
+ default=None, description="Request camera access"
+ )
+ microphone: dict[str, Any] | None = Field(
+ default=None, description="Request microphone access"
+ )
+ geolocation: dict[str, Any] | None = Field(
+ default=None, description="Request geolocation access"
+ )
+ clipboard_write: dict[str, Any] | None = Field(
+ default=None,
+ alias="clipboardWrite",
+ description="Request clipboard-write access",
+ )
+
+ model_config = {"populate_by_name": True, "extra": "allow"}
+
+
+class AppConfig(BaseModel):
+ """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).
+ """
+
+ resource_uri: str | None = Field(
+ default=None,
+ alias="resourceUri",
+ description="URI of the UI resource (typically ui:// scheme). Tools only.",
+ )
+ visibility: list[Literal["app", "model"]] | None = Field(
+ default=None,
+ description="Where this tool is visible: 'app', 'model', or both. Tools only.",
+ )
+ csp: ResourceCSP | None = Field(
+ default=None, description="Content Security Policy for the app iframe"
+ )
+ permissions: ResourcePermissions | None = Field(
+ default=None, description="Iframe sandbox permissions"
+ )
+ domain: str | None = Field(default=None, description="Domain for the iframe")
+ prefers_border: bool | None = Field(
+ default=None,
+ 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):
+ return app.model_dump(by_alias=True, exclude_none=True)
+ return app
diff --git a/src/fastmcp/apps/file_upload.py b/src/fastmcp/apps/file_upload.py
new file mode 100644
index 000000000..8cc91e039
--- /dev/null
+++ b/src/fastmcp/apps/file_upload.py
@@ -0,0 +1,393 @@
+"""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
+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 _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().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:
+ if f.get("size", 0) > provider._max_file_size:
+ raise ValueError(
+ f"File {f.get('name', '?')!r} exceeds max size "
+ f"({_format_size(f['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(), # ty:ignore[invalid-argument-type]
+ 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")) # ty:ignore[invalid-argument-type]
+ Muted(Rx("$item.type")) # ty:ignore[invalid-argument-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, # ty:ignore[invalid-argument-type]
+ 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) # ty:ignore[invalid-argument-type]
+ Muted(f.uploaded_at) # ty:ignore[invalid-argument-type]
+ with Row(gap=2):
+ Badge(f.type, variant="secondary") # ty:ignore[invalid-argument-type]
+ Badge(
+ f.size_display, # ty:ignore[invalid-argument-type]
+ 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/src/fastmcp/apps/form.py b/src/fastmcp/apps/form.py
new file mode 100644
index 000000000..1d6f67b5a
--- /dev/null
+++ b/src/fastmcp/apps/form.py
@@ -0,0 +1,184 @@
+"""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
+
+try:
+ 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
+
+import pydantic
+
+from fastmcp.apps.app import FastMCPApp
+
+
+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]) -> str:
+ """Validate and process form submission."""
+ 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,
+ ) -> 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.
+ """
+ _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), # ty:ignore[invalid-argument-type]
+ )
+
+ Form.from_model(
+ model,
+ submit_label=_submit,
+ on_submit=[
+ CallTool(
+ "submit_form",
+ on_success=on_success_actions,
+ ),
+ ],
+ )
+
+ with CardFooter(), If(STATE.submitted):
+ Muted("Submitted.")
+
+ return PrefabApp(
+ view=view,
+ state={"submitted": False},
+ )
diff --git a/src/fastmcp/apps/generative.py b/src/fastmcp/apps/generative.py
new file mode 100644
index 000000000..b3cbe3c33
--- /dev/null
+++ b/src/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] # ty:ignore[invalid-argument-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/src/fastmcp/cli/apps_dev.py b/src/fastmcp/cli/apps_dev.py
new file mode 100644
index 000000000..9a3d7be2d
--- /dev/null
+++ b/src/fastmcp/cli/apps_dev.py
@@ -0,0 +1,1806 @@
+"""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 io
+import json
+import logging
+import os
+import re
+import signal
+import sys
+import tarfile
+import tempfile
+import time
+import urllib.request
+import webbrowser
+from pathlib import Path
+from typing import Any
+from urllib.parse import quote
+
+import httpcore
+import httpx
+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__)
+
+
+# ---------------------------------------------------------------------------
+# 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}
+
+
+
+