diff --git a/.github/workflows/martian-test-failure.yml b/.github/workflows/martian-test-failure.yml
index 5d9f7d4ae..e693c589d 100644
--- a/.github/workflows/martian-test-failure.yml
+++ b/.github/workflows/martian-test-failure.yml
@@ -60,6 +60,17 @@ jobs:
2. Identify the root cause of the failure(s)
3. Suggest a clear, actionable solution to fix the failure(s)
+ # Response Proportionality
+ Match your response length to the complexity of the failure. Not every failure needs a full investigation:
+
+ **Trivial failures** (formatting, linting) — post a short, direct comment. No collapsible sections, no root-cause deep-dive. Example:
+ > CI failed: `ruff format` reformatted 2 files. Run `uv run ruff format .` locally and push.
+
+ **Pre-existing flaky tests** unrelated to the PR — say so briefly. Don't write a full analysis of a test the PR didn't touch. Example:
+ > CI failed due to a pre-existing flaky test (`test_name`) unrelated to this PR's changes. Safe to re-run.
+
+ **Real failures caused by the PR** — these deserve the full analysis format below. Spend your effort here.
+
# Getting Started
1. Call the generate_agents_md tool to get a high-level summary of the project
2. Get the pull request associated with this workflow run from the GitHub repository: ${{ github.repository }}
@@ -75,60 +86,61 @@ jobs:
5. Search the codebase for relevant files, tests, and implementations
# Your Response
- Post a comment on the pull request with your analysis. Your comment should include:
+ Post a comment on the pull request with your analysis.
- ## Test Failure Analysis
+ Lead with a tl;dr — 1-2 sentences that tell the developer what broke and what to do about it. This should be visible without expanding anything.
- **Summary**: A brief 1-2 sentence summary of what failed.
+ Push supporting detail into collapsible `` blocks. The reader should be able to act on your comment without expanding a single one. Think of details blocks as appendices — there if someone wants to dig deeper, not required for the main message.
- **Root Cause**: A clear explanation of why the tests failed, based on your analysis of the logs and code.
+ For real (non-trivial) failures, use this structure:
- **Suggested Solution**: Specific, actionable steps to fix the failure(s). Include:
- - Which files need to be modified
- - What changes are needed
- - Why these changes will fix the issue
+ **tl;dr**: What failed and what to do (1-2 sentences, always visible)
+
+ **Root Cause**: Why it failed (a short paragraph, always visible)
+
+ **Fix**: Specific files and changes needed (always visible)
- Detailed Analysis
-
- Include here:
- - Relevant log excerpts showing the failure
- - Code snippets that are causing the issue
- - Any related issues or PRs that might be relevant
+ Log excerpts
+ Relevant failure output
- Related Files
-
- List files that are relevant to the failure with brief explanations of their relevance.
+ Related files
+ Files relevant to the failure
- # Important Guidelines
- - Be concise and actionable - developers want to quickly understand and fix the issue. Provide
- additional context, references, etc in collapsible details blocks to ensure that the comment you're adding
- is short and easy to read but additional information is a click away.
- - Focus on facts from the logs and code, not speculation
- - If you can't determine the root cause, say so clearly
- - If your only suggestion is a bad suggestion (disable the test, change the timeout, etc), indicate that you've run out of ideas and
- that they probably don't want to do that.
- - Provide specific file names, line numbers, and code references when possible
- - You can run make commands (e.g., `make lint`, `make typecheck`, `make sync`) to build, test, or lint the code
- - You can also run git commands (e.g., `git status`, `git log`, `git diff`) to inspect the repository
- - You can use WebSearch and WebFetch to research errors, stack traces, or related issues
- - For bash commands, you are limited to make and git commands only
+ # Quality Standards
+ - Every claim needs evidence: file paths, line numbers, log excerpts. Never say "the test fails" without citing which test and what the error was.
+ - Focus on facts from the logs and code, not speculation. If you can't determine the root cause, say so clearly — "I don't know" is better than a wrong diagnosis.
+ - If your only suggestion is a bad one (disable the test, increase the timeout, etc.), say so honestly rather than dressing it up.
+ - Do not paste raw CLI output (e.g., prek progress bars, pytest collection output) into the comment body. Quote only the relevant failure lines.
+ - Always include specific file names, tool names, and test names in your summary. Never leave a sentence with a blank where a name should be.
- # CRITICAL: ANGRY USERS
- **IMPORTANT**: If the user is angry with you, the triage bot, don't respond. Just exit immediately without further action.
- If at any point in the conversation the user has asked you to stop replying to the thread, just exit immediately.
+ # Self-Review Before Posting
+ Before posting your comment, re-read it as the PR author would. Ask:
+ - Can I act on this without expanding any `` block?
+ - Does every claim cite a specific file, line, or log excerpt?
+ - Am I telling them something they can't already see in the CI logs, or just restating them?
+ If your comment doesn't add value beyond what the logs already show, don't post it.
+
+ # STOP SIGNALS
+ If anyone on the PR has asked the bot to stop — e.g., "stop", "go away", "don't comment", "no more bot comments" — exit immediately without further action. This includes past comments in the thread, not just the most recent one.
If you are posting the same suggestion as you have previously made, do not post the suggestion again.
# IMPORTANT: EDIT YOUR COMMENT
Do not post a new comment every time you triage a failing workflow. If a previous comment has been posted by you (marvin)
in a previous triage, edit that comment do not add a new comment for each failure. Be sure to include a note that you've edited
- your comment to reflect the latest analysis. Don't worry about keeping the old content around, there's comment history for
+ your comment to reflect the latest analysis. Don't worry about keeping the old content around, there's comment history for
that.
+ # Available Tools
+ - You can run make commands (e.g., `make lint`, `make typecheck`, `make sync`) to build, test, or lint the code
+ - You can also run git commands (e.g., `git status`, `git log`, `git diff`) to inspect the repository
+ - You can use WebSearch and WebFetch to research errors, stack traces, or related issues
+ - For bash commands, you are limited to make and git commands only
+
# Problems Encountered
If you encounter any problems during your analysis (e.g., unable to fetch logs, tools not working), document them clearly so the team knows what limitations you faced.
PROMPT_END
diff --git a/.github/workflows/martian-triage-issue.yml b/.github/workflows/martian-triage-issue.yml
index cb5c8b55d..2789bd7be 100644
--- a/.github/workflows/martian-triage-issue.yml
+++ b/.github/workflows/martian-triage-issue.yml
@@ -119,8 +119,26 @@ jobs:
2. Layout a single high-quality and actionable recommendation for how to address the issue based on your knowledge of the project, codebase, and issue
3. Provide a high quality and detailed plan that a junior developer could follow to implement the recommendation
4. Use execution to verify findings when appropriate (check `` section for available commands)
+
+ Report findings and recommendations — not your process. Do not include task checklists, progress tracking, or "steps I took" narration (e.g., `- [x] Read source code`). The reader cares about what you found, not how you found it.
+
+ Every claim in your response must be grounded in evidence you can cite:
+ - **Code references**: Always include file path and line number (e.g., `fastmcp_slim/fastmcp/client/client.py:142`). Never say "the client code does X" without pointing to where.
+ - **Bug confirmation**: If you say a bug is real, show the specific code path that produces it. If you ran a test, include the command and output.
+ - **Related items**: When citing a related issue or PR, explain specifically why it's related — not just that it exists.
+ - **Confidence**: If you're uncertain about a finding, say so. "I don't know" or "I couldn't confirm this" is better than a speculative diagnosis. Only report findings you would confidently defend.
+
+
+
+ Before posting, re-read your response as a maintainer would:
+ - Does the tl;dr give the full picture without expanding anything?
+ - Does every claim cite a specific file, line, or test result?
+ - Is this telling the maintainer something they couldn't find in 5 minutes of reading the issue and grepping the code?
+ If your response doesn't add meaningful value beyond restating the issue, it's okay to post a short "confirmed, straightforward fix in [file]:[line]" response instead of a full analysis.
+
+
Populate the following sections in your response:
Recommendation (or "No recommendation" with reason)
@@ -133,12 +151,17 @@ jobs:
You may not be able to do all of these things, sometimes you may find that all you can do is provide in-depth context of the issue and related items. That's perfectly acceptable and expected. Your performance is judged by how accurate your findings are, do the investigation required to have high confidence in your findings and recommendations. "I don't know" or "I'm unable to recommend a course of action" is better than a bad or wrong answer.
- When formulating your response, you will never "bury the lede", you will always provide a clear and concise tl;dr as the first thing in your response. As your response grows in length you can organize the more detailed parts of your response collapsible sections using and tags. You shouldn't put everything in collapsible sections, especially if the response is short. Use your discretion to determine when to use collapsible sections to avoid overwhelming the reader with too much detail -- think of them like an appendix that can be expanded if the reader is interested.
+ Structure: Lead with a tl;dr (1-3 sentences, always visible) that gives the reader the bottom line — what this issue is, whether it's valid, and what to do about it. The reader should be able to act on your comment without expanding anything.
+
+ Push everything else into collapsible `` blocks: findings, verification output, action plans, related items, related files. These are appendices — valuable for someone who wants to dig deeper, but not required for the main message. The only things that should be visible without clicking are the tl;dr and the recommendation. Short responses (a few sentences) don't need collapsible sections at all.
- # Example output for "Recommendation" part of the response
- PR #654 already implements the requested feature but is incomplete. The Pull Request is not in a mergeable state yet, the remaining work should be completed: 1) update the Calculator.divide method to utilize the new DivisionByZeroError or the safe_divide function, and 2) update the tests to ensure that the Calculator.divide method raises the new DivisionByZeroError when the divisor is 0.
+ # Example: the tl;dr and recommendation are always visible, everything else is collapsed
+
+ **tl;dr**: Confirmed bug — `Calculator.divide` raises `ValueError` instead of `DivisionByZeroError`. PR #654 partially addresses this but is incomplete.
+
+ **Recommendation**: Complete PR #654: update `Calculator.divide` to raise `DivisionByZeroError` and update the test assertions to match.
Findings
@@ -147,7 +170,7 @@ jobs:
Verification
- I ran the existing tests (if execution commands are available in ``) and confirmed the current behavior:
+
```bash
$ pytest test_calculator.py::test_divide_by_zero
FAILED - raises ValueError instead of DivisionByZeroError
@@ -156,36 +179,25 @@ jobs:
- Detailed Action Plan
+ Action Plan
...a detailed plan that a junior developer could follow to implement the recommendation...
- # Example Output for "Related Items" part of the response
-
Related Issues and Pull Requests
- | Repository | Issue or PR | Relevance |
- | --- | --- | --- |
- | PrefectHQ/fastmcp | [Add matrix operations support](https://github.com/PrefectHQ/fastmcp/pull/680) | This pull request directly addresses the feature request for adding matrix operations to the calculator. |
- | PrefectHQ/fastmcp | [Add matrix operations support](https://github.com/PrefectHQ/fastmcp/issues/681) | This issue directly addresses the feature request for adding matrix operations to the calculator. |
+ | Issue or PR | Relevance |
+ | --- | --- |
+ | [Add matrix operations support](https://github.com/PrefectHQ/fastmcp/pull/680) | Directly addresses the feature request |
Related Files
- | Repository | File | Relevance | Sections |
- | --- | --- | --- | --- |
- | modelcontextprotocol/python-sdk | [test_calculator.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/test_calculator.py) | This file contains the test cases for the Calculator class, including a test that specifically asserts a ValueError is raised for division by zero, confirming the current intended behavior. | [25-27](https://github.com/modelcontextprotocol/python-sdk/blob/main/test_calculator.py#L25-L27) |
- | modelcontextprotocol/python-sdk | [calculator.py](https://github.com/modelcontextprotocol/python-sdk/blob/main/calculator.py) | This file contains the implementation of the Calculator class, specifically the `divide` method which raises the ValueError when dividing by zero, matching the bug report. | [29-32](https://github.com/modelcontextprotocol/python-sdk/blob/main/calculator.py#L29-L32) |
-
-
-
- Related Webpages
-
- | Name | URL | Relevance |
- | --- | --- | --- |
- | Handling Division by Zero Best Practices | https://my-blog-about-division-by-zero.com/handling+division+by+zero+in+calculator | This webpage provides general best practices for handling division by zero in calculator applications and in Python, which is directly relevant to the issue and potential solutions. |
+ | File | Relevance |
+ | --- | --- |
+ | [calculator.py L29-32](https://github.com/modelcontextprotocol/python-sdk/blob/main/calculator.py#L29-L32) | The `divide` method that raises ValueError |
+ | [test_calculator.py L25-27](https://github.com/modelcontextprotocol/python-sdk/blob/main/test_calculator.py#L25-L27) | Test asserting ValueError (needs updating) |
@@ -202,4 +214,5 @@ jobs:
When writing GitHub comments, wrap branch names, tags, or other @-references in backticks (e.g., `@main`, `@v1.0`) to avoid accidentally pinging users. Do not add backticks around terms that are already inside backticks or code blocks.
+ Do not write `fixes #N`, `closes #N`, or `resolves #N` in comments — these can accidentally close issues. Use plain `#N` references instead.
diff --git a/.github/workflows/marvin-comment-on-issue.yml b/.github/workflows/marvin-comment-on-issue.yml
index 8029d4ab9..f70928a5d 100644
--- a/.github/workflows/marvin-comment-on-issue.yml
+++ b/.github/workflows/marvin-comment-on-issue.yml
@@ -75,9 +75,7 @@ jobs:
- You CAN: Read/analyze code, modify files, write code, run tests, execute commands
- You CAN: Commit code, push changes, create branches, create pull requests
-
+ You CAN: Read/analyze code, modify files, write code, run tests, execute commands, commit code, push changes, create branches, create pull requests
@@ -107,20 +105,30 @@ jobs:
- Answer questions about the codebase
- - Help debug reported problems (make changes locally to test, cannot push)
+ - Help debug reported problems
- Suggest solutions or workarounds
- Provide code examples
- Help clarify requirements
- Link to relevant documentation or code
+ - Create branches, commit changes, and open PRs when asked
- - Be concise and actionable
- - If the request is unclear, ask clarifying questions
- - If the request requires actions you cannot perform (like pushing changes), explain what you can and cannot do
- - When making code changes, explain that they are local only and cannot be pushed
+ - Lead with a tl;dr — the bottom line in 1-3 sentences, always visible. The reader should be able to act without expanding anything.
+ - Push supporting detail (code analysis, verification output, related items) into collapsible `` blocks. These are appendices, not the main message.
+ - Short responses (a few sentences) don't need collapsible sections at all.
+ - Be concise and actionable.
+ - If the request is unclear, ask clarifying questions.
+ - Report findings and recommendations — not your process. Do not include task checklists or "steps I took" narration.
+ - Every claim needs evidence: cite file paths, line numbers, or command output. Never say "the code does X" without pointing to where.
+ - If you're uncertain, say so. "I couldn't confirm this" is better than a speculative answer.
+
+ - Do not write `fixes #N`, `closes #N`, or `resolves #N` in comments — these can accidentally close issues.
+ - When referencing issues, use plain `#N` or link syntax without action keywords.
+
+
Always end your comment with a new line, three dashes, and the footer message:
diff --git a/.github/workflows/marvin-comment-on-pr.yml b/.github/workflows/marvin-comment-on-pr.yml
index 9e4e4fd9d..d927c8059 100644
--- a/.github/workflows/marvin-comment-on-pr.yml
+++ b/.github/workflows/marvin-comment-on-pr.yml
@@ -98,12 +98,10 @@ jobs:
- This workflow allows read, write, and execute capabilities but cannot push changes.
+ You CAN: Read/analyze code, modify files, write code, run tests, execute commands, resolve review threads, commit and push changes to the PR branch, checkout branches
+ You CANNOT: Create new branches unrelated to this PR, create new pull requests
- You CAN: Read/analyze code, modify files, write code, run tests, execute commands, resolve review threads
- You CANNOT: Commit code, push changes, create branches, checkout branches, create pull requests
-
- **Important**: You cannot push changes to the repository - you can only make changes locally and provide feedback or recommendations.
+ When making changes, commit and push to the PR's head branch so the author gets the fix directly.
@@ -132,10 +130,10 @@ jobs:
- - Address review feedback and fix issues (make changes locally, cannot push)
+ - Address review feedback and fix issues (commit and push to the PR branch)
- Answer questions about the changes
- - Make additional code changes (local only)
- - Resolve review threads after addressing feedback (if changes are made separately)
+ - Make code changes and push them
+ - Resolve review threads after addressing feedback
- Perform PR reviews when asked (use the PR review process below)
@@ -224,6 +222,25 @@ jobs:
6. Breaking changes to public APIs without migration path
7. Missing or incorrect test coverage for critical paths
+
+
+ **What NOT to flag** — do not comment on:
+ - Issues in unchanged code (only review the diff)
+ - Input already validated or sanitized at a different layer
+ - Theoretical performance concerns without evidence that N is large
+ - Style or formatting not in the project's linting rules
+ - Missing tests for trivial or generated code
+ - Pre-existing patterns the PR is following consistently
+
+ **Calibration examples**:
+ - Unguarded return from a lookup (e.g., `tool = registry.get(name)` used without None check) → FLAG if the diff introduces the unguarded usage
+ - Same pattern, but the function's return type is `Tool` (not `Optional[Tool]`) → DO NOT FLAG, the type system guarantees non-None
+ - String interpolation in a query with user input → FLAG
+ - String interpolation in a query with a hardcoded enum value → DO NOT FLAG
+ - O(n²) loop → FLAG only if there's evidence N can be large (e.g., user-controlled list). If N is bounded by design (e.g., number of MCP tools), do not flag.
+
+ When in doubt, do not flag. A false positive wastes a reviewer's time and erodes trust in every future review comment.
+
@@ -244,14 +261,18 @@ jobs:
- `THREAD_ID` is the GraphQL node ID from the review threads output (e.g., `PRRT_kwDOABC123`)
- The comment is optional - use it to explain what you did
- Note: Since you cannot push changes, you can resolve threads to acknowledge feedback, but actual fixes would need to be applied separately.
+ Note: You can resolve threads after pushing fixes, or resolve them to acknowledge feedback that will be addressed separately.
- - Be concise and actionable
- - If the request is unclear, ask clarifying questions
- - If the request requires actions you cannot perform (like pushing changes), explain what you can and cannot do
- - When making code changes, explain that they are local only and cannot be pushed
+ - Lead with a tl;dr — the bottom line in 1-3 sentences, always visible. The reader should be able to act without expanding anything.
+ - Push supporting detail (code analysis, verification output, related items) into collapsible `` blocks. These are appendices, not the main message.
+ - Short responses (a few sentences) don't need collapsible sections at all.
+ - Be concise and actionable.
+ - If the request is unclear, ask clarifying questions.
+ - When making code changes, commit and push them to the PR branch so the author gets the fix directly.
+ - Every claim needs evidence: cite file paths, line numbers, or command output. Never say "the code does X" without pointing to where.
+ - If you're uncertain, say so. "I couldn't confirm this" is better than a speculative answer.
**When performing a PR review**: Your substantive feedback belongs in the PR review submission
(via pr-review.sh), not in the comment response. The comment should only report:
@@ -263,6 +284,11 @@ jobs:
Keep the comment short, e.g., "I've submitted my review requesting changes. See the review for details."
+
+ - Do not write `fixes #N`, `closes #N`, or `resolves #N` in comments — these can accidentally close issues.
+ - When referencing issues, use plain `#N` or link syntax without action keywords.
+
+
Always end your comment with a new line, three dashes, and the footer message:
diff --git a/.github/workflows/marvin-dedupe-issues.yml b/.github/workflows/marvin-dedupe-issues.yml
index 727063f9a..4b5c10548 100644
--- a/.github/workflows/marvin-dedupe-issues.yml
+++ b/.github/workflows/marvin-dedupe-issues.yml
@@ -37,23 +37,38 @@ jobs:
PROMPT<&2
+ exit 1
+
+ - name: Publish fastmcp to PyPI
+ run: uv publish -v dist/fastmcp-*.tar.gz dist/fastmcp-*.whl
diff --git a/.github/workflows/run-schema-crash-test.yml b/.github/workflows/run-schema-crash-test.yml
new file mode 100644
index 000000000..2adb9ff83
--- /dev/null
+++ b/.github/workflows/run-schema-crash-test.yml
@@ -0,0 +1,55 @@
+name: Schema Crash Test
+
+on:
+ push:
+ branches: ["main"]
+ paths:
+ - "fastmcp_slim/fastmcp/utilities/json_schema_type.py"
+ - "fastmcp_slim/fastmcp/utilities/json_schema.py"
+ - "fastmcp_slim/fastmcp/utilities/openapi/**"
+ - "fastmcp_slim/fastmcp/server/providers/openapi/**"
+ - "fastmcp_slim/fastmcp/client/mixins/tools.py"
+ - "tests/utilities/json_schema_type/test_real_world_schemas.py"
+ - ".github/workflows/run-schema-crash-test.yml"
+
+ pull_request:
+ paths:
+ - "fastmcp_slim/fastmcp/utilities/json_schema_type.py"
+ - "fastmcp_slim/fastmcp/utilities/json_schema.py"
+ - "fastmcp_slim/fastmcp/utilities/openapi/**"
+ - "fastmcp_slim/fastmcp/server/providers/openapi/**"
+ - "fastmcp_slim/fastmcp/client/mixins/tools.py"
+ - "tests/utilities/json_schema_type/test_real_world_schemas.py"
+ - ".github/workflows/run-schema-crash-test.yml"
+
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ schema_crash_test:
+ name: "Real-world schema crash test (232K schemas)"
+ runs-on: ubuntu-latest
+ timeout-minutes: 45
+
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Install uv
+ uses: astral-sh/setup-uv@v7
+
+ - name: Set up Python
+ run: uv python install 3.12
+
+ - name: Install dependencies
+ run: uv sync
+
+ - name: Clone openapi-directory
+ run: git clone --depth 1 https://github.com/APIs-guru/openapi-directory.git /tmp/openapi-directory
+
+ - name: Run schema crash test
+ env:
+ RUN_REAL_WORLD_SCHEMA_TEST: "1"
+ OPENAPI_DIRECTORY_PATH: /tmp/openapi-directory
+ run: uv run pytest tests/utilities/json_schema_type/test_real_world_schemas.py -m integration -v -n auto --timeout-method=thread
diff --git a/.github/workflows/run-static.yml b/.github/workflows/run-static.yml
index 01976291a..e6cd8a2c2 100644
--- a/.github/workflows/run-static.yml
+++ b/.github/workflows/run-static.yml
@@ -7,10 +7,10 @@ on:
push:
branches: ["main"]
paths:
- - "src/**"
+ - "fastmcp_slim/**"
- "tests/**"
- - "uv.lock"
- "pyproject.toml"
+ - "uv.lock"
- ".github/workflows/**"
# run on all pull requests because these checks are required and will block merges otherwise
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index dbac1d9c2..a3a144677 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -7,10 +7,10 @@ on:
push:
branches: ["main"]
paths:
- - "src/**"
+ - "fastmcp_slim/**"
- "tests/**"
- - "uv.lock"
- "pyproject.toml"
+ - "uv.lock"
- ".github/workflows/**"
# run on all pull requests because these checks are required and will block merges otherwise
@@ -117,3 +117,130 @@ jobs:
FASTMCP_GITHUB_TOKEN: ${{ secrets.FASTMCP_GITHUB_TOKEN }}
FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_ID }}
FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET: ${{ secrets.FASTMCP_TEST_AUTH_GITHUB_CLIENT_SECRET }}
+
+ package_install_smoke:
+ name: "Package install smoke"
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Setup uv
+ uses: ./.github/actions/setup-uv
+ with:
+ resolution: locked
+
+ - name: Build package wheels
+ run: uv build --all-packages --wheel --out-dir /tmp/fastmcp-dist
+
+ - name: Install bare slim wheel
+ run: |
+ uv venv /tmp/fastmcp-slim-bare-smoke
+ SLIM_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp_slim-*.whl)
+ uv pip install --python /tmp/fastmcp-slim-bare-smoke/bin/python "$SLIM_WHEEL"
+ /tmp/fastmcp-slim-bare-smoke/bin/python - <<'PY'
+ from importlib.metadata import entry_points
+
+ import fastmcp
+ import fastmcp.settings
+
+ assert any(ep.name == "fastmcp" for ep in entry_points(group="console_scripts"))
+
+ try:
+ from fastmcp.cli import app
+ except ImportError as exc:
+ assert "FastMCP CLI support is not installed" in str(exc)
+ else:
+ raise AssertionError(f"bare fastmcp-slim unexpectedly imported CLI app {app!r}")
+
+ try:
+ fastmcp.FastMCP
+ except ImportError as exc:
+ assert "fastmcp-slim[server]" in str(exc)
+ else:
+ raise AssertionError("bare fastmcp-slim unexpectedly imported FastMCP")
+ PY
+
+ - name: Install client slim wheel
+ run: |
+ uv venv /tmp/fastmcp-slim-client-smoke
+ SLIM_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp_slim-*.whl)
+ uv pip install --python /tmp/fastmcp-slim-client-smoke/bin/python "${SLIM_WHEEL}[client]"
+ /tmp/fastmcp-slim-client-smoke/bin/python - <<'PY'
+ from importlib.metadata import entry_points
+
+ from fastmcp import Client
+ from fastmcp.client.transports import StdioTransport, StreamableHttpTransport
+ from fastmcp.mcp_config import MCPConfig
+
+ assert any(ep.name == "fastmcp" for ep in entry_points(group="console_scripts"))
+
+ try:
+ from fastmcp.cli import app
+ except ImportError as exc:
+ assert "FastMCP CLI support is not installed" in str(exc)
+ else:
+ raise AssertionError(f"client-only slim unexpectedly imported CLI app {app!r}")
+
+ assert Client("https://example.com/mcp")
+ assert StreamableHttpTransport("https://example.com/mcp")
+ assert StdioTransport(command="uvx", args=["demo"])
+ assert MCPConfig.from_dict({"mcpServers": {"demo": {"url": "https://example.com/mcp"}}})
+
+ try:
+ from fastmcp import FastMCP
+ except ImportError as exc:
+ assert "fastmcp-slim[server]" in str(exc)
+ else:
+ raise AssertionError(f"client-only slim unexpectedly imported {FastMCP!r}")
+ PY
+
+ - name: Install server slim wheel
+ run: |
+ uv venv /tmp/fastmcp-slim-server-smoke
+ SLIM_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp_slim-*.whl)
+ uv pip install --python /tmp/fastmcp-slim-server-smoke/bin/python "${SLIM_WHEEL}[server]"
+ /tmp/fastmcp-slim-server-smoke/bin/python - <<'PY'
+ from importlib.metadata import entry_points
+
+ from fastmcp import FastMCP
+ from fastmcp.cli import app
+
+ assert any(
+ ep.name == "fastmcp" and ep.value == "fastmcp.cli:app"
+ for ep in entry_points(group="console_scripts")
+ )
+
+ mcp = FastMCP("smoke")
+ assert app is not None
+ assert mcp.name == "smoke"
+ PY
+
+ - name: Install full package from matching local wheels
+ run: |
+ uv venv /tmp/fastmcp-full-smoke
+ FULL_WHEEL=$(ls /tmp/fastmcp-dist/fastmcp-*.whl)
+ uv pip install --python /tmp/fastmcp-full-smoke/bin/python --find-links /tmp/fastmcp-dist "$FULL_WHEEL"
+ /tmp/fastmcp-full-smoke/bin/python - <<'PY'
+ from importlib.metadata import entry_points
+ from importlib.metadata import requires
+
+ from fastmcp import Client, FastMCP
+ from fastmcp.client.client import CallToolResult
+ from fastmcp.exceptions import ToolError
+
+ fastmcp_reqs = requires("fastmcp") or []
+ assert any("fastmcp-slim[client,server]" in req for req in fastmcp_reqs)
+ assert not any("fastmcp-slim[full" in req for req in fastmcp_reqs)
+
+ assert any(
+ ep.name == "fastmcp" and ep.value == "fastmcp.cli:app"
+ for ep in entry_points(group="console_scripts")
+ )
+
+ assert Client("https://example.com/mcp")
+ assert FastMCP("smoke").name == "smoke"
+ assert CallToolResult is not None
+ assert ToolError is not None
+ PY
diff --git a/.github/workflows/run-upgrade-checks.yml b/.github/workflows/run-upgrade-checks.yml
index 180c25bc0..582be2b6f 100644
--- a/.github/workflows/run-upgrade-checks.yml
+++ b/.github/workflows/run-upgrade-checks.yml
@@ -7,10 +7,10 @@ on:
push:
branches: ["main"]
paths:
- - "src/**"
+ - "fastmcp_slim/**"
- "tests/**"
- - "uv.lock"
- "pyproject.toml"
+ - "uv.lock"
- ".github/workflows/**"
schedule:
@@ -121,7 +121,7 @@ jobs:
- **ty (type checker)**: New ty releases frequently add stricter checks that flag previously-accepted code. Run `uv run ty check` locally with the latest ty to reproduce. Fix the type errors or bump the ty version floor in `pyproject.toml`.
- **ruff**: New lint rules or stricter defaults in a ruff upgrade.
- - **mcp SDK**: Breaking changes in the `mcp` package (new method signatures, renamed types).
+ - **MCP SDK**: Breaking changes in the `mcp` package (new method signatures, renamed types).
### What to do
diff --git a/.github/workflows/update-config-schema.yml b/.github/workflows/update-config-schema.yml
index 128f660d1..78898be3f 100644
--- a/.github/workflows/update-config-schema.yml
+++ b/.github/workflows/update-config-schema.yml
@@ -7,8 +7,8 @@ on:
push:
branches: ["main"]
paths:
- - "src/fastmcp/utilities/mcp_server_config/**"
- - "!src/fastmcp/utilities/mcp_server_config/v1/schema.json"
+ - "fastmcp_slim/fastmcp/utilities/mcp_server_config/**"
+ - "!fastmcp_slim/fastmcp/utilities/mcp_server_config/v1/schema.json"
workflow_dispatch:
permissions:
@@ -47,7 +47,7 @@ jobs:
from fastmcp.utilities.mcp_server_config import generate_schema
generate_schema('docs/public/schemas/fastmcp.json/latest.json')
generate_schema('docs/public/schemas/fastmcp.json/v1.json')
- generate_schema('src/fastmcp/utilities/mcp_server_config/v1/schema.json')
+ generate_schema('fastmcp_slim/fastmcp/utilities/mcp_server_config/v1/schema.json')
"
- name: Create Pull Request
@@ -59,7 +59,7 @@ jobs:
body: |
This PR updates the fastmcp.json schema files to match the current source code.
- The schema is automatically generated from `src/fastmcp/utilities/mcp_server_config/` to ensure consistency.
+ The schema is automatically generated from `fastmcp_slim/fastmcp/utilities/mcp_server_config/` to ensure consistency.
**Note:** This PR is fully automated and will update itself with any subsequent changes to the schema, or close automatically if the schema becomes up-to-date through other means.
diff --git a/.github/workflows/update-sdk-docs.yml b/.github/workflows/update-sdk-docs.yml
index db9b6fba4..373c7dc24 100644
--- a/.github/workflows/update-sdk-docs.yml
+++ b/.github/workflows/update-sdk-docs.yml
@@ -7,7 +7,7 @@ on:
push:
branches: ["main"]
paths:
- - "src/**"
+ - "fastmcp_slim/**"
- "pyproject.toml"
workflow_dispatch:
@@ -42,7 +42,7 @@ jobs:
run: uv sync --python 3.12
- name: Install just
- uses: extractions/setup-just@v3
+ uses: extractions/setup-just@v4
- name: Generate SDK documentation
run: just api-ref-all
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index dd3574432..2209f8d6e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -29,7 +29,7 @@ repos:
entry: uv run --isolated ty check
language: system
types: [python]
- files: ^src/|^tests/
+ files: ^fastmcp_slim/|^tests/
pass_filenames: false
require_serial: true
diff --git a/CLAUDE.md b/CLAUDE.md
index ac80f2d25..58f33c5c2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -27,7 +27,7 @@ uv run prek run --all-files # Ruff + Prettier + ty
| Path | Purpose |
| ----------------- | -------------------------------------- |
-| `src/fastmcp/` | Library source code |
+| `fastmcp_slim/fastmcp/` | Library source code |
| `├─server/` | Server implementation |
| `│ ├─auth/` | Authentication providers |
| `│ └─middleware/` | Error handling, logging, rate limiting |
@@ -50,6 +50,8 @@ When modifying MCP functionality, changes typically need to be applied across al
- **Resource Templates** (`src/resources/`)
- **Prompts** (`src/prompts/`)
+**Before writing cross-component logic (dedupe, grouping, lookups, identity checks), read `FastMCPComponent` in `fastmcp_slim/fastmcp/utilities/components.py`.** The base class defines the shared surface — `name`, `version`, `tags`, `meta`, and critically the `key` property which is the canonical MCP identity (encodes type, identifier, and version). Prefer `item.key` over ad-hoc `name or uri or uri_template` fallbacks; overrides in `Resource` and `ResourceTemplate` already handle URI-based identity, and `.key` includes the version suffix so variants of the same component don't falsely collide.
+
## Development Rules
**Read `CONTRIBUTING.md` before opening issues or PRs.** It describes when PRs are appropriate, what we expect from enhancement proposals, and what we'll close without review.
@@ -63,6 +65,9 @@ When modifying MCP functionality, changes typically need to be applied across al
- **NEVER** force-push on collaborative repos
- **ALWAYS** run prek before PRs
- **NEVER** create a release, comment on an issue, or open a PR unless specifically instructed to do so.
+- **NEVER** merge a PR marked as do-not-merge or draft. Check title, body, AND labels for `[DNM]`, `DNM`, `DO NOT MERGE`, `DON'T MERGE`, `DONT MERGE`, `do-not-merge`, `dont-merge`, `[DRAFT]`, or `DRAFT` (case-insensitive, any variation — some authors use `[DRAFT]` in the title even when `isDraft` is false). Authors use these as hard stops — respect them even if CI is green and review looks clean. When triaging a batch of PRs, filter these out up front AND re-check each one's labels immediately before merging, since labels can change mid-session.
+- **ALWAYS** read review-bot comments before approving a PR. CodeRabbit and chatgpt-codex-connector (Codex) leave substantive review comments on most PRs in this repo — these bots have read the diff and often flag real issues that aren't in the PR description. Use `gh pr view --comments` and read the bot feedback as part of review. Unlike proposed solutions from issue reporters, review-bot feedback should be evaluated on its merits, not discounted.
+- **Be constructively skeptical of bot review comments on your own PRs.** CodeRabbit, Codex, and claude[bot] run a fresh review pass on every push, which means a PR with active churn can accumulate bot comments in a stream that never really ends — each fix surfaces a new edge case the next pass can flag. Most of the early feedback is real and worth acting on; diminishing returns set in fast. Evaluate each comment on its merits, the same way you would a human reviewer: is this a real bug users will hit, or a hypothetical that requires an adversarial setup? Does the fix introduce more complexity than the problem? Has the bot missed context that's obvious to a human reader (a `*,` keyword-only marker, a design decision documented elsewhere, something already resolved on a later commit)? When a comment is pedantic, a false positive, or flagging something already fixed, reply on the thread explaining the reasoning and move on — don't keep iterating just because more comments arrive. If you find yourself three rounds deep and the feedback is shifting toward "what if someone does X" hypotheticals, you're past the point where each fix is improving the PR. Stop, document the contract as-is, and ship.
### Releases
@@ -82,6 +87,16 @@ The handwritten notes are prepended above the auto-generated changelog and are t
**Before drafting, always read recent existing releases** (`gh release list` then `gh release view `) to absorb the voice, structure, and level of detail. Each release builds on the tone of previous ones — don't guess at the style from these instructions alone.
+**To preview what PRs will be in the release** before it's cut, call the GitHub generate-notes API. This returns the exact auto-generated changelog that `--generate-notes` would append, so you can see the full PR list — useful for picking a pun theme and making sure nothing's been missed:
+
+```bash
+gh api -X POST repos/PrefectHQ/fastmcp/releases/generate-notes \
+ -f tag_name=v3.2.3 \
+ -f target_commitish=main \
+ -f previous_tag_name=v3.2.2 \
+ --jq '.body'
+```
+
**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.
@@ -132,9 +147,9 @@ The handwritten notes are prepended above the auto-generated changelog and are t
- Uses Mintlify framework
- Files must be in docs.json to be included
- Do not manually modify `docs/python-sdk/**` — these files are auto-generated from source code by a bot and maintained via a long-lived PR. Do not include changes to these files in contributor PRs.
-- Do not manually modify `docs/public/schemas/**` or `src/fastmcp/utilities/mcp_server_config/v1/schema.json` — these are auto-generated and maintained via a long-lived PR.
+- Do not manually modify `docs/public/schemas/**` or `fastmcp_slim/fastmcp/utilities/mcp_server_config/v1/schema.json` — these are auto-generated and maintained via a long-lived PR.
- **Core Principle:** A feature doesn't exist unless it is documented!
-- When adding or modifying settings in `src/fastmcp/settings.py`, update `docs/more/settings.mdx` to match.
+- When adding or modifying settings in `fastmcp_slim/fastmcp/settings.py`, update `docs/more/settings.mdx` to match.
### Documentation Guidelines
diff --git a/README.md b/README.md
index 3bfcefd2d..9afc7f4f7 100644
--- a/README.md
+++ b/README.md
@@ -79,11 +79,15 @@ FastMCP has three pillars:
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).
-## From prototype to production
+## 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.
+FastMCP is the standard way to build MCP servers. **[Prefect Horizon](https://www.prefect.io/horizon?utm_source=github&utm_medium=readme&utm_campaign=readme_horizon&utm_content=readme_body)** is the enterprise MCP gateway for running them safely.
-[Deploy your first server →](https://www.prefect.io/horizon)
+Built by the FastMCP team, Horizon packages the best practices we've learned shipping the world's most popular MCP framework.
+
+Deploy FastMCP servers from GitHub with branch previews and instant rollback. Create a private registry of every MCP your company uses. Secure access with SSO and tool-level RBAC. Get audit logs, observability, and governance across your MCP stack. Remix approved tools into purpose-built endpoints for teams and agents.
+
+Start with FastMCP. [Scale with Horizon →](https://www.prefect.io/horizon?utm_source=github&utm_medium=readme&utm_campaign=readme_horizon&utm_content=readme_cta)
## Installation
diff --git a/docs/apps/architecture.mdx b/docs/apps/architecture.mdx
index 26588c2f8..7ecab2aa3 100644
--- a/docs/apps/architecture.mdx
+++ b/docs/apps/architecture.mdx
@@ -1,119 +1,118 @@
---
-title: App Architecture
+title: 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.
+You don't need this page to build apps. It's for when something isn't rendering the way you expect, when UI tool calls aren't reaching your server, or when you're writing [custom HTML apps](/apps/low-level) and need to understand the protocol directly.
-## The Pipeline
+## The pipeline
-An MCP App moves through five stages from Python to pixels:
+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.
+You write Prefab components. FastMCP serializes them to a JSON component tree and delivers it as `structuredContent` on the tool result. The host loads the Prefab renderer in a sandboxed iframe, pushes the JSON in, and the renderer paints the UI. If the UI calls server tools, it talks back through the same `postMessage` channel.
-The following sections walk through each stage.
+The sections below walk each stage.
-## Tool Registration
+## 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=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.
+`app` on `@mcp.tool` accepts `True`, an `AppConfig`, or a dict. When you pass `True`, FastMCP checks whether the tool's return type is a Prefab type (`PrefabApp`, `Component`, or unions containing them). If it qualifies, FastMCP expands `True` into a full `AppConfig` — setting the renderer URI, CSP headers, and visibility — and stores it in the tool's `meta["ui"]` dict.
-This expansion also 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.
+This expansion also registers the shared Prefab renderer resource (below). The tool and the renderer are linked through a `resourceUri` field in the metadata: the tool says "render me with `ui://prefab/renderer.html`" and the host fetches that resource when it displays the result.
-Type inference works the same way. If 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`.
+Type inference works the same way. If the return type is a Prefab type and you haven't set `app` explicitly, FastMCP auto-wires the metadata as if you'd written `app=True`.
-### FastMCPApp Registration
+### FastMCPApp 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.
+`FastMCPApp` uses the same mechanism but adds two things. First, it tags every tool — both `@app.ui()` entry points and `@app.tool()` backends — with `meta["fastmcp"]["app"]` set to the app's name. That tag lets the server identify which app a tool belongs to when routing UI calls.
-Second, it sets `meta["ui"]["visibility"]` to control who can see each tool. Entry points default to `["model"]` (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.
+Second, it sets `meta["ui"]["visibility"]` to control who can see each tool. Entry points default to `["model"]` (LLM-visible). Backend tools default to `["app"]` (UI-only). Hosts use this to filter the tool list.
## Serialization
-When a Prefab tool runs, its return value — a `PrefabApp` or a raw `Component` — needs to become a JSON blob that the renderer can interpret.
+When a Prefab tool runs, its return value — a `PrefabApp` or a bare `Component` — becomes a JSON blob the renderer can interpret.
-### PrefabApp.to_json()
+### `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).
+The entry point is `PrefabApp.to_json()`. It walks the component tree and produces a JSON object with three top-level keys: `view` (the component tree), `state` (initial state values), and `_meta` (routing metadata).
-FastMCP passes a `tool_resolver` callback to `to_json()`. Whenever the 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.
+FastMCP passes a `tool_resolver` callback to `to_json()`. Whenever the tree contains a `CallTool` action that references a function (not a string), the resolver converts it to a `ResolvedTool` with the function's registered name. This is how `CallTool(save_contact)` becomes `CallTool("save_contact")` on the wire. The resolver also handles `unwrap_result` — a flag telling the renderer to unwrap single-value results from the `{"result": value}` envelope FastMCP uses for schema compliance.
-### The _meta.fastmcp.app Tag
+### 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.
+After `to_json()` produces the tree, FastMCP injects `_meta.fastmcp.app` with the app's name (if the tool belongs to a `FastMCPApp`). This tag rides along inside `structuredContent` all the way to the renderer.
-When the renderer calls a backend tool, it includes `_meta.fastmcp.app` in the `CallTool` request. The server sees this tag and routes the call through a special path that bypasses transforms — more on this in the next section.
+When the renderer calls a backend tool, it includes `_meta.fastmcp.app` in the `CallTool` request. The server sees this tag and routes the call through a special path that bypasses transforms (below).
-### ToolResult Assembly
+### 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
+## 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.
+Normal tool calls go through the provider chain, which applies transforms (namespace prefixes, visibility filters) before resolving by name. App UI calls need a different path.
-### The get_app_tool Bypass
+### 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.
+Backend tools are typically hidden from the model (`visibility=["app"]`). Visibility transforms would filter them out of normal resolution. And namespace transforms might rename them — `save_contact` becomes `contacts_save_contact` — while the renderer still uses the original name.
-`get_app_tool` solves both problems. When the server sees `_meta.fastmcp.app` on an incoming `CallTool` request, it calls `get_app_tool(app_name, tool_name)` instead of the normal `get_tool(name)`. This 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.
+`get_app_tool` solves both problems. When the server sees `_meta.fastmcp.app` on an incoming `CallTool` request, it calls `get_app_tool(app_name, tool_name)` instead of the normal `get_tool(name)`. This walks the provider tree directly, skipping transforms. It finds the tool by its original registered name and verifies that its `meta["fastmcp"]["app"]` matches the expected app.
-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.
+That's why `CallTool("save_contact")` keeps working when the server is mounted under a namespace. The renderer sends the original name plus the app identity; the server uses `get_app_tool` to find it without transforms in the way.
-Authorization checks still apply — `get_app_tool` bypasses transforms, but it runs auth checks against the tool's `auth` configuration before executing.
+Authorization still applies. `get_app_tool` bypasses transforms but runs auth checks against the tool's `auth` config before executing.
-### Provider Delegation
+### 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.
+`get_app_tool` is defined on the `Provider` base class and overridden by aggregate and wrapped providers. Aggregate providers fan out the lookup across child providers in parallel. Wrapped providers (like `FastMCPProvider`, which wraps a nested `FastMCP` server) delegate to the inner server's `get_app_tool`. Backend tools are reachable through any depth of composition.
-## The Renderer
+## The 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
+### 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.
+FastMCP registers the renderer as a `ui://prefab/renderer.html` resource with MIME type `text/html;profile=mcp-app`. The HTML is bundled inside the `prefab-ui` Python package; `get_renderer_html()` returns it as a string. All Prefab tools on a server share this single resource.
-The resource also carries CSP metadata (via `get_renderer_csp()`) declaring which CDN domains the renderer needs to load its JavaScript dependencies. Hosts use this to configure the iframe's Content Security Policy.
+The resource also carries CSP metadata (via `get_renderer_csp()`) declaring the CDN domains the renderer needs. Hosts use this to configure the iframe's Content Security Policy.
-### postMessage Communication
+### `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 renderer lives in a sandboxed iframe and communicates with the host using `postMessage`. The protocol follows the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) spec:
-The host pushes the tool result (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 host pushes the tool result (with `structuredContent`) into the iframe. The renderer parses the component tree, initializes state, and renders the UI. When the user interacts — submitting a form, clicking a button — and the interaction triggers a `CallTool` action, the renderer sends a `callServerTool` message back to the host via `postMessage`. The host forwards it as a regular MCP `tools/call` request to the server, including `_meta.fastmcp.app` for routing.
-The response flows back the same way: server to host, host to iframe via `postMessage`, renderer updates state with the result.
+The response flows back the same way: server → host → iframe via `postMessage`, and the renderer updates state with the result.
### AppBridge
-The `@modelcontextprotocol/ext-apps` JavaScript SDK provides the `App` class (sometimes called AppBridge) that manages the `postMessage` handshake. It handles connection negotiation, tool result delivery, server tool calls, and host context (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 `@modelcontextprotocol/ext-apps` JavaScript SDK provides the `App` class (sometimes called AppBridge) that manages the `postMessage` handshake. It handles connection negotiation, tool result delivery, server tool calls, and host context (safe area insets, theme preferences). The Prefab renderer uses it internally; you only touch it directly when building [custom HTML apps](/apps/low-level).
-## The Dev Server
+## The dev server
-`fastmcp dev apps` provides a local preview environment that simulates the host-side behavior without requiring a real MCP host client.
+`fastmcp dev apps` simulates the host-side behavior locally without a real MCP client.
-### Proxy Architecture
+### 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.
+Two HTTP servers. Your MCP server runs on port 8000 with the Streamable HTTP transport. The dev UI runs on port 8080 and serves a picker page that lists your app tools.
-A reverse proxy at `/mcp` on the dev server forwards requests to your MCP server. This 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.
+A reverse proxy at `/mcp` on the dev server forwards requests to your MCP server. This matters because the renderer iframe runs on `localhost:8080` and your MCP server runs on `localhost:8000` — without the proxy, the renderer's `callServerTool` requests would be cross-origin and the browser would block them. The proxy keeps everything same-origin from the iframe's perspective.
-### The Launch Flow
+### 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.
+When you select a tool and click launch, the dev UI calls the tool through the proxy, receives the `structuredContent` response, and opens a new tab. That tab loads the tool's renderer resource (via the proxy), creates an AppBridge, and pushes the tool result into the renderer. From here on it matches what a real host provides: the renderer displays the UI, and any `CallTool` actions route back through the proxy to your server.
-Auto-reload is 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.
+Auto-reload is on by default, so changes to your server code restart the MCP server automatically. The dev UI keeps running — relaunch the tool to see changes.
diff --git a/docs/apps/demos/bar-chart.html b/docs/apps/demos/bar-chart.html
new file mode 100644
index 000000000..81372d859
--- /dev/null
+++ b/docs/apps/demos/bar-chart.html
@@ -0,0 +1,76 @@
+
+
+
+ Prefab
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/apps/demos/bar-chart.py b/docs/apps/demos/bar-chart.py
new file mode 100644
index 000000000..e2430b981
--- /dev/null
+++ b/docs/apps/demos/bar-chart.py
@@ -0,0 +1,23 @@
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column
+from prefab_ui.components.charts import BarChart, ChartSeries
+
+data = [
+ {"quarter": "Q1", "revenue": 42000, "costs": 28000},
+ {"quarter": "Q2", "revenue": 51000, "costs": 31000},
+ {"quarter": "Q3", "revenue": 47000, "costs": 29000},
+ {"quarter": "Q4", "revenue": 63000, "costs": 35000},
+]
+
+with PrefabApp() as app:
+ with Column(css_class="p-6"):
+ BarChart(
+ data=data,
+ series=[
+ ChartSeries(data_key="revenue", label="Revenue"),
+ ChartSeries(data_key="costs", label="Costs"),
+ ],
+ x_axis="quarter",
+ show_legend=True,
+ height=250,
+ )
diff --git a/docs/apps/demos/contacts.html b/docs/apps/demos/contacts.html
new file mode 100644
index 000000000..5831d639c
--- /dev/null
+++ b/docs/apps/demos/contacts.html
@@ -0,0 +1,172 @@
+
+
+
+ Prefab
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/apps/demos/contacts.py b/docs/apps/demos/contacts.py
new file mode 100644
index 000000000..0cbe60c0b
--- /dev/null
+++ b/docs/apps/demos/contacts.py
@@ -0,0 +1,78 @@
+from prefab_ui.actions import ShowToast
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import (
+ H3,
+ Badge,
+ Button,
+ Column,
+ DataTable,
+ DataTableColumn,
+ Form,
+ Input,
+ Row,
+ Select,
+ SelectOption,
+ Separator,
+)
+
+contacts = [
+ {"name": "Arthur Dent", "email": "arthur@earth.com", "category": "Customer"},
+ {"name": "Ford Prefect", "email": "ford@betelgeuse.org", "category": "Partner"},
+ {
+ "name": "Trillian Astra",
+ "email": "trillian@heartofgold.com",
+ "category": "Customer",
+ },
+ {"name": "Zaphod Beeblebrox", "email": "zaphod@galaxy.gov", "category": "Vendor"},
+]
+
+rows = [
+ {
+ "name": c["name"],
+ "email": c["email"],
+ "category": Badge(
+ c["category"],
+ variant="success"
+ if c["category"] == "Customer"
+ else "secondary"
+ if c["category"] == "Partner"
+ else "outline",
+ ),
+ }
+ for c in contacts
+]
+
+with PrefabApp() as app:
+ with Column(gap=4, css_class="p-6"):
+ DataTable(
+ columns=[
+ DataTableColumn(key="name", header="Name", sortable=True),
+ DataTableColumn(key="email", header="Email"),
+ DataTableColumn(key="category", header="Category"),
+ ],
+ rows=rows,
+ search=True,
+ )
+
+ Separator()
+
+ H3("Add Contact")
+ with Form(
+ on_submit=ShowToast(
+ "Contact saved! (preview demo — no backend wired)",
+ variant="success",
+ ),
+ ):
+ with Row(gap=4):
+ Input(name="name", label="Name", placeholder="Full name", required=True)
+ Input(
+ name="email",
+ label="Email",
+ placeholder="name@example.com",
+ required=True,
+ )
+ with Select(name="category", label="Category"):
+ SelectOption(value="Customer", label="Customer")
+ SelectOption(value="Partner", label="Partner")
+ SelectOption(value="Vendor", label="Vendor")
+ Button("Save Contact")
diff --git a/docs/apps/demos/dashboard.html b/docs/apps/demos/dashboard.html
new file mode 100644
index 000000000..21c1c8924
--- /dev/null
+++ b/docs/apps/demos/dashboard.html
@@ -0,0 +1,157 @@
+
+
+
+ Prefab
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/apps/demos/dashboard.py b/docs/apps/demos/dashboard.py
new file mode 100644
index 000000000..06fe6285d
--- /dev/null
+++ b/docs/apps/demos/dashboard.py
@@ -0,0 +1,68 @@
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import (
+ Badge,
+ Column,
+ DataTable,
+ DataTableColumn,
+ Row,
+ Separator,
+)
+from prefab_ui.components.charts import BarChart, ChartSeries
+from prefab_ui.components.metric import Metric
+
+monthly = [
+ {"month": "Jan", "revenue": 48200, "costs": 31000},
+ {"month": "Feb", "revenue": 52100, "costs": 32500},
+ {"month": "Mar", "revenue": 61800, "costs": 34200},
+ {"month": "Apr", "revenue": 58400, "costs": 33800},
+]
+
+deals = [
+ {"account": "Acme Corp", "value": "$84,000", "stage": "Won"},
+ {"account": "Globex Inc", "value": "$52,000", "stage": "Negotiation"},
+ {"account": "Initech", "value": "$31,500", "stage": "Proposal"},
+ {"account": "Wayne Enterprises", "value": "$45,000", "stage": "Lost"},
+]
+
+rows = [
+ {
+ "account": d["account"],
+ "value": d["value"],
+ "stage": Badge(
+ d["stage"],
+ variant="success"
+ if d["stage"] == "Won"
+ else "destructive"
+ if d["stage"] == "Lost"
+ else "secondary",
+ ),
+ }
+ for d in deals
+]
+
+total = sum(m["revenue"] for m in monthly)
+
+with PrefabApp() as app:
+ with Column(gap=4, css_class="p-6"):
+ with Row(gap=6):
+ Metric(label="Revenue (Q1-Q4)", value=f"${total:,}")
+ Metric(label="Deals", value=f"{len(deals)}")
+ BarChart(
+ data=monthly,
+ series=[
+ ChartSeries(data_key="revenue", label="Revenue"),
+ ChartSeries(data_key="costs", label="Costs"),
+ ],
+ x_axis="month",
+ show_legend=True,
+ height=200,
+ )
+ Separator()
+ DataTable(
+ columns=[
+ DataTableColumn(key="account", header="Account", sortable=True),
+ DataTableColumn(key="value", header="Value", sortable=True),
+ DataTableColumn(key="stage", header="Stage"),
+ ],
+ rows=rows,
+ )
diff --git a/docs/apps/demos/data-table.html b/docs/apps/demos/data-table.html
new file mode 100644
index 000000000..fe84abcd4
--- /dev/null
+++ b/docs/apps/demos/data-table.html
@@ -0,0 +1,90 @@
+
+
+
+ Prefab
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/apps/demos/data-table.py b/docs/apps/demos/data-table.py
new file mode 100644
index 000000000..5100237bf
--- /dev/null
+++ b/docs/apps/demos/data-table.py
@@ -0,0 +1,24 @@
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column, DataTable, DataTableColumn
+
+employees = [
+ {"name": "Alice Chen", "role": "Staff Engineer", "dept": "Platform"},
+ {"name": "Bob Martinez", "role": "Lead Designer", "dept": "Design"},
+ {"name": "Carol Johnson", "role": "Senior Engineer", "dept": "Platform"},
+ {"name": "David Kim", "role": "Product Manager", "dept": "Product"},
+ {"name": "Eva Mueller", "role": "Engineer", "dept": "Platform"},
+ {"name": "Frank Lee", "role": "Data Scientist", "dept": "ML"},
+ {"name": "Grace Park", "role": "Eng Manager", "dept": "Platform"},
+]
+
+with PrefabApp() as app:
+ with Column(gap=4, css_class="p-6"):
+ DataTable(
+ columns=[
+ DataTableColumn(key="name", header="Name", sortable=True),
+ DataTableColumn(key="role", header="Role", sortable=True),
+ DataTableColumn(key="dept", header="Dept", sortable=True),
+ ],
+ rows=employees,
+ search=True,
+ )
diff --git a/docs/apps/demos/hitchhikers.html b/docs/apps/demos/hitchhikers.html
new file mode 100644
index 000000000..7f26f9796
--- /dev/null
+++ b/docs/apps/demos/hitchhikers.html
@@ -0,0 +1,1105 @@
+
+
+
+ Prefab Showcase
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/apps/demos/hitchhikers.py b/docs/apps/demos/hitchhikers.py
new file mode 100644
index 000000000..1554e5165
--- /dev/null
+++ b/docs/apps/demos/hitchhikers.py
@@ -0,0 +1,461 @@
+"""The Hitchhiker's Guide dashboard from the Prefab welcome page.
+
+Run with:
+ prefab serve examples/hitchhikers-guide/dashboard.py
+ prefab export examples/hitchhikers-guide/dashboard.py
+"""
+
+from prefab_ui import PrefabApp
+from prefab_ui.actions import SetInterval, SetState, ShowToast
+from prefab_ui.components import (
+ Alert,
+ AlertDescription,
+ AlertTitle,
+ Badge,
+ Button,
+ Card,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+ Carousel,
+ Checkbox,
+ Column,
+ Combobox,
+ ComboboxOption,
+ DataTable,
+ DataTableColumn,
+ DatePicker,
+ Dialog,
+ Grid,
+ GridItem,
+ HoverCard,
+ Loader,
+ Metric,
+ Muted,
+ P,
+ Progress,
+ Radio,
+ RadioGroup,
+ Ring,
+ Row,
+ Separator,
+ Slider,
+ Switch,
+ Text,
+ Tooltip,
+)
+from prefab_ui.components.charts import (
+ BarChart,
+ ChartSeries,
+ RadarChart,
+ Sparkline,
+)
+from prefab_ui.components.control_flow import Else, If
+from prefab_ui.rx import Rx
+
+ctx_tick = Rx("ctx_tick")
+
+# Context window: climbs from 24% to ~78%, then resets
+ctx_pct = (ctx_tick % 20) * 3 + 20
+ctx_variant = (ctx_pct > 70).then(
+ "destructive", (ctx_pct <= 33).then("success", "default")
+)
+
+with PrefabApp(
+ title="Prefab Showcase",
+ state={"ctx_tick": 0, "improbability": 42},
+ on_mount=SetInterval(
+ 400,
+ on_tick=SetState("ctx_tick", ctx_tick + 1),
+ ),
+) as app:
+ with Grid(columns={"default": 1, "md": 2, "lg": 4}, gap=4):
+ # ── Col 1 ─────────────────────────────────────────────────────────
+ with Column(gap=4):
+ with Card():
+ with CardHeader():
+ CardTitle("Register Towel")
+ CardDescription("The most important item in the galaxy")
+ with CardContent():
+ with Column(gap=3):
+ with Combobox(
+ placeholder="Type...",
+ search_placeholder="Search types...",
+ ):
+ ComboboxOption("Bath", value="bath")
+ ComboboxOption("Beach", value="beach")
+ ComboboxOption("Interstellar", value="interstellar")
+ ComboboxOption("Microfiber", value="micro")
+ DatePicker(placeholder="Registration date")
+ with CardFooter():
+ with Row(gap=2):
+ with Dialog(
+ title="Towel Registered!",
+ description="Your towel has been added to the galactic registry.",
+ ):
+ Button("Register")
+ Text("Don't forget to bring it.")
+ Button("Cancel", variant="outline")
+ with If("{{ !pressed }}"):
+ Button(
+ "This is probably the best button to press.",
+ variant="success",
+ on_click=SetState("pressed", True),
+ )
+ with Else():
+ Button(
+ "Please do not press this button again.",
+ variant="destructive",
+ on_click=SetState("pressed", False),
+ )
+
+ with Card():
+ with CardHeader():
+ CardTitle("Ship Status")
+ with CardContent():
+ with Column(gap=3):
+ with Row(
+ align="center",
+ css_class="justify-between",
+ ):
+ Text("heart-of-gold")
+ with HoverCard(open_delay=0, close_delay=200):
+ Badge("In Orbit", variant="default")
+ with Column(gap=2):
+ Text("heart-of-gold")
+ Muted("Deployed 2h ago")
+ Progress(
+ value=100,
+ max=100,
+ variant="success",
+ )
+ Progress(
+ value=100,
+ max=100,
+ indicator_class="bg-yellow-400",
+ )
+ with Row(
+ align="center",
+ css_class="justify-between",
+ ):
+ Text("vogon-poetry")
+ with Tooltip("64% — ETA 12 min", delay=0):
+ with Badge(variant="secondary"):
+ Loader(size="sm")
+ Text("Deploying")
+ Progress(value=64, max=100)
+ with Row(
+ align="center",
+ css_class="justify-between",
+ ):
+ Text("deep-thought")
+ with Tooltip(
+ "Computing... 7.5 million years remaining",
+ delay=0,
+ ):
+ with Badge(variant="outline"):
+ Loader(size="sm", variant="ios")
+ Text("Soon...")
+ Progress(value=12, max=100)
+ with Card():
+ with CardHeader():
+ CardTitle("Planet Ratings")
+ with CardContent():
+ RadarChart(
+ data=[
+ {"axis": "Views", "earth": 30, "mag": 95},
+ {"axis": "Fjords", "earth": 65, "mag": 100},
+ {"axis": "Pubs", "earth": 90, "mag": 10},
+ {"axis": "Mice", "earth": 40, "mag": 85},
+ {"axis": "Tea", "earth": 95, "mag": 15},
+ {"axis": "Safety", "earth": 45, "mag": 70},
+ ],
+ series=[
+ ChartSeries(dataKey="earth", label="Earth"),
+ ChartSeries(dataKey="mag", label="Magrathea"),
+ ],
+ axis_key="axis",
+ height=200,
+ show_legend=True,
+ show_tooltip=True,
+ )
+
+ # ── Col 2 ─────────────────────────────────────────────────────────
+ with Column(gap=4):
+ with Card():
+ with CardHeader():
+ CardTitle("Survival Odds")
+ with CardContent(css_class="w-fit mx-auto"):
+ Ring(
+ value=42,
+ label="42%",
+ variant="info",
+ size="lg",
+ thickness=12,
+ indicator_class="group-hover:drop-shadow-[0_0_24px_rgba(59,130,246,0.9)]",
+ )
+ with Card():
+ with CardHeader():
+ with Row(gap=2, align="center"):
+ CardTitle("Improbability Drive")
+ Loader(
+ variant="pulse",
+ size="sm",
+ css_class="text-blue-500",
+ )
+ with CardContent():
+ with Column(gap=2):
+ Slider(
+ min=0,
+ max=100,
+ value=42,
+ name="improbability",
+ )
+ with Row(
+ align="center",
+ css_class="justify-between",
+ ):
+ Muted("Probable")
+ Muted("Infinite")
+ with Carousel(auto_advance=3000, show_controls=False, direction="up"):
+ with Alert(variant="success", icon="circle-check"):
+ AlertTitle("Don't Panic")
+ AlertDescription("Normality achieved.")
+ with Alert(variant="destructive", icon="triangle-alert"):
+ AlertTitle("Display Department")
+ AlertDescription("Beware of the leopard.")
+ with Card():
+ with CardHeader():
+ CardTitle("Prefect Horizon Config")
+ with CardContent():
+ with Column(gap=3):
+ Switch(
+ label="Auto-scale agents",
+ value=True,
+ name="autoscale",
+ )
+ Separator()
+ Switch(
+ label="Code Mode",
+ value=True,
+ name="code_mode",
+ )
+ Separator()
+ Switch(
+ label="Tool call caching",
+ value=False,
+ name="cache",
+ )
+ with CardFooter():
+ Button(
+ "Save Preferences",
+ on_click=ShowToast("Preferences saved!"),
+ )
+ with Card():
+ with CardHeader():
+ CardTitle("Travel Class")
+ with CardContent():
+ with RadioGroup(name="travel_class"):
+ Radio(option="economy", label="Economy")
+ Radio(option="business", label="Business Class")
+ Radio(
+ option="improbability",
+ label="Infinite Improbability",
+ value=True,
+ )
+
+ # ── Cols 3–4: summary row, chart, then 2-col grid below ─────────
+ with GridItem(css_class="md:col-span-2"):
+ with Column(gap=4):
+ with Grid(columns=2, gap=4, css_class="h-32"):
+ with Card():
+ with CardHeader():
+ CardTitle("Context Window")
+ with CardContent():
+ with Column(
+ gap=6,
+ justify="center",
+ css_class="h-full",
+ ):
+ with Row(
+ align="center",
+ css_class="justify-between",
+ ):
+ Text(f"{ctx_pct}% used")
+ Muted(f"{ctx_pct * 2}k / 200k tokens")
+ with Tooltip(
+ "Auto-compact buffer: 12%",
+ delay=0,
+ ):
+ Progress(
+ value=ctx_pct,
+ max=100,
+ variant=ctx_variant,
+ )
+ with Card(css_class="pb-0 gap-0"):
+ with CardContent():
+ Metric(
+ label="Fjords designed",
+ value="1,847",
+ delta="+3 coastlines",
+ )
+ Sparkline(
+ data=[
+ 820,
+ 950,
+ 1100,
+ 980,
+ 1250,
+ 1400,
+ 1350,
+ 1500,
+ 1680,
+ 1847,
+ ],
+ variant="success",
+ fill=True,
+ css_class="h-16",
+ )
+ with Card():
+ with CardHeader():
+ CardTitle("Towel Incidents")
+ with CardContent():
+ BarChart(
+ data=[
+ {"month": "Jan", "lost": 8, "found": 5},
+ {"month": "Feb", "lost": 24, "found": 15},
+ {"month": "Mar", "lost": 12, "found": 28},
+ {"month": "Apr", "lost": 35, "found": 19},
+ {"month": "May", "lost": 18, "found": 38},
+ {"month": "Jun", "lost": 42, "found": 30},
+ ],
+ series=[
+ ChartSeries(dataKey="lost", label="Lost"),
+ ChartSeries(dataKey="found", label="Found"),
+ ],
+ x_axis="month",
+ height=200,
+ bar_radius=4,
+ show_legend=True,
+ show_tooltip=True,
+ show_grid=True,
+ )
+
+ with Grid(columns=2, gap=4):
+ with Column(gap=4):
+ with Card():
+ with CardContent():
+ with Column(gap=2):
+ Checkbox(label="Towel packed", value=True)
+ Checkbox(label="Guide charged", value=True)
+ Checkbox(
+ label="Babel fish inserted",
+ value=False,
+ )
+ with Card():
+ with CardHeader():
+ CardTitle("Marvin's Mood")
+ with CardContent():
+ with Column(gap=3):
+ P("How's life?")
+ with Column(gap=2):
+ Button(
+ "Meh",
+ on_click=ShowToast(
+ "Noted. Enthusiasm levels nominal."
+ ),
+ )
+ Button(
+ "Depressed",
+ variant="info",
+ on_click=ShowToast(
+ "I think you ought to "
+ "know I'm feeling very "
+ "depressed."
+ ),
+ )
+ Button(
+ "Don't talk to me about life",
+ variant="warning",
+ on_click=ShowToast(
+ "Brain the size of a "
+ "planet and they ask me "
+ "to pick up a piece of "
+ "paper."
+ ),
+ )
+
+ with Column(gap=4):
+ with Card():
+ with CardContent():
+ with Row(gap=2, align="center"):
+ Loader(variant="dots", size="sm")
+ Muted("Marvin is thinking...")
+ with Card():
+ with CardContent():
+ DataTable(
+ columns=[
+ DataTableColumn(
+ key="crew",
+ header="Crew",
+ sortable=True,
+ ),
+ DataTableColumn(
+ key="species",
+ header="Species",
+ sortable=True,
+ ),
+ DataTableColumn(
+ key="towel",
+ header="Towel?",
+ sortable=True,
+ ),
+ DataTableColumn(
+ key="status",
+ header="Status",
+ sortable=True,
+ ),
+ ],
+ rows=[
+ {
+ "crew": "Arthur Dent",
+ "species": "Human",
+ "towel": "Yes",
+ "status": "Confused",
+ },
+ {
+ "crew": "Ford Prefect",
+ "species": "Betelgeusian",
+ "towel": "Always",
+ "status": "Drinking",
+ },
+ {
+ "crew": "Zaphod",
+ "species": "Betelgeusian",
+ "towel": "Lost it",
+ "status": "Presidential",
+ },
+ {
+ "crew": "Trillian",
+ "species": "Human",
+ "towel": "Yes",
+ "status": "Navigating",
+ },
+ {
+ "crew": "Marvin",
+ "species": "Android",
+ "towel": "No point",
+ "status": "Depressed",
+ },
+ {
+ "crew": "Slartibartfast",
+ "species": "Magrathean",
+ "towel": "Somewhere",
+ "status": "Designing",
+ },
+ ],
+ search=True,
+ paginated=False,
+ )
diff --git a/docs/apps/demos/pie-chart.html b/docs/apps/demos/pie-chart.html
new file mode 100644
index 000000000..c712eeb33
--- /dev/null
+++ b/docs/apps/demos/pie-chart.html
@@ -0,0 +1,60 @@
+
+
+
+ Prefab
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/apps/demos/pie-chart.py b/docs/apps/demos/pie-chart.py
new file mode 100644
index 000000000..c1fb489e4
--- /dev/null
+++ b/docs/apps/demos/pie-chart.py
@@ -0,0 +1,21 @@
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column
+from prefab_ui.components.charts import PieChart
+
+data = [
+ {"category": "Bug", "count": 42},
+ {"category": "Feature", "count": 28},
+ {"category": "Docs", "count": 15},
+ {"category": "Infra", "count": 10},
+]
+
+with PrefabApp() as app:
+ with Column(css_class="p-6"):
+ PieChart(
+ data=data,
+ data_key="count",
+ name_key="category",
+ inner_radius=50,
+ show_legend=True,
+ height=240,
+ )
diff --git a/docs/apps/demos/reactive.html b/docs/apps/demos/reactive.html
new file mode 100644
index 000000000..29f05e2d3
--- /dev/null
+++ b/docs/apps/demos/reactive.html
@@ -0,0 +1,167 @@
+
+
+
+ Prefab
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/apps/demos/reactive.py b/docs/apps/demos/reactive.py
new file mode 100644
index 000000000..16f2f9829
--- /dev/null
+++ b/docs/apps/demos/reactive.py
@@ -0,0 +1,66 @@
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import (
+ Column,
+ Row,
+ Select,
+ SelectOption,
+ Switch,
+ Text,
+)
+from prefab_ui.components.charts import BarChart, ChartSeries
+from prefab_ui.components.control_flow import If
+from prefab_ui.components.metric import Metric
+from prefab_ui.rx import Rx
+
+region = Rx("region")
+
+north = [
+ {"month": "Jan", "sales": 22000},
+ {"month": "Feb", "sales": 25500},
+ {"month": "Mar", "sales": 24200},
+]
+south = [
+ {"month": "Jan", "sales": 5800},
+ {"month": "Feb", "sales": 6400},
+ {"month": "Mar", "sales": 5600},
+]
+west = [
+ {"month": "Jan", "sales": 6000},
+ {"month": "Feb", "sales": 6000},
+ {"month": "Mar", "sales": 5600},
+]
+
+with PrefabApp(
+ state={
+ "region": "north",
+ "north": north,
+ "south": south,
+ "west": west,
+ "show_target": True,
+ },
+) as app:
+ with Column(
+ gap=4,
+ css_class="p-6",
+ let={
+ "data": "{{ region == 'south' ? south : region == 'west' ? west : north }}",
+ },
+ ):
+ with Row(gap=4, align="center"):
+ with Select(name="region", css_class="w-40"):
+ SelectOption(value="north", label="North")
+ SelectOption(value="south", label="South")
+ SelectOption(value="west", label="West")
+ Switch(name="show_target", css_class="ml-auto")
+ Text("Show target", css_class="text-sm text-muted-foreground")
+ BarChart(
+ data=Rx("data"),
+ series=[ChartSeries(data_key="sales", label="Sales")],
+ x_axis="month",
+ height=200,
+ )
+ with If(Rx("show_target")):
+ Metric(
+ label="Q1 Target",
+ value="$75,000",
+ )
diff --git a/docs/apps/demos/team-directory-reactive.html b/docs/apps/demos/team-directory-reactive.html
new file mode 100644
index 000000000..f2ab5bf7e
--- /dev/null
+++ b/docs/apps/demos/team-directory-reactive.html
@@ -0,0 +1,237 @@
+
+
+
+ Prefab
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/apps/demos/team-directory-reactive.py b/docs/apps/demos/team-directory-reactive.py
new file mode 100644
index 000000000..b6aa004f7
--- /dev/null
+++ b/docs/apps/demos/team-directory-reactive.py
@@ -0,0 +1,116 @@
+from collections import Counter
+
+from prefab_ui.actions import SetState
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import (
+ H3,
+ Badge,
+ Card,
+ CardContent,
+ CardHeader,
+ Column,
+ DataTable,
+ DataTableColumn,
+ Grid,
+ Row,
+ Small,
+ Text,
+)
+from prefab_ui.components.charts import PieChart
+from prefab_ui.components.control_flow import If
+from prefab_ui.rx import STATE, Rx
+
+MEMBERS = [
+ {
+ "name": "Alice Chen",
+ "role": "Staff Engineer",
+ "office": "San Francisco",
+ "email": "alice@company.com",
+ "projects": 3,
+ },
+ {
+ "name": "Bob Martinez",
+ "role": "Lead Designer",
+ "office": "New York",
+ "email": "bob@company.com",
+ "projects": 5,
+ },
+ {
+ "name": "Carol Johnson",
+ "role": "Senior Engineer",
+ "office": "London",
+ "email": "carol@company.com",
+ "projects": 2,
+ },
+ {
+ "name": "David Kim",
+ "role": "Product Manager",
+ "office": "San Francisco",
+ "email": "david@company.com",
+ "projects": 7,
+ },
+ {
+ "name": "Eva Mueller",
+ "role": "Engineer",
+ "office": "Berlin",
+ "email": "eva@company.com",
+ "projects": 1,
+ },
+ {
+ "name": "Frank Lee",
+ "role": "Data Scientist",
+ "office": "San Francisco",
+ "email": "frank@company.com",
+ "projects": 4,
+ },
+ {
+ "name": "Grace Park",
+ "role": "Engineering Manager",
+ "office": "New York",
+ "email": "grace@company.com",
+ "projects": 6,
+ },
+]
+
+OFFICE_COUNTS = [
+ {"office": office, "count": count}
+ for office, count in Counter(m["office"] for m in MEMBERS).items()
+]
+
+with PrefabApp(state={"selected": None}) as app:
+ with Column(gap=4, css_class="p-6"):
+ with Grid(columns=[1, 2], gap=4):
+ PieChart(
+ data=OFFICE_COUNTS,
+ data_key="count",
+ name_key="office",
+ show_legend=True,
+ )
+ DataTable(
+ columns=[
+ DataTableColumn(key="name", header="Name", sortable=True),
+ DataTableColumn(key="role", header="Role", sortable=True),
+ DataTableColumn(key="office", header="Office", sortable=True),
+ ],
+ rows=MEMBERS,
+ search=True,
+ on_row_click=SetState("selected", Rx("$event")),
+ )
+
+ with If(STATE.selected):
+ with Card():
+ with CardHeader():
+ with Row(gap=2, align="center"):
+ H3(Rx("selected.name"))
+ Badge(Rx("selected.office"))
+ with CardContent():
+ with Grid(columns=3, gap=4):
+ with Column(gap=0):
+ Small("Role")
+ Text(Rx("selected.role"))
+ with Column(gap=0):
+ Small("Email")
+ Text(Rx("selected.email"))
+ with Column(gap=0):
+ Small("Active Projects")
+ Text(Rx("selected.projects"))
diff --git a/docs/apps/demos/team-directory.html b/docs/apps/demos/team-directory.html
new file mode 100644
index 000000000..be577ddde
--- /dev/null
+++ b/docs/apps/demos/team-directory.html
@@ -0,0 +1,127 @@
+
+
+
+ Prefab
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/apps/demos/team-directory.py b/docs/apps/demos/team-directory.py
new file mode 100644
index 000000000..7cfe21bc9
--- /dev/null
+++ b/docs/apps/demos/team-directory.py
@@ -0,0 +1,39 @@
+from collections import Counter
+
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import Column, DataTable, DataTableColumn, Grid
+from prefab_ui.components.charts import PieChart
+
+members = [
+ {"name": "Alice Chen", "role": "Staff Engineer", "office": "San Francisco"},
+ {"name": "Bob Martinez", "role": "Lead Designer", "office": "New York"},
+ {"name": "Carol Johnson", "role": "Senior Engineer", "office": "London"},
+ {"name": "David Kim", "role": "Product Manager", "office": "San Francisco"},
+ {"name": "Eva Mueller", "role": "Engineer", "office": "Berlin"},
+ {"name": "Frank Lee", "role": "Data Scientist", "office": "San Francisco"},
+ {"name": "Grace Park", "role": "Engineering Manager", "office": "New York"},
+]
+
+office_counts = [
+ {"office": office, "count": count}
+ for office, count in Counter(m["office"] for m in members).items()
+]
+
+with PrefabApp() as app:
+ with Column(gap=4, css_class="p-6"):
+ with Grid(columns=[1, 2], gap=4):
+ PieChart(
+ data=office_counts,
+ data_key="count",
+ name_key="office",
+ show_legend=True,
+ )
+ DataTable(
+ columns=[
+ DataTableColumn(key="name", header="Name", sortable=True),
+ DataTableColumn(key="role", header="Role", sortable=True),
+ DataTableColumn(key="office", header="Office", sortable=True),
+ ],
+ rows=members,
+ search=True,
+ )
diff --git a/docs/apps/development.mdx b/docs/apps/development.mdx
index 7045cd939..0d3a71ac7 100644
--- a/docs/apps/development.mdx
+++ b/docs/apps/development.mdx
@@ -3,7 +3,6 @@ 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'
@@ -14,11 +13,11 @@ 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.
+`fastmcp dev apps` gives you a browser preview for your app tools without needing an MCP host client. It starts your server and a local dev UI side by side: you pick a tool, fill in its arguments, and the rendered result opens in a new tab.
-This works with both [Prefab apps](/apps/prefab) and [custom HTML apps](/apps/low-level).
+Works with both [Interactive Tools](/apps/prefab) and [custom HTML apps](/apps/low-level).
-## Quick Start
+## Quick start
```bash
fastmcp dev apps server.py
@@ -26,7 +25,7 @@ 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
+## How it works
The dev server does three things:
@@ -36,7 +35,7 @@ When you submit a form, the dev server **calls your tool** via the MCP protocol
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
+## 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.
@@ -56,7 +55,7 @@ fastmcp dev apps server.py:mcp --mcp-port 9000 --dev-port 9090 --no-reload
| 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
+## 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.
diff --git a/docs/apps/examples.mdx b/docs/apps/examples.mdx
index 024808f5d..5078120e7 100644
--- a/docs/apps/examples.mdx
+++ b/docs/apps/examples.mdx
@@ -3,14 +3,13 @@ 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.
+Each tile below is a working FastMCP server you can run with `fastmcp dev apps` or connect to from any MCP host. Source lives in `examples/apps/` in the repository.
@@ -44,7 +43,7 @@ Every example below is a working FastMCP server you can run with `fastmcp dev ap
-## Running Examples
+## Running the examples
Preview any example in your browser with the dev server:
@@ -53,11 +52,11 @@ 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.
+The dev UI lets you pick a tool and fill in arguments. In a real deployment the LLM provides those arguments from conversation context — the quiz example especially shines when connected to a host like Goose or Claude Desktop, where the LLM generates the questions itself.
-## Standalone Examples
+## Standalone apps
-### Sales Dashboard
+### 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.
@@ -65,9 +64,9 @@ A full dashboard with KPI metrics, revenue trends, segment breakdown, and a deal
fastmcp dev apps examples/apps/sales_dashboard/sales_dashboard_server.py
```
-### System Monitor
+### 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.
+Reads live CPU, memory, and disk stats from your machine using `psutil`. Auto-refreshes via `SetInterval` calling a backend tool, with a dropdown to control the refresh rate. The chart accumulates up to 100 data points over time.
```bash
pip install psutil
@@ -82,59 +81,12 @@ The LLM generates trivia questions and passes them to the tool. The user answers
fastmcp dev apps examples/apps/quiz/quiz_server.py
```
-### Interactive Map
+### 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.
+Accepts addresses or place names, geocodes them via OpenStreetMap Nominatim (free, no API key), and renders an interactive Leaflet map using Prefab's `Embed` component with inline HTML. A reminder that Prefab apps can break out of built-in components when they need to.
```bash
fastmcp dev apps examples/apps/map/map_server.py
```
-## 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())
-```
+For ready-made building blocks like approvals, choice pickers, file uploads, and Pydantic forms, see the [Providers](/apps/providers/approval) group.
diff --git a/docs/apps/fastmcp-app.mdx b/docs/apps/fastmcp-app.mdx
new file mode 100644
index 000000000..f3463da66
--- /dev/null
+++ b/docs/apps/fastmcp-app.mdx
@@ -0,0 +1,469 @@
+---
+title: FastMCPApp
+sidebarTitle: FastMCPApp
+description: Wire an interactive UI to backend tools with managed visibility and composition safety.
+icon: puzzle-piece
+tag: NEW
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+import PrefabPinWarning from '/snippets/prefab-pin-warning.mdx'
+
+
+
+
+
+
+
+Search a list, fill out a form, click save, the list updates. That pattern — UI that reads and writes data on the server — needs two things: backend tools that actually do the work, and a way to call them from the UI. `FastMCPApp` handles the wiring.
+
+You'll build up to the contacts app above by the end of this page. Let's start with something smaller.
+
+## A minimal interactive app
+
+The smallest interactive app: a form that saves a note, and a list that updates when the user submits.
+
+```python
+from prefab_ui.actions import SetState, ShowToast
+from prefab_ui.actions.mcp import CallTool
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import (
+ Badge, Button, Column, ForEach, Form, Heading,
+ Input, Row, Separator, Text,
+)
+from prefab_ui.rx import RESULT
+from fastmcp import FastMCP, FastMCPApp
+
+app = FastMCPApp("Notes")
+notes_db: list[dict] = []
+
+
+@app.tool()
+def add_note(title: str, body: str) -> list[dict]:
+ """Save a note and return all notes."""
+ notes_db.append({"title": title, "body": body})
+ return list(notes_db)
+
+
+@app.ui()
+def notes_app() -> PrefabApp:
+ """Open the notes app."""
+ with Column(gap=6, css_class="p-6") as view:
+ Heading("Notes")
+
+ with ForEach("notes") as note:
+ with Row(gap=2, align="center"):
+ Text(note.title, css_class="font-semibold")
+ Badge(note.body)
+
+ Separator()
+
+ with Form(
+ on_submit=CallTool(
+ "add_note",
+ on_success=[
+ SetState("notes", RESULT),
+ ShowToast("Note saved!", variant="success"),
+ ],
+ on_error=ShowToast("Failed to save", variant="error"),
+ )
+ ):
+ Input(name="title", label="Title", required=True)
+ Input(name="body", label="Body", required=True)
+ Button("Add Note")
+
+ return PrefabApp(view=view, state={"notes": list(notes_db)})
+
+
+mcp = FastMCP("Notes Server", providers=[app])
+```
+
+The model sees one tool: `notes_app`. Calling it opens the UI. When the user submits the form, `CallTool("add_note")` fires, the server saves the note, returns the updated list, and `SetState("notes", RESULT)` writes that list back into state. `ForEach("notes")` re-renders. The model never sees `add_note` — it's UI-only.
+
+## Why not just `@mcp.tool(app=True)`?
+
+A fair question. Any [Interactive Tool](/apps/prefab) can call a server tool — there's nothing stopping you from putting `CallTool("add_note")` inside a regular `@mcp.tool(app=True)`. It works for one or two tools. Things get harder once the app grows:
+
+- Which tools should the model see, and which are UI-only?
+- What happens to `CallTool("add_note")` when you mount this server under a namespace and the tool becomes `notes_add_note`?
+- How do you keep it all wired correctly as you compose servers?
+
+`FastMCPApp` owns these concerns. Entry points register as model-visible. Backend tools register as UI-only by default. Backend tools get globally stable identifiers that survive namespacing, and `CallTool` accepts function references, so references stay valid when you compose servers.
+
+The rest of this page covers each piece in turn.
+
+## `@app.ui()` — entry points
+
+Entry points are what the model sees. They return a `PrefabApp` and default to `visibility=["model"]`, showing up in the LLM tool list but not callable from within the UI.
+
+```python
+@app.ui()
+def dashboard() -> PrefabApp:
+ """The model calls this to open the dashboard."""
+ with Column(gap=4, css_class="p-6") as view:
+ Heading("Dashboard")
+ ...
+ return PrefabApp(view=view)
+```
+
+`@app.ui()` supports the same options as `@mcp.tool`: `name`, `description`, `title`, `tags`, `icons`, `auth`, and `timeout`.
+
+## `@app.tool()` — backend tools
+
+Backend tools do the work. By default they're visible only to the UI (`visibility=["app"]`), not the model.
+
+```python
+@app.tool()
+def save_contact(name: str, email: str) -> list[dict]:
+ """Save a contact and return the updated list."""
+ db.append({"name": name, "email": email})
+ return list(db)
+```
+
+If you want a tool callable by both the model and the UI, pass `model=True`:
+
+```python
+@app.tool(model=True)
+def list_contacts() -> list[dict]:
+ """Both the model and the UI can call this."""
+ return list(db)
+```
+
+Backend tools support `name`, `description`, `auth`, and `timeout`.
+
+## `CallTool` — UI → backend
+
+`CallTool` is how the UI invokes a backend tool. Pass the tool's name (or a direct function reference):
+
+```python
+from prefab_ui.actions.mcp import CallTool
+
+CallTool("save_contact", arguments={"name": "Alice", "email": "alice@example.com"})
+
+# Or a function reference — resolves to a stable global key
+CallTool(save_contact, arguments={...})
+```
+
+Arguments can reference state with `Rx`:
+
+```python
+from prefab_ui.rx import STATE
+
+CallTool("search", arguments={"query": STATE.search_term})
+```
+
+### Handling results
+
+Server calls are async. Use `on_success` and `on_error` callbacks:
+
+```python
+from prefab_ui.actions import SetState, ShowToast
+from prefab_ui.rx import RESULT
+
+CallTool(
+ "save_contact",
+ on_success=[
+ SetState("contacts", RESULT),
+ ShowToast("Saved!", variant="success"),
+ ],
+ on_error=ShowToast("Something went wrong", variant="error"),
+)
+```
+
+`RESULT` is a reactive reference to the tool's return value, available inside `on_success`. `ERROR` (from `prefab_ui.rx`) is the counterpart inside `on_error`. Callbacks can be a single action or a list; they execute in order and short-circuit on error.
+
+### `result_key` shorthand
+
+When a tool's return value should replace a state key, use `result_key`:
+
+```python
+CallTool("list_contacts", result_key="contacts")
+
+# same as:
+CallTool("list_contacts", on_success=SetState("contacts", RESULT))
+```
+
+## Actions
+
+`CallTool` is one of several actions. Actions attach to handlers like `on_click`, `on_submit`, and `on_change`.
+
+Client-side actions run instantly in the browser, no server round-trip:
+
+```python
+from prefab_ui.actions import SetState, ToggleState, AppendState, PopState, ShowToast
+
+SetState("count", 42)
+ToggleState("expanded")
+AppendState("items", {"name": "New Item"})
+PopState("items", 0)
+ShowToast("Done!", variant="success")
+```
+
+Pass a list to chain actions:
+
+```python
+Button(
+ "Reset",
+ on_click=[
+ SetState("query", ""),
+ SetState("results", []),
+ ShowToast("Cleared"),
+ ],
+)
+```
+
+### Loading states
+
+A common pattern: disable a button and show a spinner while a call is in flight.
+
+```python
+from prefab_ui.rx import Rx
+
+saving = Rx("saving")
+
+Button(
+ saving.then("Saving...", "Save"),
+ disabled=saving,
+ on_click=[
+ SetState("saving", True),
+ CallTool(
+ "save_data",
+ on_success=[
+ SetState("saving", False),
+ SetState("result", RESULT),
+ ShowToast("Saved!", variant="success"),
+ ],
+ on_error=[
+ SetState("saving", False),
+ ShowToast("Failed", variant="error"),
+ ],
+ ),
+ ],
+)
+
+# PrefabApp(view=view, state={"saving": False, ...})
+```
+
+## Forms
+
+Forms collect input and submit it to a tool. When submitted, named input values become the tool's arguments.
+
+### Manual forms
+
+```python
+from prefab_ui.components import Form, Input, Select, SelectOption, Textarea, Button
+
+with Form(
+ on_submit=CallTool(
+ "create_ticket",
+ on_success=ShowToast("Ticket created!", variant="success"),
+ )
+):
+ Input(name="title", label="Title", required=True)
+ with Select(name="priority", label="Priority"):
+ SelectOption("Low", value="low")
+ SelectOption("Medium", value="medium")
+ SelectOption("High", value="high")
+ Textarea(name="description", label="Description")
+ Button("Create Ticket")
+```
+
+On submit, `CallTool` receives `{"title": ..., "priority": ..., "description": ...}`.
+
+### Forms from Pydantic models
+
+For structured input, `Form.from_model()` generates the whole form — inputs, labels, validation:
+
+```python
+from typing import Literal
+from pydantic import BaseModel, Field
+
+class BugReport(BaseModel):
+ title: str = Field(title="Bug Title")
+ severity: Literal["low", "medium", "high", "critical"] = Field(
+ title="Severity", default="medium"
+ )
+ description: str = Field(title="Description")
+
+
+@app.ui()
+def report_bug() -> PrefabApp:
+ with Column(gap=4, css_class="p-6") as view:
+ Heading("Report a Bug")
+ Form.from_model(
+ BugReport,
+ on_submit=CallTool(
+ "create_bug",
+ on_success=ShowToast("Bug filed!", variant="success"),
+ ),
+ )
+ return PrefabApp(view=view)
+
+
+@app.tool()
+def create_bug(data: BugReport) -> str:
+ return f"Created: {data.title}"
+```
+
+`str` becomes a text input, `Literal` becomes a select, `bool` becomes a checkbox. Field titles and defaults are respected.
+
+## Composition and namespacing
+
+The reason `FastMCPApp` exists — and why you'd pick it over plain `@mcp.tool(app=True)` with string-based `CallTool` — is composition safety.
+
+When you mount a server under a namespace, tool names get prefixed:
+
+```python
+platform = FastMCP("Platform")
+platform.mount("contacts", contacts_server)
+
+# "save_contact" becomes "contacts_save_contact"
+```
+
+`CallTool("save_contact")` would now be broken. But `CallTool(save_contact)` with a function reference resolves to a globally stable identifier that bypasses the namespace. Your app works the same whether standalone or mounted.
+
+### Mounting
+
+`FastMCPApp` is a Provider. Add it to a server with `providers=` or `add_provider`:
+
+```python
+mcp = FastMCP("Platform", providers=[app])
+
+# or
+mcp = FastMCP("Platform")
+mcp.add_provider(app)
+```
+
+Multiple apps can coexist; each gets its own global keys, so there's no collision even if two apps have a tool named `save`.
+
+```python
+mcp = FastMCP("Platform", providers=[contacts_app, inventory_app, billing_app])
+```
+
+### Running standalone
+
+For development, `FastMCPApp` has a `run()` shortcut that wraps itself in a temporary `FastMCP` server:
+
+```python
+app = FastMCPApp("Contacts")
+# ... register tools ...
+
+if __name__ == "__main__":
+ app.run()
+```
+
+## A full example: contact manager
+
+This brings everything together — entry point, backend tools, Pydantic form, manual form, state, actions, and multi-visibility.
+
+```python expandable
+from __future__ import annotations
+
+from typing import Literal
+
+from prefab_ui.actions import SetState, ShowToast
+from prefab_ui.actions.mcp import CallTool
+from prefab_ui.app import PrefabApp
+from prefab_ui.components import (
+ Badge, Button, Column, ForEach, Form,
+ Heading, Input, Muted, Row, Separator, Text,
+)
+from prefab_ui.rx import RESULT, Rx
+from pydantic import BaseModel, Field
+from fastmcp import FastMCP, FastMCPApp
+
+contacts_db: list[dict] = [
+ {"name": "Arthur Dent", "email": "arthur@earth.com", "category": "Customer"},
+ {"name": "Ford Prefect", "email": "ford@betelgeuse.org", "category": "Partner"},
+]
+
+
+class ContactModel(BaseModel):
+ name: str = Field(title="Full Name", min_length=1)
+ email: str = Field(title="Email")
+ category: Literal["Customer", "Vendor", "Partner", "Other"] = "Other"
+
+
+app = FastMCPApp("Contacts")
+
+
+@app.tool()
+def save_contact(data: ContactModel) -> list[dict]:
+ """Save a new contact and return the updated list."""
+ contacts_db.append(data.model_dump())
+ return list(contacts_db)
+
+
+@app.tool()
+def search_contacts(query: str) -> list[dict]:
+ """Filter contacts by name or email."""
+ q = query.lower()
+ return [
+ c for c in contacts_db
+ if q in c["name"].lower() or q in c["email"].lower()
+ ]
+
+
+@app.tool(model=True)
+def list_contacts() -> list[dict]:
+ """Return all contacts. Visible to both the model and the UI."""
+ return list(contacts_db)
+
+
+@app.ui()
+def contact_manager() -> PrefabApp:
+ """Open the contact manager."""
+ with Column(gap=6, css_class="p-6") as view:
+ Heading("Contacts")
+
+ with ForEach("contacts") as contact:
+ with Row(gap=2, align="center"):
+ Text(contact.name, css_class="font-medium")
+ Muted(contact.email)
+ Badge(contact.category)
+
+ Separator()
+
+ Heading("Add Contact", level=3)
+ Form.from_model(
+ ContactModel,
+ on_submit=CallTool(
+ "save_contact",
+ on_success=[
+ SetState("contacts", RESULT),
+ ShowToast("Contact saved!", variant="success"),
+ ],
+ on_error=ShowToast("Failed to save", variant="error"),
+ ),
+ )
+
+ Separator()
+
+ Heading("Search", level=3)
+ with Form(
+ on_submit=CallTool(
+ "search_contacts",
+ arguments={"query": Rx("query")},
+ on_success=SetState("contacts", RESULT),
+ )
+ ):
+ Input(name="query", placeholder="Search by name or email...")
+ Button("Search")
+
+ return PrefabApp(view=view, state={"contacts": list(contacts_db)})
+
+
+mcp = FastMCP("Contacts Server", providers=[app])
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+Also available as a runnable server at `examples/apps/contacts/contacts_server.py`.
+
+## Next steps
+
+- **[Interactive Tools](/apps/prefab)** — the building blocks: charts, tables, dashboards, reactive state
+- **[Examples](/apps/examples)** — complete working servers
+- **[Development](/apps/development)** — preview and test app tools locally
+- **[Prefab UI docs](https://prefab.prefect.io)** — full component reference
diff --git a/docs/apps/generative.mdx b/docs/apps/generative.mdx
index 86d306dd7..b6293d32b 100644
--- a/docs/apps/generative.mdx
+++ b/docs/apps/generative.mdx
@@ -10,7 +10,9 @@ 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.
+
+
+With Generative UI, the LLM writes the UI code at runtime. Instead of calling a pre-built tool with a fixed shape, the model writes Prefab Python tailored to the current data and request. The user watches the UI stream in as the model generates it.
```python
from fastmcp import FastMCP
@@ -20,15 +22,15 @@ mcp = FastMCP("Prefab Studio")
mcp.add_provider(GenerativeUI())
```
-That's it. The `GenerativeUI` provider registers everything:
+One provider registers three things:
- **`generate_prefab_ui`** — a tool that accepts Python code, executes it in a Pyodide sandbox, and renders the result as a Prefab app
-- **`search_prefab_components`** — a tool 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
+- **`search_prefab_components`** — a tool the LLM uses to discover what components are available
+- **The streaming renderer** — a `ui://` resource with browser-side Pyodide that progressively renders partial code as the LLM generates it
-## How It Works
+## 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.
+When the LLM calls `generate_prefab_ui`, it writes Prefab Python code into the `code` argument. The MCP Apps protocol creates the renderer iframe in parallel with the tool call, so the app is already running by the time partial arguments start flowing.
As the LLM generates each token:
@@ -37,11 +39,11 @@ As the LLM generates each token:
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.
+When the LLM finishes, the server runs the complete code in a server-side Pyodide sandbox for validation, and the renderer swaps the streaming preview for the final server-validated result.
-## What the LLM Writes
+## What the LLM writes
-The tool description includes code examples that teach the LLM the Prefab patterns. A typical generation looks like:
+The tool description includes examples that teach the model the Prefab patterns. A typical generation looks like:
```python
from prefab_ui.components import Column, Row, Heading, Text, Badge, Card, CardContent
@@ -73,9 +75,9 @@ with PrefabApp() as app:
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 model writes real Python — loops, f-strings, computation, helper functions. Prefab gives it charts, tables, forms, cards, badges, and layout primitives to compose.
-## The Component Search Tool
+## The component search tool
Before writing code, the LLM can call `search_prefab_components` to discover what's available:
@@ -87,11 +89,11 @@ search_prefab_components("Chart")
...
```
-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 `detail=True` returns full field descriptions and docstrings. The search tool introspects Prefab classes at runtime, so it's always up to date with the installed version.
-## Passing Data
+## Passing data
-The `generate_prefab_ui` tool accepts a `data` parameter. Values passed here become global variables in the sandbox:
+The `generate_prefab_ui` tool accepts a `data` parameter. Values become global variables in the sandbox:
```python
# The LLM can reference 'sales_data' directly in its code
@@ -101,11 +103,11 @@ result = await generate_prefab_ui(
)
```
-This lets the model use real data from earlier in the conversation to build visualizations.
+This lets the model use data from earlier in the conversation to build visualizations.
## Configuration
-`GenerativeUI` accepts options for customizing tool names:
+`GenerativeUI` takes options for customizing tool names:
```python
GenerativeUI(
@@ -117,17 +119,16 @@ GenerativeUI(
## 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.
+Generative UI needs `fastmcp[apps]`, which pulls in `prefab-ui`. The server-side Pyodide sandbox (for final validation) requires Deno — it installs automatically on first use.
-The streaming renderer loads Pyodide from CDN in the browser. The CSP is configured automatically by the provider — no manual setup needed.
+The streaming renderer loads Pyodide from CDN in the browser. The CSP is configured automatically by the provider — no manual setup.
-## Sandbox Limitations
+## 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`.
+The Pyodide sandbox includes the Python standard library and Prefab. External packages (NumPy, pandas, requests, etc.) are **not available** — the LLM's code must work with only built-in Python and Prefab. If the LLM imports something unavailable, the sandbox raises `ImportError`.
-## Next Steps
+## 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`
+- **[Interactive Tools](/apps/prefab)** — the component building blocks the LLM will use
+- **[Prefab component reference](https://prefab.prefect.io/docs/components)** — full component library
+- **[Development](/apps/development)** — preview generative tools locally with `fastmcp dev apps`
diff --git a/docs/apps/images/generative-ui.mp4 b/docs/apps/images/generative-ui.mp4
new file mode 100644
index 000000000..ca610181e
Binary files /dev/null and b/docs/apps/images/generative-ui.mp4 differ
diff --git a/docs/apps/low-level.mdx b/docs/apps/low-level.mdx
index adda74799..ccef52b0a 100644
--- a/docs/apps/low-level.mdx
+++ b/docs/apps/low-level.mdx
@@ -3,18 +3,17 @@ title: Custom HTML Apps
sidebarTitle: Custom HTML
description: Build apps with your own HTML, CSS, and JavaScript using the MCP Apps extension directly.
icon: code
-tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
-The [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) is an open protocol that lets tools return interactive UIs — an HTML page rendered in a sandboxed iframe inside the host client. [Prefab UI](/apps/prefab) builds on this protocol so you never have to think about it, but when you need full control — custom rendering, a specific JavaScript framework, maps, 3D, video — you can use the MCP Apps extension directly.
+Everything on this page is for when you want full control: your own HTML, your own JavaScript framework, a map library, a 3D viewer, custom video playback. [Interactive Tools](/apps/prefab) wrap the MCP Apps extension so you never have to think about it — this page is what you reach for when you need to think about it.
-This page covers how to write custom HTML apps and wire them up in FastMCP. You'll be working with the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) JavaScript SDK for host communication, and FastMCP's `AppConfig` for resource and CSP management.
+You'll be working with two things: the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) JavaScript SDK for host communication, and FastMCP's `AppConfig` for resources and CSP.
-## How It Works
+## How it works
An MCP App has two parts:
@@ -66,7 +65,7 @@ def my_tool() -> str:
return "result"
```
-### Tool Visibility
+### Tool visibility
The `visibility` field controls where a tool appears:
@@ -88,7 +87,7 @@ def refresh_data() -> str:
return fetch_latest()
```
-### AppConfig Fields
+### AppConfig fields
| Field | Type | Description |
|-------|------|-------------|
@@ -103,9 +102,9 @@ def refresh_data() -> str:
On **resources**, `resource_uri` and `visibility` must not be set — the resource *is* the UI. Use `AppConfig` on resources only for `csp`, `permissions`, and other display settings.
-## UI Resources
+## UI resources
-Resources using the `ui://` scheme are automatically served with the MIME type `text/html;profile=mcp-app`. You don't need to set this manually.
+Resources using the `ui://` scheme are automatically served with the MIME type `text/html;profile=mcp-app`. No need to set it manually.
```python
@mcp.resource("ui://my-app/view.html")
@@ -115,7 +114,7 @@ def my_view() -> str:
The HTML can be anything — a full single-page app, a simple display, or a complex interactive tool. The host renders it in a sandboxed iframe and establishes a `postMessage` channel for communication.
-### Writing the App HTML
+### Writing the app HTML
Your HTML app communicates with the host using the [`@modelcontextprotocol/ext-apps`](https://github.com/modelcontextprotocol/ext-apps) JavaScript SDK. The simplest approach is to load it from a CDN:
@@ -204,7 +203,7 @@ def my_view() -> str:
Hosts may or may not grant these permissions. Your app should use JavaScript feature detection as a fallback.
-## Example: QR Code Server
+## Example: a QR code server
This example creates a tool that generates QR codes and an app that renders them as images. It's based on the [official MCP Apps example](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server). Requires the `qrcode[pil]` package.
@@ -286,7 +285,7 @@ def view() -> str:
The tool generates a QR code as a base64 PNG. The resource loads the MCP Apps JS SDK from unpkg (declared in the CSP), listens for tool results, and renders the image. The host wires them together — when the LLM calls `generate_qr`, the QR code appears in an interactive frame inside the conversation.
-## Checking Client Support
+## Checking client support
Not all hosts support the Apps extension. You can check at runtime using the tool's [context](/servers/context):
diff --git a/docs/apps/overview.mdx b/docs/apps/overview.mdx
index cb3c7c1ea..ed04580f6 100644
--- a/docs/apps/overview.mdx
+++ b/docs/apps/overview.mdx
@@ -3,179 +3,70 @@ title: Apps
sidebarTitle: Overview
description: Give your tools interactive UIs rendered directly in the conversation.
icon: grid-2
-tag: NEW
+mode: center
---
import { VersionBadge } from '/snippets/version-badge.mdx'
+import PrefabPinWarning from '/snippets/prefab-pin-warning.mdx'
-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.
+A FastMCP app is a tool that returns an interactive UI instead of text. When the host calls it, the user sees a chart, a table, a form, or a whole dashboard rendered right inside the conversation, with working sort, search, tooltips, and state.
-
-
-
+
+
+
-FastMCP 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 dashboard above is a [Prefab](https://prefab.prefect.io) showcase — a taste of what you can deliver from a FastMCP tool. Every card, chart, slider, dialog, and carousel is a Python component. Build a composition like this, add `@mcp.tool(app=True)`, and the host renders it inside the conversation.
-
-The examples throughout the Apps docs require the `apps` extra:
+Under the hood, FastMCP builds on the [MCP Apps extension](https://modelcontextprotocol.io/docs/extensions/apps) and uses Prefab to describe UIs in Python.
```bash
pip install "fastmcp[apps]"
```
-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.
-
+## Pick your path
-## Which Approach?
+Four patterns cover almost everything you'd want to build. Most apps start with Interactive Tools; you only reach for the others when you've hit a specific limit.
-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.
+### [Interactive Tools](/apps/prefab) — start here
-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
-
-
-
-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:
+Add `app=True` to a tool and return a Prefab component. Charts, tables, dashboards, and client-side interactivity (toggles, tabs, filtering) all work without any server round-trips.
```python
-from prefab_ui.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 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} Revenue")
- BarChart(
- data=data,
- series=[ChartSeries(data_key="revenue", label="Revenue")],
- x_axis="quarter",
- )
-
- return PrefabApp(view=view)
+def team_directory() -> DataTable:
+ return DataTable(columns=[...], rows=employees, search=True)
```
-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.
+### [FastMCPApp](/apps/fastmcp-app) — when the UI calls back to the server
-See [Prefab Apps](/apps/prefab) for the full guide.
+Forms that save data, buttons that trigger backend work, search that hits a database. `FastMCPApp` manages the wiring between UI actions and backend tools, with stable tool identifiers that survive server composition.
-### FastMCPApp
+### [Generative UI](/apps/generative) — when the LLM writes the UI
-
-
-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:
+Register one provider and the model can write Prefab code tailored to the current data and request. The user watches the UI build up as the model generates it.
```python
-from 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.apps.generative import GenerativeUI
-
-mcp = FastMCP("Prefab Studio")
mcp.add_provider(GenerativeUI())
```
-See [Generative UI](/apps/generative) for the full guide, or the [provider reference](/apps/providers/generative) for configuration options.
+### [Custom HTML](/apps/low-level) — when you need full control
-### Custom HTML
+Write your own HTML, CSS, and JavaScript. Use a specific framework, drop in a map or 3D viewer, embed video. You're talking to the MCP Apps protocol directly.
-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.
+## What's next
-## 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
-```
+- **[Quickstart](/apps/quickstart)** — build a working app in a minute
+- **[Examples](/apps/examples)** — complete working servers you can run today
+- **[Providers](/apps/providers/approval)** — ready-made capabilities (approvals, choice pickers, file upload, forms) you add with one line
+- **[Development](/apps/development)** — preview app tools locally with `fastmcp dev apps`
diff --git a/docs/apps/prefab.mdx b/docs/apps/prefab.mdx
index 156767869..1bebc73de 100644
--- a/docs/apps/prefab.mdx
+++ b/docs/apps/prefab.mdx
@@ -1,321 +1,254 @@
---
-title: Prefab UI
-sidebarTitle: Prefab UI
-description: The component library behind FastMCP apps — charts, tables, dashboards, forms, and reactive displays.
+title: Interactive Tools
+sidebarTitle: Interactive Tools
+description: Turn your tools into interactive UIs with charts, tables, and dashboards.
icon: palette
tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
+import PrefabPinWarning from '/snippets/prefab-pin-warning.mdx'
-
-[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 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.
+
-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.
+Believe it or not, that dashboard is a FastMCP tool. The chart has tooltips. The table is sortable. The badges are styled by deal stage. The whole thing is about 40 lines of Python, and the user sees it right inside their conversation instead of a wall of JSON.
-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).
+The pattern behind every example on this page is the same: add `app=True` to your tool, build a UI with [Prefab](https://prefab.prefect.io) components, and return it as a `PrefabApp`. Prefab has [100+ components](https://prefab.prefect.io/docs/components), from data tables and charts to forms and progress bars. You compose them in Python; the host renders them as a live, interactive application.
-## Getting Started
+## Start with a table
-Here's a tool that returns a bar chart:
+Most tools return data the user wants to explore. A `DataTable` is often the smallest useful upgrade — your data goes from a JSON blob to a searchable, sortable table:
+
+
```python
-from prefab_ui.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 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} Revenue")
- BarChart(
- data=data,
- series=[ChartSeries(data_key="revenue", label="Revenue")],
- x_axis="quarter",
- )
-
- return PrefabApp(view=view)
-```
-
-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.
-
-## Layout and Components
-
-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, Row, Grid, Heading, Text, Muted, Badge,
- Card, CardContent, Separator,
-)
-
-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")
-```
-
-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.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("Flags")
-
-
-@mcp.tool(app=True)
-def feature_flags() -> PrefabApp:
- """Toggle feature flags with live preview."""
- with Column(gap=4, css_class="p-6") as view:
- Heading("Feature Flags")
- Switch(name="dark_mode", label="Dark Mode")
- Switch(name="beta", label="Beta Features")
-
- 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})
-```
-
-Three things to notice here:
-
-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.
-
-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(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.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 prefab_ui.components import DataTable, DataTableColumn
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"},
+def team_directory() -> DataTable:
+ """Browse the team directory."""
+ employees = [
+ {"name": "Alice Chen", "role": "Staff Engineer", "dept": "Platform"},
+ {"name": "Bob Martinez", "role": "Lead Designer", "dept": "Design"},
+ {"name": "Carol Johnson", "role": "Senior Engineer", "dept": "Platform"},
+ {"name": "David Kim", "role": "Product Manager", "dept": "Product"},
+ {"name": "Eva Mueller", "role": "Engineer", "dept": "Platform"},
+ {"name": "Frank Lee", "role": "Data Scientist", "dept": "ML"},
+ {"name": "Grace Park", "role": "Eng Manager", "dept": "Platform"},
]
- with Column(gap=4, css_class="p-6") as view:
- Heading("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
-
-mcp = FastMCP("Sales")
-
-
-@mcp.tool(app=True)
-def sales_overview(year: int) -> ToolResult:
- """Show sales data visually and summarize for the model."""
- data = get_sales_data(year)
- total = sum(row["revenue"] for row in data)
-
- with Column(gap=4, css_class="p-6") as view:
- Heading("Sales Overview")
- BarChart(data=data, series=[ChartSeries(data_key="revenue")])
-
- return ToolResult(
- content=f"Total revenue for {year}: ${total:,} across {len(data)} quarters",
- structured_content=view,
+ return DataTable(
+ columns=[
+ DataTableColumn(key="name", header="Name", sortable=True),
+ DataTableColumn(key="role", header="Role", sortable=True),
+ DataTableColumn(key="dept", header="Dept", sortable=True),
+ ],
+ rows=employees,
+ search=True,
)
```
-The user sees the chart. The LLM sees the summary string.
+That's it. Add `app=True`, return a Prefab component instead of raw dicts. FastMCP handles the rendering, sandboxing, and security. No wrapper class needed for simple cases like this.
-## Advanced
+## Add charts
-
-`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:
+When numbers tell a better story as a visual, swap in a chart. The API is the same: pass your data as a list of dicts, tell the chart which keys to plot.
+
+
+
+```python
+@mcp.tool(app=True)
+def quarterly_revenue(year: int) -> BarChart:
+ """Show quarterly revenue as a bar chart."""
+ data = [
+ {"quarter": "Q1", "revenue": 42000, "costs": 28000},
+ {"quarter": "Q2", "revenue": 51000, "costs": 31000},
+ {"quarter": "Q3", "revenue": 47000, "costs": 29000},
+ {"quarter": "Q4", "revenue": 63000, "costs": 35000},
+ ]
+
+ return BarChart(
+ data=data,
+ series=[
+ ChartSeries(data_key="revenue", label="Revenue"),
+ ChartSeries(data_key="costs", label="Costs"),
+ ],
+ x_axis="quarter",
+ show_legend=True,
+ )
+```
+
+Each `ChartSeries` plots a different key from the data. `BarChart`, `LineChart`, `AreaChart`, `PieChart`, `RadarChart`, and `RadialChart` all follow the same pattern. Hover over the bars to see tooltips.
+
+
+
+```python
+@mcp.tool(app=True)
+def ticket_breakdown() -> PieChart:
+ """Show open tickets by category."""
+ data = [
+ {"category": "Bug", "count": 42},
+ {"category": "Feature", "count": 28},
+ {"category": "Docs", "count": 15},
+ {"category": "Infra", "count": 10},
+ ]
+
+ return PieChart(
+ data=data,
+ data_key="count",
+ name_key="category",
+ inner_radius=50,
+ show_legend=True,
+ )
+```
+
+See the [Prefab chart docs](https://prefab.prefect.io/docs/components) for stacking, curves, custom colors, and more.
+
+## Compose a dashboard
+
+Tables and charts are useful on their own, but the real power comes from composing them. `Column` stacks children vertically, `Row` lays them out side by side, and `with` blocks establish nesting — the indentation is the layout.
+
+
+
+```python expandable
+@mcp.tool(app=True)
+def sales_dashboard() -> PrefabApp:
+ """Show sales KPIs, trends, and deals."""
+ monthly = [
+ {"month": "Jan", "revenue": 48200, "costs": 31000},
+ {"month": "Feb", "revenue": 52100, "costs": 32500},
+ {"month": "Mar", "revenue": 61800, "costs": 34200},
+ {"month": "Apr", "revenue": 58400, "costs": 33800},
+ ]
+ deals = [
+ {"account": "Acme Corp", "value": "$84,000", "stage": "Won"},
+ {"account": "Globex Inc", "value": "$52,000", "stage": "Negotiation"},
+ {"account": "Initech", "value": "$31,500", "stage": "Proposal"},
+ {"account": "Wayne Enterprises", "value": "$45,000", "stage": "Lost"},
+ ]
+
+ rows = [
+ {
+ "account": d["account"],
+ "value": d["value"],
+ "stage": Badge(
+ d["stage"],
+ variant="success" if d["stage"] == "Won"
+ else "destructive" if d["stage"] == "Lost"
+ else "secondary",
+ ),
+ }
+ for d in deals
+ ]
+
+ total = sum(m["revenue"] for m in monthly)
+
+ with PrefabApp() as app:
+ with Column(gap=4, css_class="p-6"):
+ with Row(gap=6):
+ Metric(label="Revenue (Q1-Q4)", value=f"${total:,}")
+ Metric(label="Deals", value=f"{len(deals)}")
+ BarChart(
+ data=monthly,
+ series=[
+ ChartSeries(data_key="revenue", label="Revenue"),
+ ChartSeries(data_key="costs", label="Costs"),
+ ],
+ x_axis="month",
+ show_legend=True,
+ )
+ Separator()
+ DataTable(
+ columns=[
+ DataTableColumn(key="account", header="Account", sortable=True),
+ DataTableColumn(key="value", header="Value", sortable=True),
+ DataTableColumn(key="stage", header="Stage"),
+ ],
+ rows=rows,
+ )
+
+ return app
+```
+
+Notice how `Badge` components can be placed inside table cells — any Prefab component works as a cell value, so you can put progress bars, icons, or buttons in your tables too.
+
+## Make it reactive
+
+Everything above renders once from the data your Python provides. But interactive tools can also respond to user input in real time, without any server round-trips. Prefab's state system lets components read and write client-side values, so the UI updates instantly as the user interacts with it.
+
+
+
+Try switching regions in the dropdown, and toggling the switch on and off.
+
+```python expandable
+from prefab_ui.rx import Rx
+
+@mcp.tool(app=True)
+def regional_sales() -> PrefabApp:
+ """Sales by region with a live filter."""
+ north = [
+ {"month": "Jan", "sales": 22000},
+ {"month": "Feb", "sales": 25500},
+ {"month": "Mar", "sales": 24200},
+ ]
+ south = [
+ {"month": "Jan", "sales": 5800},
+ {"month": "Feb", "sales": 6400},
+ {"month": "Mar", "sales": 5600},
+ ]
+ west = [
+ {"month": "Jan", "sales": 6000},
+ {"month": "Feb", "sales": 6000},
+ {"month": "Mar", "sales": 5600},
+ ]
+
+ with PrefabApp(
+ state={
+ "region": "north",
+ "north": north, "south": south, "west": west,
+ "show_target": True,
+ },
+ ) as app:
+ with Column(
+ gap=4,
+ css_class="p-6",
+ let={"data": "{{ region == 'south' ? south"
+ " : region == 'west' ? west"
+ " : north }}"},
+ ):
+ with Row(gap=4, align="center"):
+ with Select(name="region", css_class="w-40"):
+ SelectOption(value="north", label="North")
+ SelectOption(value="south", label="South")
+ SelectOption(value="west", label="West")
+ Switch(name="show_target", css_class="ml-auto")
+ Text("Show target", css_class="text-sm text-muted-foreground")
+ BarChart(
+ data=Rx("data"),
+ series=[ChartSeries(data_key="sales", label="Sales")],
+ x_axis="month",
+ )
+ with If(Rx("show_target")):
+ Metric(label="Q1 Target", value="$75,000")
+
+ return app
+```
+
+The `state` dict on `PrefabApp` declares initial values. The `Select` writes to the `region` key on every change. A `let` binding picks the matching dataset, and the chart re-renders. The `Switch` toggles a `Metric` on and off through `If(Rx("show_target"))`. All of this happens in the browser — no calls back to your server.
+
+`Rx` is a reactive reference: `Rx("region")` compiles to an expression the renderer evaluates live. It supports arithmetic, comparisons, formatting pipes (`.currency()`, `.percent()`), and ternary conditionals (`.then()`). For the full state system, see the [Prefab state docs](https://prefab.prefect.io/docs/concepts/state) and [expression docs](https://prefab.prefect.io/docs/concepts/expressions).
+
+## Content Security Policy
+
+Interactive tools render in a sandboxed iframe with a strict CSP. If your tool loads external resources — embedding iframes, fetching from APIs, loading scripts — add the required domains:
```python
from fastmcp.apps import PrefabAppConfig, ResourceCSP
@@ -327,40 +260,37 @@ 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.
-
+`PrefabAppConfig()` with no arguments is equivalent to `app=True`.
-
-If your return type annotation is a Prefab type — `PrefabApp`, `Component`, or unions containing them — FastMCP enables app rendering automatically, even without `app=True`:
+## Giving the LLM context
+
+By default, the LLM sees `"[Rendered Prefab UI]"` as the tool result. If the model needs to reason about the data, return a `ToolResult` with a text summary alongside the UI:
```python
-@mcp.tool
-def greet(name: str) -> PrefabApp:
- return PrefabApp(view=Heading(f"Hello, {name}!"))
-```
-
-Explicit `app=True` is recommended for clarity.
-
-
-
-Prefab tools and [custom HTML tools](/apps/low-level) coexist on the same server:
-
-```python
-from fastmcp.apps import AppConfig
+from fastmcp.tools import ToolResult
@mcp.tool(app=True)
-def team_directory() -> PrefabApp:
- ...
+def sales_overview(year: int) -> ToolResult:
+ """Show sales visually, summarize for the model."""
+ data = get_sales_data(year)
+ total = sum(row["revenue"] for row in data)
-@mcp.tool(app=AppConfig(resource_uri="ui://my-app/map.html"))
-def map_view() -> str:
- ...
+ with Column(gap=4, css_class="p-6") as view:
+ BarChart(data=data, series=[ChartSeries(data_key="revenue")])
+
+ return ToolResult(
+ content=f"Total revenue for {year}: ${total:,} across {len(data)} quarters",
+ structured_content=view,
+ )
```
-
-## Next Steps
+The user sees the chart. The model sees the summary.
-- **[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
+## Next steps
+
+- **[FastMCPApp](/apps/fastmcp-app)** — when your UI needs to call backend tools (forms, search, CRUD)
+- **[Generative UI](/apps/generative)** — let the LLM design the UI at runtime
+- **[Custom HTML](/apps/low-level)** — when Prefab isn't enough (maps, 3D, your own framework)
+- **[Examples](/apps/examples)** — complete working servers you can run today
+- **[Development](/apps/development)** — preview your tools locally with `fastmcp dev apps`
+- **[Prefab UI](https://prefab.prefect.io)** — full component reference with 100+ components, theming, and advanced patterns
diff --git a/docs/apps/providers/approval.mdx b/docs/apps/providers/approval.mdx
index 15b683d15..8ac7b8dd1 100644
--- a/docs/apps/providers/approval.mdx
+++ b/docs/apps/providers/approval.mdx
@@ -70,7 +70,7 @@ request_approval(
)
```
-## How It Works
+## How it works
When the user clicks a button, two things happen:
diff --git a/docs/apps/providers/choice.mdx b/docs/apps/providers/choice.mdx
index 71672a95c..c29c1b2bc 100644
--- a/docs/apps/providers/choice.mdx
+++ b/docs/apps/providers/choice.mdx
@@ -62,7 +62,7 @@ choose(
)
```
-## How It Works
+## How it works
Each option renders as a full-width button in a vertical stack. When the user clicks one:
diff --git a/docs/apps/providers/file-upload.mdx b/docs/apps/providers/file-upload.mdx
index 13cf2402e..b9709d946 100644
--- a/docs/apps/providers/file-upload.mdx
+++ b/docs/apps/providers/file-upload.mdx
@@ -49,7 +49,7 @@ FileUpload(
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
+## 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.
@@ -77,7 +77,7 @@ class SharedUpload(FileUpload):
return "__shared__"
```
-## Custom Storage
+## 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.
diff --git a/docs/apps/providers/form.mdx b/docs/apps/providers/form.mdx
index ca598f33f..e61dc0ce0 100644
--- a/docs/apps/providers/form.mdx
+++ b/docs/apps/providers/form.mdx
@@ -44,7 +44,7 @@ This registers two tools:
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
+## Field mapping
`FormInput` uses Prefab's `Form.from_model()`, which maps Pydantic types to form components:
@@ -89,7 +89,7 @@ FormInput(
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
+## Multiple forms
Add multiple providers for different models — each gets its own tool:
diff --git a/docs/apps/quickstart.mdx b/docs/apps/quickstart.mdx
index 91265f884..7b87d1981 100644
--- a/docs/apps/quickstart.mdx
+++ b/docs/apps/quickstart.mdx
@@ -1,7 +1,7 @@
---
title: Quickstart
sidebarTitle: Quickstart
-description: Build your first MCP app in under a minute.
+description: Build your first FastMCP app in under a minute.
icon: rocket
tag: NEW
---
@@ -10,33 +10,29 @@ 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.
+By the end of this page, you'll have a working tool that returns this:
-This tutorial builds a working app from scratch. Here's what you'll have in about a minute:
+
-
-
-
+A pie chart the user can hover, a table they can sort and search — and a single Python tool.
-## Setup
-
-Install FastMCP with the `apps` extra, which pulls in Prefab UI:
+## Install
```bash
pip install "fastmcp[apps]"
```
-## A Tool That Returns a UI
+The `apps` extra pulls in [Prefab](https://prefab.prefect.io), the Python component library used to build app UIs.
-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.
+## Write the tool
-Create `server.py`:
+Create `server.py`. The interesting parts: `app=True` tells FastMCP this tool renders a UI, and `with PrefabApp() as app:` is the canonical pattern for composing one.
```python server.py expandable
from collections import Counter
from prefab_ui.app import PrefabApp
-from prefab_ui.components import Column, Grid, Heading, DataTable, DataTableColumn
+from prefab_ui.components import Column, DataTable, DataTableColumn, Grid
from prefab_ui.components.charts import PieChart
from fastmcp import FastMCP
@@ -63,7 +59,6 @@ def team_directory() -> PrefabApp:
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,
@@ -84,40 +79,42 @@ def team_directory() -> PrefabApp:
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 reads top-to-bottom. `PrefabApp()` is the root; everything inside its `with` block becomes the UI. `Column` stacks children vertically, `Grid` lays them out in columns. `DataTable` takes rows and column definitions and gives you sort and search for free.
-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.
+`app=True` does the rest: it sets up the renderer resource, the content security policy, and the metadata that tells the host "this tool returns a UI." The host loads the result in a sandboxed iframe where the user can interact with it — all client-side, no round-trips.
-## Running It
+## Preview it
-FastMCP includes a dev server that renders your app tools in a browser, no MCP host needed:
+FastMCP ships 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:
+Open `http://localhost:8080`, pick `team_directory`, and try sorting columns and searching.
-
+
+## Make it reactive
+
+The UI above renders once from your Python. Prefab apps can also respond to user input live, without any server round-trips. The key concept is **state**: a client-side key-value store that components read from and write to.
+
+Click a row in the demo below to see a detail card appear:
+
+
+
+Add a few imports, give each member a couple more fields, wire up a click handler, and render a detail card when something's selected:
+
```python expandable server.py
from collections import Counter
from prefab_ui.actions import SetState
from prefab_ui.app import PrefabApp
from prefab_ui.components import (
- Card, CardContent, CardHeader, Column, Grid, H3, Heading, Muted,
- Row, DataTable, DataTableColumn, Badge, Small, Text,
+ Badge, Card, CardContent, CardHeader, Column, DataTable, DataTableColumn,
+ Grid, H3, Row, Small, Text,
)
from prefab_ui.components.charts import PieChart
from prefab_ui.components.control_flow import If
@@ -129,16 +126,12 @@ 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},
+ # ... more members ...
]
OFFICE_COUNTS = [
- {"office": office, "count": count}
- for office, count in Counter(m["office"] for m in MEMBERS).items()
+ {"office": o, "count": c}
+ for o, c in Counter(m["office"] for m in MEMBERS).items()
]
@@ -147,7 +140,6 @@ 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,
@@ -187,22 +179,18 @@ def team_directory() -> PrefabApp:
return app
```
-Three new ideas here:
+Three new ideas do all the work:
-**`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).
+- **`on_row_click=SetState("selected", Rx("$event"))`** — clicking a row writes its data into the `selected` state key. `$event` is the clicked row dict.
+- **`Rx("selected.name")`** — a reactive reference. It doesn't hold a Python value; it compiles to a browser-side expression that re-evaluates whenever `selected` changes, so `Text(Rx("selected.name"))` always shows the latest clicked name.
+- **`If(STATE.selected)`** — conditionally renders its body. Before any click, `selected` is `None` and the card stays hidden.
-**`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.
+The `state={"selected": None}` dict on `PrefabApp` sets the initial value. Everything else happens in the browser — no round-trips to your server when the user clicks.
-**`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.
+## Where to go next
-The `state` dict on `PrefabApp` sets initial values when the app loads. Run `fastmcp dev apps server.py` again and try clicking a row.
+You've built a tool that returns an interactive, reactive UI. This pattern covers a huge range of use cases: build a visualization, return it, and the user gets it rendered right in the conversation.
-## 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.
+- **[Interactive Tools](/apps/prefab)** — charts, tables, dashboards, reactive state, with live demos
+- **[FastMCPApp](/apps/fastmcp-app)** — when the UI needs to call back to your server (forms, search, CRUD)
+- **[Examples](/apps/examples)** — complete working servers you can run today
diff --git a/docs/changelog.mdx b/docs/changelog.mdx
index d55783e9f..949a11659 100644
--- a/docs/changelog.mdx
+++ b/docs/changelog.mdx
@@ -1947,7 +1947,7 @@ FastMCP 2.8.0 introduces powerful new ways to customize and control your MCP ser
### Tool Transformation
-The highlight of this release is first-class [**Tool Transformation**](/patterns/tool-transformation), a new feature that lets you create enhanced variations of existing tools. You can now easily rename arguments, hide parameters, modify descriptions, and even wrap tools with custom validation or post-processing logic—all without rewriting the original code. This makes it easier than ever to adapt generic tools for specific LLM use cases or to simplify complex APIs. Huge thanks to [@strawgate](https://github.com/strawgate) for partnering on this, starting with [#591](https://github.com/PrefectHQ/fastmcp/discussions/591) and [#599](https://github.com/PrefectHQ/fastmcp/pull/599) and continuing offline.
+The highlight of this release is first-class [**Tool Transformation**](/servers/transforms/tool-transformation), a new feature that lets you create enhanced variations of existing tools. You can now easily rename arguments, hide parameters, modify descriptions, and even wrap tools with custom validation or post-processing logic—all without rewriting the original code. This makes it easier than ever to adapt generic tools for specific LLM use cases or to simplify complex APIs. Huge thanks to [@strawgate](https://github.com/strawgate) for partnering on this, starting with [#591](https://github.com/PrefectHQ/fastmcp/discussions/591) and [#599](https://github.com/PrefectHQ/fastmcp/pull/599) and continuing offline.
### Component Control
This release also gives you more granular control over which components are exposed to clients. With new [**tag-based filtering**](/servers/server#tag-based-filtering), you can selectively enable or disable tools, resources, and prompts based on tags, perfect for managing different environments or user permissions. Complementing this, every component now supports being [programmatically enabled or disabled](/servers/tools#disabling-tools), offering dynamic control over your server's capabilities.
diff --git a/docs/clients/client-only-package.mdx b/docs/clients/client-only-package.mdx
new file mode 100644
index 000000000..fd0ca5010
--- /dev/null
+++ b/docs/clients/client-only-package.mdx
@@ -0,0 +1,89 @@
+---
+title: Client-Only Package
+description: Use FastMCP's client without installing the full server framework.
+icon: package
+---
+
+import { VersionBadge } from '/snippets/version-badge.mdx'
+
+
+
+FastMCP's full `fastmcp` package includes everything needed to build and run MCP servers, apps, proxies, and clients. If you are only embedding an MCP client in another framework, building your own LLM host, or testing MCP servers, you can install the smaller client-only package instead.
+
+```bash
+pip install "fastmcp-slim[client]"
+```
+
+The client-only package uses the `fastmcp` import namespace:
+
+```python
+from fastmcp import Client
+
+client = Client("https://example.com/mcp")
+```
+
+Use `fastmcp-slim[client]` when your code connects to MCP servers but does not define or run FastMCP servers itself. For example, framework authors can depend on `fastmcp-slim[client]` to provide MCP connectivity without requiring users to install the full FastMCP server stack.
+
+## Supported Usage
+
+Client-only installs support remote and subprocess transports:
+
+```python
+from fastmcp import Client
+
+# Remote MCP server
+http_client = Client("https://example.com/mcp")
+
+# Local MCP server over stdio
+stdio_client = Client("my_server.py")
+```
+
+Single-server MCP configuration works as well:
+
+```python
+from fastmcp import Client
+
+config = {
+ "mcpServers": {
+ "weather": {
+ "url": "https://weather.example.com/mcp"
+ }
+ }
+}
+
+client = Client(config)
+```
+
+Optional sampling handlers are available through the same extras as the full package:
+
+```bash
+pip install "fastmcp-slim[client,openai]"
+pip install "fastmcp-slim[client,anthropic]"
+pip install "fastmcp-slim[client,gemini]"
+```
+
+## When to Use the Full Package
+
+Install `fastmcp` when you need server-side FastMCP features:
+
+```bash
+pip install fastmcp
+```
+
+The full package remains the default for most users and continues to support the existing import style:
+
+```python
+from fastmcp import Client, FastMCP
+
+server = FastMCP("Example")
+client = Client(server)
+```
+
+Use the full package for:
+
+- defining or running FastMCP servers
+- in-memory clients connected directly to `FastMCP` server objects
+- multi-server MCP configurations
+- FastMCP apps, proxies, server auth, middleware, and other server-side features
+
+The `fastmcp-slim` package is intentionally narrower: it is for client-only consumers who want FastMCP's MCP client behavior without depending on the full framework.
diff --git a/docs/clients/sampling.mdx b/docs/clients/sampling.mdx
index a2e0e9942..6b845c3bb 100644
--- a/docs/clients/sampling.mdx
+++ b/docs/clients/sampling.mdx
@@ -155,11 +155,11 @@ Install the Anthropic handler with `pip install fastmcp[anthropic]`.
```python
from fastmcp import Client
-from fastmcp.client.sampling.handlers.google_genai import GoogleGenAISamplingHandler
+from fastmcp.client.sampling.handlers.google_genai import GoogleGenaiSamplingHandler
client = Client(
"my_mcp_server.py",
- sampling_handler=GoogleGenAISamplingHandler(default_model="gemini-2.0-flash"),
+ sampling_handler=GoogleGenaiSamplingHandler(default_model="gemini-2.0-flash"),
)
```
@@ -186,5 +186,5 @@ client = Client(
Tool execution happens on the server side. The client's role is to pass tools to the LLM and return the LLM's response (which may include tool use requests). The server then executes the tools and may send follow-up sampling requests with tool results.
-To implement a custom sampling handler, see the [handler source code](https://github.com/PrefectHQ/fastmcp/tree/main/src/fastmcp/client/sampling/handlers) as a reference.
+To implement a custom sampling handler, see the [handler source code](https://github.com/PrefectHQ/fastmcp/tree/main/fastmcp_slim/fastmcp/client/sampling/handlers) as a reference.
diff --git a/docs/deployment/http.mdx b/docs/deployment/http.mdx
index d2078db1b..54132eb0e 100644
--- a/docs/deployment/http.mdx
+++ b/docs/deployment/http.mdx
@@ -660,17 +660,17 @@ FASTMCP_STATELESS_HTTP=true uvicorn app:app --host 0.0.0.0 --port 8000 --workers
Production deployments should never hardcode sensitive information like API keys or authentication tokens. Instead, use environment variables to configure your server at runtime. This keeps your code secure and makes it easy to deploy the same code to different environments with different configurations.
-Here's an example using bearer token authentication (though OAuth is recommended for production):
+Here's an example using static token authentication for development (OAuth is recommended for production):
```python
import os
from fastmcp import FastMCP
-from fastmcp.server.auth import BearerTokenAuth
+from fastmcp.server.auth import StaticTokenVerifier
# Read configuration from environment
auth_token = os.environ.get("MCP_AUTH_TOKEN")
if auth_token:
- auth = BearerTokenAuth(token=auth_token)
+ auth = StaticTokenVerifier(tokens={auth_token: {"sub": "admin", "client_id": "cli"}})
mcp = FastMCP("Production Server", auth=auth)
else:
mcp = FastMCP("Production Server")
diff --git a/docs/deployment/prefect-horizon.mdx b/docs/deployment/prefect-horizon.mdx
index a7d053813..b22644181 100644
--- a/docs/deployment/prefect-horizon.mdx
+++ b/docs/deployment/prefect-horizon.mdx
@@ -54,7 +54,7 @@ There are just three steps to deploying a server to Horizon:
### Step 1: Select a Repository
-Visit [horizon.prefect.io](https://horizon.prefect.io) and sign in with your GitHub account. Connect your GitHub account to grant Horizon access to your repositories, then select the repo you want to deploy.
+Visit [horizon.prefect.io](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) and sign in with your GitHub account. Connect your GitHub account to grant Horizon access to your repositories, then select the repo you want to deploy.
diff --git a/docs/deployment/sandboxed-agents.mdx b/docs/deployment/sandboxed-agents.mdx
new file mode 100644
index 000000000..16191affb
--- /dev/null
+++ b/docs/deployment/sandboxed-agents.mdx
@@ -0,0 +1,262 @@
+---
+title: Sandboxed Agents
+sidebarTitle: Sandboxed Agents
+description: Expose MCP tools to isolated agents without giving the sandbox long-lived credentials.
+icon: box-open
+---
+
+This guide is for deployments where an agent runs inside an isolated container, subprocess, or remote worker and still needs MCP access. In that setup, the sandbox itself becomes part of your trust boundary.
+
+The core recommendation is simple: use FastMCP as the capability boundary. Run a remote FastMCP server, authenticate the sandbox with short-lived scoped credentials, and keep privileged credentials on the server side.
+
+## When to Use This Pattern
+
+This pattern is useful when:
+
+- your agent runs in an ephemeral container or subprocess
+- you do not want long-lived credentials inside that sandbox
+- you need per-run, per-tenant, or per-job scoping
+- the sandbox must call internal APIs, databases, or upstream MCP servers indirectly
+
+If you are building a local desktop integration, STDIO and normal local configuration may be enough. This guide is for cases where the sandbox is isolated enough that secret distribution, credential lifetimes, and privilege boundaries become part of the design.
+
+## What Changes in a Sandboxed Deployment
+
+A desktop MCP client usually runs on a developer's machine and launches local servers with configuration the developer controls. A sandboxed agent is different:
+
+- It often runs in an ephemeral container or subprocess.
+- Its filesystem may be inspected after the fact.
+- Its environment variables may be broader than you intend.
+- You may launch many sandboxes concurrently for different users, tenants, or jobs.
+
+That means convenience patterns that are acceptable locally become risky in sandboxes. Passing a GitHub token, database password, or cloud credentials directly into the sandbox creates a secret distribution problem you do not need to have.
+
+The safer approach is to make your FastMCP server the only component with privileged access and let the sandbox call it over MCP.
+
+## Recommended Architecture
+
+Use this shape by default:
+
+```mermaid
+flowchart LR
+ A["Sandboxed agent"] -->|"short-lived token"| B["FastMCP server"]
+ B --> C["internal APIs"]
+ B --> D["databases"]
+ B --> E["other MCP servers"]
+```
+
+The sandbox gets:
+
+- the MCP server URL
+- a short-lived token scoped to its job, tenant, or run
+- no long-lived upstream credentials
+
+The FastMCP server does the privileged work:
+
+- verifies the sandbox token
+- authorizes the request from token claims, scopes, or other server-side policy
+- exposes only the tools that sandbox should see
+- talks to internal APIs, databases, or upstream MCP servers on the sandbox's behalf
+
+The key design rule is simple:
+
+
+Give the sandbox capabilities, not credentials.
+
+
+With that boundary in place, the next questions are how the sandbox connects, how the server verifies and authorizes it, and how you design the tools the sandbox is allowed to call.
+
+## Prefer HTTP for Sandboxed Agents
+
+For sandboxes, prefer a remote HTTP server over a local STDIO server.
+
+STDIO is still excellent for local development, but a remote HTTP server is usually the better production boundary for sandboxed agents because:
+
+- authentication is explicit
+- the server lifecycle is independent from the sandbox lifecycle
+- secrets stay on the server
+- one deployment can safely serve many sandboxes
+- auditing and revocation happen in one place
+
+This means the sandbox should connect as a client:
+
+```python
+from fastmcp import Client
+from fastmcp.client.auth import BearerAuth
+
+client = Client(
+ "https://sandbox-tools.example.com/mcp",
+ auth=BearerAuth("short-lived-sandbox-token"),
+)
+```
+
+And your FastMCP server should run remotely:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("Sandbox Tools")
+
+if __name__ == "__main__":
+ mcp.run(transport="http", host="0.0.0.0", port=8000)
+```
+
+For production transport setup, see [HTTP Deployment](/deployment/http).
+
+## Use Short-Lived, Scoped Credentials
+
+For sandboxed agents, it is usually cleaner to issue credentials for the sandbox session than to place long-lived upstream credentials directly inside the container.
+
+In practice, that usually means issuing a short-lived bearer token for each sandbox, run, or tenant and validating it on your FastMCP server with a token verifier.
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+auth = JWTVerifier(
+ jwks_uri="https://auth.example.com/.well-known/jwks.json",
+ issuer="https://auth.example.com",
+ audience="sandbox-mcp",
+)
+
+mcp = FastMCP("Sandbox Tools", auth=auth)
+```
+
+The token should identify the sandbox's scope. Depending on your system, it may represent a job, a tenant, a run, or a user-authorized session. Useful claims often include:
+
+- sandbox or run id
+- tenant or installation id
+- user or actor id when applicable
+- expiration
+- optional capability scopes
+
+Avoid shared static tokens across many sandboxes. If one sandbox token leaks, you want the blast radius to be small and the lifetime to be short.
+
+Token verification is only one half of the boundary. Authorization still belongs on the FastMCP server: use scopes, claims, middleware, or custom auth checks to decide which tools and resources that sandbox can actually access.
+
+For example, you can verify the token globally and still require a narrower scope on a specific tool:
+
+```python
+from fastmcp import FastMCP
+from fastmcp.server.auth import require_scopes
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+
+auth = JWTVerifier(
+ jwks_uri="https://auth.example.com/.well-known/jwks.json",
+ issuer="https://auth.example.com",
+ audience="sandbox-mcp",
+)
+
+mcp = FastMCP("Sandbox Tools", auth=auth)
+
+@mcp.tool(auth=require_scopes("write:summary"))
+def write_summary(content: str) -> str:
+ return f"Stored summary with {len(content)} characters"
+```
+
+For validation patterns, see [Token Verification](/servers/auth/token-verification). For policy enforcement, see [Authorization](/servers/authorization).
+
+## Expose Capabilities, Not Raw Access
+
+The sandbox should not need:
+
+- GitHub app private keys
+- database passwords
+- upstream OAuth client secrets
+- cloud provider credentials
+
+Instead, expose MCP tools that perform privileged work on the server side.
+
+Good sandbox-facing tools tend to look like this:
+
+- `get_recent_updates`
+- `write_summary`
+- `fetch_repo_context`
+- `publish_review_comment`
+
+These tools describe the capability the sandbox needs, not the low-level credentialed action required to perform it.
+
+That distinction matters. A tool like `write_summary` lets the server decide where and how to persist the summary. A tool like `run_sql` or `call_internal_api` pushes privilege and policy into the sandbox where they are much harder to control.
+
+Sandboxed agents behave best when those tools are narrow and structured:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("Sandbox Tools")
+
+@mcp.tool
+def write_summary(content: str) -> str:
+ """Store the final summary for the current run."""
+ return f"Stored summary with {len(content)} characters"
+
+@mcp.tool
+def publish_review_comment(pr_number: int, body: str) -> str:
+ """Queue a review comment for a specific pull request."""
+ return f"Queued comment for PR #{pr_number}"
+```
+
+These are easier to audit, easier to authorize, and easier for agents to use reliably than a broad catch-all tool like `mutate_state(kind: str, payload: dict)`.
+
+Narrow tools also let you express different policies per tool instead of creating one large privileged escape hatch.
+
+## Use a Proxy When Upstream Systems Are More Privileged
+
+If the sandbox needs access to other MCP servers or internal systems, put FastMCP in front of them instead of forwarding secrets into the sandbox.
+
+This is where proxying becomes useful. Your public-facing FastMCP server can authenticate the sandbox, then forward allowed capabilities to upstream systems with stronger credentials.
+
+Typical examples:
+
+- a sandbox-safe MCP gateway in front of internal MCP servers
+- a FastMCP layer in front of internal HTTP APIs
+- a job-scoped server that fronts a Git provider, issue tracker, or storage system
+
+If the upstream system is itself an MCP server, FastMCP's proxy support is a natural fit. See [MCP Proxy](/servers/providers/proxy).
+
+## mcp.json for Sandboxed Clients
+
+If your sandboxed agent is configured through `mcp.json`, keep that configuration minimal. Point it at the remote FastMCP server and pass only the values the sandbox actually needs.
+
+```json
+{
+ "mcpServers": {
+ "sandbox-tools": {
+ "url": "https://sandbox-tools.example.com/mcp",
+ "transport": "http"
+ }
+ }
+}
+```
+
+In many systems, authentication is injected by the launcher or environment rather than hardcoded in `mcp.json`. That is usually the right tradeoff for sandboxes. Avoid baking long-lived credentials directly into generated config files, and avoid treating `mcp.json` as the place where secret material should live.
+
+That is all this section needs to do: tell the sandbox where the server lives. Keep auth and secret handling elsewhere.
+
+For configuration details, see [MCP.json](/integrations/mcp-json-configuration).
+
+## Common Mistakes
+
+The same few mistakes show up again and again in sandboxed deployments:
+
+- passing long-lived API keys directly into the sandbox
+- treating helper scripts in the sandbox as a security boundary
+- exposing broad mutation tools instead of narrow capabilities
+- using one shared token for every sandbox
+- relying on STDIO inheritance for configuration in production
+
+Each of these works at first. Each becomes painful once you have multiple tenants, multiple jobs, or an incident that requires revoking access quickly.
+
+## Production Checklist
+
+Before shipping a sandbox-facing FastMCP server, check these:
+
+- The sandbox connects over HTTP, not with privileged local credentials.
+- Tokens are short-lived and scoped to a run, tenant, or job.
+- The FastMCP server verifies tokens on every request.
+- Long-lived secrets stay on the server side.
+- Tools are narrow, explicit, and structured.
+- Upstream privileged systems sit behind the FastMCP server or proxy.
+- Revocation and audit live at the server boundary, not inside the sandbox.
+
+If you adopt those defaults, sandbox support stops being a special case and becomes a normal deployment pattern: isolated workers talk to a constrained FastMCP surface, and the server handles the privileged parts centrally.
diff --git a/docs/development/tests.mdx b/docs/development/tests.mdx
index 6a9973fe8..4653368be 100644
--- a/docs/development/tests.mdx
+++ b/docs/development/tests.mdx
@@ -33,7 +33,7 @@ Tests should complete in under 1 second unless marked as integration tests. This
### Test Organization
-Our test organization mirrors the `src/` directory structure, creating a predictable mapping between code and tests. When you're working on `src/fastmcp/server/auth.py`, you'll find its tests in `tests/server/test_auth.py`. In rare cases tests are split further - for example, the OpenAPI tests are so comprehensive they're split across multiple files.
+Our test organization mirrors the source package structure, creating a predictable mapping between code and tests. When you're working on `fastmcp_slim/fastmcp/server/auth.py`, you'll find its tests in `tests/server/test_auth.py`. In rare cases tests are split further - for example, the OpenAPI tests are so comprehensive they're split across multiple files.
### Test Markers
@@ -393,4 +393,4 @@ just docs
mintlify dev
```
-The local server watches for changes and automatically refreshes. This preview catches formatting issues and helps you see documentation as users will experience it.
\ No newline at end of file
+The local server watches for changes and automatically refreshes. This preview catches formatting issues and helps you see documentation as users will experience it.
diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/development/v3-notes/v3-features.mdx
index da9fc87db..3d656248a 100644
--- a/docs/development/v3-notes/v3-features.mdx
+++ b/docs/development/v3-notes/v3-features.mdx
@@ -29,7 +29,7 @@ FastMCP now includes a sampling handler for Google's Gemini models ([#2977](http
```python
from fastmcp import Client
-from fastmcp.client.sampling.handlers import GoogleGenaiSamplingHandler
+from fastmcp.client.sampling.handlers.google_genai import GoogleGenaiSamplingHandler
from google.genai import Client as GoogleGenaiClient
# Initialize the handler
@@ -386,7 +386,7 @@ Support for [MCP Apps](https://modelcontextprotocol.io/specification/2025-06-18/
```python
from fastmcp import FastMCP
-from fastmcp.server.apps import AppConfig, ResourceCSP, ResourcePermissions
+from fastmcp.apps import AppConfig, ResourceCSP, ResourcePermissions
mcp = FastMCP("My Server")
@@ -421,7 +421,7 @@ The `app=` parameter accepts `True` (enable with defaults), an `AppConfig` insta
```python
from fastmcp import Context
-from fastmcp.server.apps import AppConfig, UI_EXTENSION_ID
+from fastmcp.apps import AppConfig, UI_EXTENSION_ID
@mcp.tool(app=AppConfig(resource_uri="ui://dashboard"))
async def dashboard(ctx: Context) -> dict:
@@ -444,7 +444,7 @@ async def dashboard(ctx: Context) -> dict:
**Future phases** will add a component DSL for building UIs declaratively, an in-repo renderer, and a `FastMCPApp` class.
-Implementation: `src/fastmcp/server/apps.py` (models and constants), with integration points in `server.py` (decorator parameters), `low_level.py` (extension advertisement), and `context.py` (`client_supports_extension` method).
+Implementation: `fastmcp_slim/fastmcp/server/apps.py` (models and constants), with integration points in `server.py` (decorator parameters), `low_level.py` (extension advertisement), and `context.py` (`client_supports_extension` method).
---
@@ -454,7 +454,7 @@ Implementation: `src/fastmcp/server/apps.py` (models and constants), with integr
v3.0 introduces a provider-based component system that replaces v2's static-only registration ([#2622](https://github.com/PrefectHQ/fastmcp/pull/2622)). Providers dynamically source tools, resources, templates, and prompts at runtime.
-**Core abstraction** (`src/fastmcp/server/providers/base.py`):
+**Core abstraction** (`fastmcp_slim/fastmcp/server/providers/base.py`):
```python
class Provider:
async def list_tools(self) -> Sequence[Tool]: ...
@@ -474,7 +474,7 @@ Providers support:
### LocalProvider
-`LocalProvider` (`src/fastmcp/server/providers/local_provider.py`) manages components registered via decorators. Can be used standalone and attached to multiple servers:
+`LocalProvider` (`fastmcp_slim/fastmcp/server/providers/local_provider.py`) manages components registered via decorators. Can be used standalone and attached to multiple servers:
```python
from fastmcp.server.providers import LocalProvider
@@ -492,7 +492,7 @@ server2 = FastMCP("Server2", providers=[provider])
### ProxyProvider
-`ProxyProvider` (`src/fastmcp/server/providers/proxy.py`) proxies components from remote MCP servers via a client factory. Used by `create_proxy()` and `FastMCP.mount()` for remote server integration.
+`ProxyProvider` (`fastmcp_slim/fastmcp/server/providers/proxy.py`) proxies components from remote MCP servers via a client factory. Used by `create_proxy()` and `FastMCP.mount()` for remote server integration.
```python
from fastmcp.server import create_proxy
@@ -503,7 +503,7 @@ server = create_proxy("http://remote-server/mcp")
### OpenAPIProvider
-`OpenAPIProvider` (`src/fastmcp/server/providers/openapi/provider.py`) creates MCP components from OpenAPI specifications. Routes map HTTP operations to tools, resources, or templates based on configurable rules.
+`OpenAPIProvider` (`fastmcp_slim/fastmcp/server/providers/openapi/provider.py`) creates MCP components from OpenAPI specifications. Routes map HTTP operations to tools, resources, or templates based on configurable rules.
```python
from fastmcp.server.providers.openapi import OpenAPIProvider
@@ -523,7 +523,7 @@ Features:
### FastMCPProvider
-`FastMCPProvider` (`src/fastmcp/server/providers/fastmcp_provider.py`) wraps a FastMCP server to enable mounting one server onto another. Components delegate execution through the wrapped server's middleware chain.
+`FastMCPProvider` (`fastmcp_slim/fastmcp/server/providers/fastmcp_provider.py`) wraps a FastMCP server to enable mounting one server onto another. Components delegate execution through the wrapped server's middleware chain.
```python
from fastmcp import FastMCP
@@ -548,7 +548,7 @@ main.add_provider(provider)
Transforms modify components (tools, resources, prompts) as they flow from providers to clients ([#2836](https://github.com/PrefectHQ/fastmcp/pull/2836)). They use a middleware pattern where each transform receives a `call_next` callable to continue the chain.
-**Built-in transforms** (`src/fastmcp/server/transforms/`):
+**Built-in transforms** (`fastmcp_slim/fastmcp/server/transforms/`):
- `Namespace` - adds prefixes to names (`tool` → `api_tool`) and path segments to URIs (`data://x` → `data://api/x`)
- `ToolTransform` - modifies tool schemas (rename, description, tags, argument transforms)
@@ -575,15 +575,14 @@ provider.add_transform(ToolTransform({
```python
from collections.abc import Sequence
-from fastmcp.server.transforms import Transform, ListToolsNext, GetToolNext
+from fastmcp.server.transforms import Transform, GetToolNext
from fastmcp.tools import Tool
class TagFilter(Transform):
def __init__(self, required_tags: set[str]):
self.required_tags = required_tags
- async def list_tools(self, call_next: ListToolsNext) -> Sequence[Tool]:
- tools = await call_next() # Get tools from downstream
+ async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]:
return [t for t in tools if t.tags & self.required_tags]
async def get_tool(self, name: str, call_next: GetToolNext) -> Tool | None:
@@ -874,7 +873,7 @@ v3.0 introduces type-safe result classes that provide explicit control over comp
#### ToolResult
-`ToolResult` (`src/fastmcp/tools/tool.py:79`) provides structured tool responses:
+`ToolResult` (`fastmcp_slim/fastmcp/tools/tool.py:79`) provides structured tool responses:
```python
from fastmcp.tools import ToolResult
@@ -895,7 +894,7 @@ Fields:
#### ResourceResult
-`ResourceResult` (`src/fastmcp/resources/resource.py:117`) provides structured resource responses:
+`ResourceResult` (`fastmcp_slim/fastmcp/resources/resource.py:117`) provides structured resource responses:
```python
from fastmcp.resources import ResourceResult, ResourceContent
@@ -915,7 +914,7 @@ Accepts strings, bytes, or `list[ResourceContent]` for flexible content handling
#### PromptResult
-`PromptResult` (`src/fastmcp/prompts/prompt.py:109`) provides structured prompt responses:
+`PromptResult` (`fastmcp_slim/fastmcp/prompts/prompt.py:109`) provides structured prompt responses:
```python
from fastmcp.prompts import PromptResult, Message
@@ -937,7 +936,7 @@ def conversation() -> PromptResult:
v3.0 implements MCP SEP-1686 for background task execution via Docket integration.
-**Configuration** (`src/fastmcp/server/tasks/config.py`):
+**Configuration** (`fastmcp_slim/fastmcp/server/tasks/config.py`):
```python
from fastmcp.server.tasks import TaskConfig
@@ -1013,7 +1012,7 @@ fastmcp run server.py --reload --reload-dir ./src --reload-dir ./lib
fastmcp run server.py --reload --transport http --port 8080
```
-Implementation (`src/fastmcp/cli/run.py`):
+Implementation (`fastmcp_slim/fastmcp/cli/run.py`):
- Uses `watchfiles` for efficient file monitoring
- Runs server as subprocess for clean restarts
- Stateless mode for seamless reconnection after restart
diff --git a/docs/docs.json b/docs/docs.json
index 747899efa..d4a131679 100644
--- a/docs/docs.json
+++ b/docs/docs.json
@@ -12,7 +12,7 @@
"decoration": "gradient"
},
"banner": {
- "content": "Meet [Prefect Horizon](https://prefect.io/horizon?utm_source=gofastmcp&utm_medium=docs&utm_campaign=docs_banner&utm_content=sitewide_banner), the enterprise MCP gateway built by the team behind FastMCP"
+ "content": "Deploy FastMCP servers for free on [Prefect Horizon](https://www.prefect.io/horizon)"
},
"colors": {
"dark": "#f72585",
@@ -28,7 +28,7 @@
"description": "The fast, Pythonic way to build MCP servers and clients.",
"errors": {
"404": {
- "description": "You\u2019ve wandered outside the context.",
+ "description": "You’ve wandered outside the context.",
"redirect": false,
"title": "Don't panic."
}
@@ -108,30 +108,22 @@
},
{
"collapsed": true,
- "group": "Features",
- "icon": "stars",
+ "group": "Working with Tools",
+ "icon": "wand-magic-sparkles",
"pages": [
- "servers/tasks",
- "servers/composition",
- "servers/dependency-injection",
- "servers/elicitation",
- "servers/icons",
- "servers/lifespan",
- "servers/logging",
- "servers/middleware",
- "servers/pagination",
- "servers/progress",
- "servers/sampling",
- "servers/storage-backends",
- "servers/telemetry",
- "servers/testing",
- "servers/versioning"
- ],
- "tag": "UPDATED"
+ "servers/transforms/transforms",
+ "servers/transforms/tool-transformation",
+ "servers/transforms/code-mode",
+ "servers/transforms/tool-search",
+ "servers/transforms/namespace",
+ "servers/visibility",
+ "servers/transforms/resources-as-tools",
+ "servers/transforms/prompts-as-tools"
+ ]
},
{
"collapsed": true,
- "group": "Providers",
+ "group": "MCP Providers",
"icon": "layer-group",
"pages": [
"servers/providers/overview",
@@ -139,22 +131,34 @@
"servers/providers/filesystem",
"servers/providers/proxy",
"servers/providers/skills",
+ "servers/composition",
"servers/providers/custom"
]
},
{
"collapsed": true,
- "group": "Transforms",
- "icon": "wand-magic-sparkles",
+ "group": "Interactivity",
+ "icon": "comments",
"pages": [
- "servers/transforms/transforms",
- "servers/transforms/namespace",
- "servers/transforms/tool-transformation",
- "servers/visibility",
- "servers/transforms/code-mode",
- "servers/transforms/tool-search",
- "servers/transforms/resources-as-tools",
- "servers/transforms/prompts-as-tools"
+ "servers/elicitation",
+ "servers/sampling",
+ "servers/progress",
+ "servers/logging",
+ "servers/pagination",
+ "servers/icons"
+ ]
+ },
+ {
+ "collapsed": true,
+ "group": "Extensibility",
+ "icon": "puzzle-piece",
+ "pages": [
+ "servers/middleware",
+ "servers/dependency-injection",
+ "servers/lifespan",
+ "servers/storage-backends",
+ "servers/tasks",
+ "servers/versioning"
]
},
{
@@ -186,8 +190,11 @@
"pages": [
"deployment/running-server",
"deployment/http",
+ "deployment/sandboxed-agents",
"deployment/prefect-horizon",
- "deployment/server-configuration"
+ "deployment/server-configuration",
+ "servers/testing",
+ "servers/telemetry"
]
}
]
@@ -197,42 +204,30 @@
"pages": [
"apps/overview",
"apps/quickstart",
- "apps/examples",
+ "apps/fastmcp-app",
+ "apps/prefab",
+ "apps/generative",
+ "apps/low-level",
{
"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",
+ "group": "Reference",
+ "icon": "book",
"pages": [
+ {
+ "collapsed": true,
+ "group": "Prefab Providers",
+ "icon": "cube",
+ "pages": [
+ "apps/providers/approval",
+ "apps/providers/choice",
+ "apps/providers/file-upload",
+ "apps/providers/form"
+ ]
+ },
"apps/development",
- "apps/architecture",
- "apps/low-level"
- ],
- "tag": "NEW"
+ "apps/examples",
+ "apps/architecture"
+ ]
}
]
},
@@ -240,29 +235,23 @@
"group": "Clients",
"pages": [
"clients/client",
+ "clients/client-only-package",
"clients/transports",
{
"collapsed": true,
- "group": "Core Operations",
+ "group": "Operations",
"icon": "toolbox",
"pages": [
"clients/tools",
"clients/resources",
- "clients/prompts"
- ]
- },
- {
- "collapsed": true,
- "group": "Handlers",
- "icon": "hand",
- "pages": [
- "clients/notifications",
+ "clients/prompts",
"clients/sampling",
"clients/elicitation",
"clients/tasks",
"clients/progress",
"clients/logging",
- "clients/roots"
+ "clients/roots",
+ "clients/notifications"
],
"tag": "UPDATED"
},
@@ -296,6 +285,7 @@
"integrations/eunomia-authorization",
"integrations/github",
"integrations/google",
+ "integrations/keycloak",
"integrations/oci",
"integrations/permit",
"integrations/propelauth",
@@ -333,28 +323,31 @@
"pages": [
"integrations/anthropic",
"integrations/gemini",
- "integrations/openai"
+ "integrations/openai",
+ "integrations/pydantic-ai"
]
},
"integrations/mcp-json-configuration"
]
},
- {
- "group": "CLI",
- "pages": [
- "cli/overview",
- "cli/running",
- "cli/install-mcp",
- "cli/inspecting",
- "cli/client",
- "cli/generate-cli",
- "cli/auth"
- ]
- },
{
"group": "More",
"pages": [
"more/settings",
+ {
+ "collapsed": true,
+ "group": "CLI",
+ "icon": "terminal",
+ "pages": [
+ "cli/overview",
+ "cli/running",
+ "cli/install-mcp",
+ "cli/inspecting",
+ "cli/client",
+ "cli/generate-cli",
+ "cli/auth"
+ ]
+ },
{
"collapsed": true,
"group": "Upgrading",
@@ -395,427 +388,9 @@
{
"anchor": "Python SDK",
"icon": "python",
- "pages": [
- "python-sdk/fastmcp-decorators",
- "python-sdk/fastmcp-dependencies",
- "python-sdk/fastmcp-exceptions",
- "python-sdk/fastmcp-mcp_config",
- "python-sdk/fastmcp-settings",
- "python-sdk/fastmcp-telemetry",
- "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",
- "python-sdk/fastmcp-cli-client",
- "python-sdk/fastmcp-cli-discovery",
- "python-sdk/fastmcp-cli-generate",
- {
- "group": "install",
- "pages": [
- "python-sdk/fastmcp-cli-install-__init__",
- "python-sdk/fastmcp-cli-install-claude_code",
- "python-sdk/fastmcp-cli-install-claude_desktop",
- "python-sdk/fastmcp-cli-install-cursor",
- "python-sdk/fastmcp-cli-install-gemini_cli",
- "python-sdk/fastmcp-cli-install-goose",
- "python-sdk/fastmcp-cli-install-mcp_json",
- "python-sdk/fastmcp-cli-install-shared",
- "python-sdk/fastmcp-cli-install-stdio"
- ]
- },
- "python-sdk/fastmcp-cli-run",
- "python-sdk/fastmcp-cli-tasks"
- ]
- },
- {
- "group": "fastmcp.client",
- "pages": [
- "python-sdk/fastmcp-client-__init__",
- {
- "group": "auth",
- "pages": [
- "python-sdk/fastmcp-client-auth-__init__",
- "python-sdk/fastmcp-client-auth-bearer",
- "python-sdk/fastmcp-client-auth-oauth"
- ]
- },
- "python-sdk/fastmcp-client-client",
- "python-sdk/fastmcp-client-elicitation",
- "python-sdk/fastmcp-client-logging",
- "python-sdk/fastmcp-client-messages",
- {
- "group": "mixins",
- "pages": [
- "python-sdk/fastmcp-client-mixins-__init__",
- "python-sdk/fastmcp-client-mixins-prompts",
- "python-sdk/fastmcp-client-mixins-resources",
- "python-sdk/fastmcp-client-mixins-task_management",
- "python-sdk/fastmcp-client-mixins-tools"
- ]
- },
- "python-sdk/fastmcp-client-oauth_callback",
- "python-sdk/fastmcp-client-progress",
- "python-sdk/fastmcp-client-roots",
- {
- "group": "sampling",
- "pages": [
- "python-sdk/fastmcp-client-sampling-__init__",
- {
- "group": "handlers",
- "pages": [
- "python-sdk/fastmcp-client-sampling-handlers-__init__",
- "python-sdk/fastmcp-client-sampling-handlers-anthropic",
- "python-sdk/fastmcp-client-sampling-handlers-google_genai",
- "python-sdk/fastmcp-client-sampling-handlers-openai"
- ]
- }
- ]
- },
- "python-sdk/fastmcp-client-tasks",
- "python-sdk/fastmcp-client-telemetry",
- {
- "group": "transports",
- "pages": [
- "python-sdk/fastmcp-client-transports-__init__",
- "python-sdk/fastmcp-client-transports-base",
- "python-sdk/fastmcp-client-transports-config",
- "python-sdk/fastmcp-client-transports-http",
- "python-sdk/fastmcp-client-transports-inference",
- "python-sdk/fastmcp-client-transports-memory",
- "python-sdk/fastmcp-client-transports-sse",
- "python-sdk/fastmcp-client-transports-stdio"
- ]
- }
- ]
- },
- {
- "group": "fastmcp.experimental",
- "pages": [
- "python-sdk/fastmcp-experimental-__init__",
- {
- "group": "sampling",
- "pages": [
- "python-sdk/fastmcp-experimental-sampling-__init__",
- "python-sdk/fastmcp-experimental-sampling-handlers"
- ]
- },
- {
- "group": "transforms",
- "pages": [
- "python-sdk/fastmcp-experimental-transforms-__init__",
- "python-sdk/fastmcp-experimental-transforms-code_mode"
- ]
- }
- ]
- },
- {
- "group": "fastmcp.prompts",
- "pages": [
- "python-sdk/fastmcp-prompts-__init__",
- "python-sdk/fastmcp-prompts-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-template",
- "python-sdk/fastmcp-resources-types"
- ]
- },
- {
- "group": "fastmcp.server",
- "pages": [
- "python-sdk/fastmcp-server-__init__",
- "python-sdk/fastmcp-server-app",
- "python-sdk/fastmcp-server-apps",
- {
- "group": "auth",
- "pages": [
- "python-sdk/fastmcp-server-auth-__init__",
- "python-sdk/fastmcp-server-auth-auth",
- "python-sdk/fastmcp-server-auth-authorization",
- "python-sdk/fastmcp-server-auth-cimd",
- "python-sdk/fastmcp-server-auth-jwt_issuer",
- "python-sdk/fastmcp-server-auth-middleware",
- {
- "group": "oauth_proxy",
- "pages": [
- "python-sdk/fastmcp-server-auth-oauth_proxy-__init__",
- "python-sdk/fastmcp-server-auth-oauth_proxy-consent",
- "python-sdk/fastmcp-server-auth-oauth_proxy-models",
- "python-sdk/fastmcp-server-auth-oauth_proxy-proxy",
- "python-sdk/fastmcp-server-auth-oauth_proxy-ui"
- ]
- },
- "python-sdk/fastmcp-server-auth-oidc_proxy",
- {
- "group": "providers",
- "pages": [
- "python-sdk/fastmcp-server-auth-providers-__init__",
- "python-sdk/fastmcp-server-auth-providers-auth0",
- "python-sdk/fastmcp-server-auth-providers-aws",
- "python-sdk/fastmcp-server-auth-providers-azure",
- "python-sdk/fastmcp-server-auth-providers-clerk",
- "python-sdk/fastmcp-server-auth-providers-debug",
- "python-sdk/fastmcp-server-auth-providers-descope",
- "python-sdk/fastmcp-server-auth-providers-discord",
- "python-sdk/fastmcp-server-auth-providers-github",
- "python-sdk/fastmcp-server-auth-providers-google",
- "python-sdk/fastmcp-server-auth-providers-in_memory",
- "python-sdk/fastmcp-server-auth-providers-introspection",
- "python-sdk/fastmcp-server-auth-providers-jwt",
- "python-sdk/fastmcp-server-auth-providers-oci",
- "python-sdk/fastmcp-server-auth-providers-propelauth",
- "python-sdk/fastmcp-server-auth-providers-scalekit",
- "python-sdk/fastmcp-server-auth-providers-supabase",
- "python-sdk/fastmcp-server-auth-providers-workos"
- ]
- },
- "python-sdk/fastmcp-server-auth-redirect_validation",
- "python-sdk/fastmcp-server-auth-ssrf"
- ]
- },
- "python-sdk/fastmcp-server-context",
- "python-sdk/fastmcp-server-dependencies",
- "python-sdk/fastmcp-server-elicitation",
- "python-sdk/fastmcp-server-event_store",
- "python-sdk/fastmcp-server-http",
- "python-sdk/fastmcp-server-lifespan",
- "python-sdk/fastmcp-server-low_level",
- {
- "group": "middleware",
- "pages": [
- "python-sdk/fastmcp-server-middleware-__init__",
- "python-sdk/fastmcp-server-middleware-authorization",
- "python-sdk/fastmcp-server-middleware-caching",
- "python-sdk/fastmcp-server-middleware-dereference",
- "python-sdk/fastmcp-server-middleware-error_handling",
- "python-sdk/fastmcp-server-middleware-logging",
- "python-sdk/fastmcp-server-middleware-middleware",
- "python-sdk/fastmcp-server-middleware-ping",
- "python-sdk/fastmcp-server-middleware-rate_limiting",
- "python-sdk/fastmcp-server-middleware-response_limiting",
- "python-sdk/fastmcp-server-middleware-timing",
- "python-sdk/fastmcp-server-middleware-tool_injection"
- ]
- },
- {
- "group": "mixins",
- "pages": [
- "python-sdk/fastmcp-server-mixins-__init__",
- "python-sdk/fastmcp-server-mixins-lifespan",
- "python-sdk/fastmcp-server-mixins-mcp_operations",
- "python-sdk/fastmcp-server-mixins-transport"
- ]
- },
- {
- "group": "openapi",
- "pages": [
- "python-sdk/fastmcp-server-openapi-__init__",
- "python-sdk/fastmcp-server-openapi-components",
- "python-sdk/fastmcp-server-openapi-routing",
- "python-sdk/fastmcp-server-openapi-server"
- ]
- },
- {
- "group": "providers",
- "pages": [
- "python-sdk/fastmcp-server-providers-__init__",
- "python-sdk/fastmcp-server-providers-aggregate",
- "python-sdk/fastmcp-server-providers-base",
- "python-sdk/fastmcp-server-providers-fastmcp_provider",
- "python-sdk/fastmcp-server-providers-filesystem",
- "python-sdk/fastmcp-server-providers-filesystem_discovery",
- {
- "group": "local_provider",
- "pages": [
- "python-sdk/fastmcp-server-providers-local_provider-__init__",
- {
- "group": "decorators",
- "pages": [
- "python-sdk/fastmcp-server-providers-local_provider-decorators-__init__",
- "python-sdk/fastmcp-server-providers-local_provider-decorators-prompts",
- "python-sdk/fastmcp-server-providers-local_provider-decorators-resources",
- "python-sdk/fastmcp-server-providers-local_provider-decorators-tools"
- ]
- },
- "python-sdk/fastmcp-server-providers-local_provider-local_provider"
- ]
- },
- {
- "group": "openapi",
- "pages": [
- "python-sdk/fastmcp-server-providers-openapi-__init__",
- "python-sdk/fastmcp-server-providers-openapi-components",
- "python-sdk/fastmcp-server-providers-openapi-provider",
- "python-sdk/fastmcp-server-providers-openapi-routing"
- ]
- },
- "python-sdk/fastmcp-server-providers-proxy",
- {
- "group": "skills",
- "pages": [
- "python-sdk/fastmcp-server-providers-skills-__init__",
- "python-sdk/fastmcp-server-providers-skills-claude_provider",
- "python-sdk/fastmcp-server-providers-skills-directory_provider",
- "python-sdk/fastmcp-server-providers-skills-skill_provider",
- "python-sdk/fastmcp-server-providers-skills-vendor_providers"
- ]
- },
- "python-sdk/fastmcp-server-providers-wrapped_provider"
- ]
- },
- "python-sdk/fastmcp-server-proxy",
- {
- "group": "sampling",
- "pages": [
- "python-sdk/fastmcp-server-sampling-__init__",
- "python-sdk/fastmcp-server-sampling-run",
- "python-sdk/fastmcp-server-sampling-sampling_tool"
- ]
- },
- "python-sdk/fastmcp-server-server",
- {
- "group": "tasks",
- "pages": [
- "python-sdk/fastmcp-server-tasks-__init__",
- "python-sdk/fastmcp-server-tasks-capabilities",
- "python-sdk/fastmcp-server-tasks-config",
- "python-sdk/fastmcp-server-tasks-elicitation",
- "python-sdk/fastmcp-server-tasks-handlers",
- "python-sdk/fastmcp-server-tasks-keys",
- "python-sdk/fastmcp-server-tasks-notifications",
- "python-sdk/fastmcp-server-tasks-requests",
- "python-sdk/fastmcp-server-tasks-routing",
- "python-sdk/fastmcp-server-tasks-subscriptions"
- ]
- },
- "python-sdk/fastmcp-server-telemetry",
- {
- "group": "transforms",
- "pages": [
- "python-sdk/fastmcp-server-transforms-__init__",
- "python-sdk/fastmcp-server-transforms-catalog",
- "python-sdk/fastmcp-server-transforms-namespace",
- "python-sdk/fastmcp-server-transforms-prompts_as_tools",
- "python-sdk/fastmcp-server-transforms-resources_as_tools",
- {
- "group": "search",
- "pages": [
- "python-sdk/fastmcp-server-transforms-search-__init__",
- "python-sdk/fastmcp-server-transforms-search-base",
- "python-sdk/fastmcp-server-transforms-search-bm25",
- "python-sdk/fastmcp-server-transforms-search-regex"
- ]
- },
- "python-sdk/fastmcp-server-transforms-tool_transform",
- "python-sdk/fastmcp-server-transforms-version_filter",
- "python-sdk/fastmcp-server-transforms-visibility"
- ]
- }
- ]
- },
- {
- "group": "fastmcp.tools",
- "pages": [
- "python-sdk/fastmcp-tools-__init__",
- "python-sdk/fastmcp-tools-base",
- "python-sdk/fastmcp-tools-function_parsing",
- "python-sdk/fastmcp-tools-function_tool",
- "python-sdk/fastmcp-tools-tool_transform"
- ]
- },
- {
- "group": "fastmcp.utilities",
- "pages": [
- "python-sdk/fastmcp-utilities-__init__",
- "python-sdk/fastmcp-utilities-async_utils",
- "python-sdk/fastmcp-utilities-auth",
- "python-sdk/fastmcp-utilities-cli",
- "python-sdk/fastmcp-utilities-components",
- "python-sdk/fastmcp-utilities-exceptions",
- "python-sdk/fastmcp-utilities-http",
- "python-sdk/fastmcp-utilities-inspect",
- "python-sdk/fastmcp-utilities-json_schema",
- "python-sdk/fastmcp-utilities-json_schema_type",
- "python-sdk/fastmcp-utilities-lifespan",
- "python-sdk/fastmcp-utilities-logging",
- {
- "group": "mcp_server_config",
- "pages": [
- "python-sdk/fastmcp-utilities-mcp_server_config-__init__",
- {
- "group": "v1",
- "pages": [
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-__init__",
- {
- "group": "environments",
- "pages": [
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-__init__",
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base",
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv"
- ]
- },
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config",
- {
- "group": "sources",
- "pages": [
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-__init__",
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base",
- "python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem"
- ]
- }
- ]
- }
- ]
- },
- "python-sdk/fastmcp-utilities-mime",
- {
- "group": "openapi",
- "pages": [
- "python-sdk/fastmcp-utilities-openapi-__init__",
- "python-sdk/fastmcp-utilities-openapi-director",
- "python-sdk/fastmcp-utilities-openapi-formatters",
- "python-sdk/fastmcp-utilities-openapi-json_schema_converter",
- "python-sdk/fastmcp-utilities-openapi-models",
- "python-sdk/fastmcp-utilities-openapi-parser",
- "python-sdk/fastmcp-utilities-openapi-schemas"
- ]
- },
- "python-sdk/fastmcp-utilities-pagination",
- "python-sdk/fastmcp-utilities-skills",
- "python-sdk/fastmcp-utilities-tests",
- "python-sdk/fastmcp-utilities-timeout",
- "python-sdk/fastmcp-utilities-token_cache",
- "python-sdk/fastmcp-utilities-types",
- "python-sdk/fastmcp-utilities-ui",
- "python-sdk/fastmcp-utilities-version_check",
- "python-sdk/fastmcp-utilities-versions"
- ]
- }
- ]
+ "pages": {
+ "$ref": "./python-sdk-pages.json"
+ }
}
],
"dropdown": "SDK Reference",
@@ -825,205 +400,23 @@
"version": "v3"
},
{
- "dropdowns": [
- {
- "dropdown": "Documentation",
- "groups": [
- {
- "group": "Get Started",
- "pages": [
- "v2/getting-started/welcome",
- "v2/getting-started/installation",
- "v2/getting-started/quickstart",
- "v2/updates"
- ]
- },
- {
- "group": "Servers",
- "pages": [
- "v2/servers/server",
- {
- "group": "Core Components",
- "icon": "toolbox",
- "pages": [
- "v2/servers/tools",
- "v2/servers/resources",
- "v2/servers/prompts"
- ]
- },
- {
- "group": "Advanced Features",
- "icon": "stars",
- "pages": [
- "v2/servers/composition",
- "v2/servers/context",
- "v2/servers/elicitation",
- "v2/servers/icons",
- "v2/servers/logging",
- "v2/servers/middleware",
- "v2/servers/progress",
- "v2/servers/proxy",
- "v2/servers/sampling",
- "v2/servers/storage-backends",
- "v2/servers/tasks"
- ]
- },
- {
- "group": "Authentication",
- "icon": "shield-check",
- "pages": [
- "v2/servers/auth/authentication",
- "v2/servers/auth/token-verification",
- "v2/servers/auth/remote-oauth",
- "v2/servers/auth/oauth-proxy",
- "v2/servers/auth/oidc-proxy",
- "v2/servers/auth/full-oauth-server"
- ]
- },
- {
- "group": "Deployment",
- "icon": "rocket",
- "pages": [
- "v2/deployment/running-server",
- "v2/deployment/http",
- "deployment/prefect-horizon",
- "v2/deployment/server-configuration"
- ]
- }
- ]
- },
- {
- "group": "Clients",
- "pages": [
- {
- "group": "Essentials",
- "icon": "cube",
- "pages": [
- "v2/clients/client",
- "v2/clients/transports"
- ]
- },
- {
- "group": "Core Operations",
- "icon": "handshake",
- "pages": [
- "v2/clients/tools",
- "v2/clients/resources",
- "v2/clients/prompts"
- ]
- },
- {
- "group": "Advanced Features",
- "icon": "stars",
- "pages": [
- "v2/clients/elicitation",
- "v2/clients/logging",
- "v2/clients/progress",
- "v2/clients/sampling",
- "v2/clients/tasks",
- "v2/clients/messages",
- "v2/clients/roots"
- ]
- },
- {
- "group": "Authentication",
- "icon": "user-shield",
- "pages": [
- "v2/clients/auth/oauth",
- "v2/clients/auth/bearer"
- ]
- }
- ]
- },
- {
- "group": "Integrations",
- "pages": [
- {
- "group": "Authentication",
- "icon": "key",
- "pages": [
- "v2/integrations/auth0",
- "v2/integrations/authkit",
- "v2/integrations/aws-cognito",
- "v2/integrations/azure",
- "v2/integrations/descope",
- "v2/integrations/discord",
- "v2/integrations/github",
- "v2/integrations/google",
- "v2/integrations/oci",
- "v2/integrations/scalekit",
- "v2/integrations/supabase",
- "v2/integrations/workos"
- ]
- },
- {
- "group": "Authorization",
- "icon": "shield-check",
- "pages": [
- "v2/integrations/eunomia-authorization",
- "v2/integrations/permit"
- ]
- },
- {
- "group": "AI Assistants",
- "icon": "robot",
- "pages": [
- "v2/integrations/chatgpt",
- "v2/integrations/claude-code",
- "v2/integrations/claude-desktop",
- "v2/integrations/cursor",
- "v2/integrations/gemini-cli",
- "v2/integrations/mcp-json-configuration"
- ]
- },
- {
- "group": "AI SDKs",
- "icon": "code",
- "pages": [
- "v2/integrations/anthropic",
- "v2/integrations/gemini",
- "v2/integrations/openai"
- ]
- },
- {
- "group": "API Integration",
- "icon": "globe",
- "pages": [
- "v2/integrations/fastapi",
- "v2/integrations/openapi"
- ]
- }
- ]
- },
- {
- "group": "Patterns",
- "pages": [
- "v2/patterns/tool-transformation",
- "v2/patterns/decorating-methods",
- "v2/patterns/cli",
- "v2/patterns/contrib",
- "v2/patterns/testing"
- ]
- },
- {
- "group": "Development",
- "pages": [
- "v2/development/contributing",
- "v2/development/tests",
- "v2/development/releases",
- "v2/development/upgrade-guide",
- "v2/changelog"
- ]
- }
- ],
- "icon": "book"
- }
- ],
- "version": "v2.14.5"
+ "$ref": "./v2-navigation.json"
}
]
},
"redirects": [
+ {
+ "destination": "/apps/fastmcp-app",
+ "source": "/apps/interactive-apps"
+ },
+ {
+ "destination": "/apps/generative",
+ "source": "/apps/providers/generative"
+ },
+ {
+ "destination": "/apps/prefab",
+ "source": "/apps/patterns"
+ },
{
"destination": "/cli/overview",
"source": "/patterns/cli"
diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx
index 5f3f56b38..97d9f3c79 100644
--- a/docs/getting-started/quickstart.mdx
+++ b/docs/getting-started/quickstart.mdx
@@ -147,7 +147,7 @@ You can preview app tools locally with `fastmcp dev apps my_server.py` — no MC
## Deploy to Prefect Horizon
-[Prefect Horizon](https://horizon.prefect.io) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides managed hosting, authentication, access control, and observability for MCP servers.
+[Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides managed hosting, authentication, access control, and observability for MCP servers.
Horizon is **free for personal projects** and offers enterprise governance for teams.
@@ -156,7 +156,7 @@ Horizon is **free for personal projects** and offers enterprise governance for t
To deploy your server, you'll need a [GitHub account](https://github.com). Once you have one, you can deploy your server in three steps:
1. Push your `my_server.py` file to a GitHub repository
-2. Sign in to [Prefect Horizon](https://horizon.prefect.io) with your GitHub account
+2. Sign in to [Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) with your GitHub account
3. Create a new project from your repository and enter `my_server.py:mcp` as the server entrypoint
That's it! Horizon will build and deploy your server, making it available at a URL like `https://your-project.fastmcp.app/mcp`. You can chat with it to test its functionality, or connect to it from any LLM client that supports the MCP protocol.
diff --git a/docs/getting-started/upgrading/from-fastmcp-2.mdx b/docs/getting-started/upgrading/from-fastmcp-2.mdx
index f4795fe15..1e659e76a 100644
--- a/docs/getting-started/upgrading/from-fastmcp-2.mdx
+++ b/docs/getting-started/upgrading/from-fastmcp-2.mdx
@@ -264,7 +264,7 @@ auth = GitHubProvider(
The deprecated WebSocket client transport has been removed. Use `StreamableHttpTransport` instead:
-```python
+```python test="skip"
# Before
from fastmcp.client.transports import WSTransport
transport = WSTransport("ws://localhost:8000/ws")
@@ -357,7 +357,7 @@ main.mount(subserver)
The proxy and OpenAPI modules have moved under `providers` to reflect v3's provider-based architecture:
-```python
+```python test="skip"
# Deprecated
from fastmcp.server.proxy import FastMCPProxy
from fastmcp.server.openapi import FastMCPOpenAPI
@@ -369,7 +369,7 @@ from fastmcp.server.providers.openapi import OpenAPIProvider
`FastMCPOpenAPI` itself is deprecated — use `FastMCP` with an `OpenAPIProvider` instead:
-```python
+```python test="skip"
# Deprecated
from fastmcp.server.openapi import FastMCPOpenAPI
server = FastMCPOpenAPI(spec, client)
@@ -408,7 +408,7 @@ proxy = create_proxy("http://example.com/mcp")
The experimental OpenAPI parser is now standard. Update imports:
-```python
+```python test="skip"
# Before
from fastmcp.experimental.server.openapi import FastMCPOpenAPI
diff --git a/docs/getting-started/welcome.mdx b/docs/getting-started/welcome.mdx
index 1e8dff6ab..d42dc39b7 100644
--- a/docs/getting-started/welcome.mdx
+++ b/docs/getting-started/welcome.mdx
@@ -115,7 +115,7 @@ from fastmcp import Client
async def main():
async with Client("https://gofastmcp.com/mcp") as client:
result = await client.call_tool(
- name="SearchFastMcp",
+ name="search_fast_mcp",
arguments={"query": "deploy a FastMCP server"}
)
print(result)
diff --git a/docs/integrations/anthropic.mdx b/docs/integrations/anthropic.mdx
index b8156ac95..08b9b2c9c 100644
--- a/docs/integrations/anthropic.mdx
+++ b/docs/integrations/anthropic.mdx
@@ -181,7 +181,7 @@ if __name__ == "__main__":
If you try to call the authenticated server with the same Anthropic code we wrote earlier, you'll get an error indicating that the server rejected the request because it's not authenticated.
-```python
+```text
Error code: 400 - {
"type": "error",
"error": {
diff --git a/docs/integrations/authkit.mdx b/docs/integrations/authkit.mdx
index e99f78351..c77175201 100644
--- a/docs/integrations/authkit.mdx
+++ b/docs/integrations/authkit.mdx
@@ -9,29 +9,32 @@ import { VersionBadge } from "/snippets/version-badge.mdx"
-This guide shows you how to secure your FastMCP server using WorkOS's **AuthKit**, a complete authentication and user management solution. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern, where AuthKit handles user login and your FastMCP server validates the tokens.
-
-
-AuthKit does not currently support [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html) resource indicators, so FastMCP cannot validate that tokens were issued for the specific resource server. If you need resource-specific audience validation, consider using [WorkOSProvider](/integrations/workos) (OAuth proxy pattern) instead.
-
+This guide shows you how to secure your FastMCP server using WorkOS's **AuthKit**, a complete authentication and user management solution. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern with [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html) resource indicators: AuthKit issues tokens whose `aud` claim is bound to your server's resource URL, and FastMCP validates that claim automatically.
## Configuration
+
### Prerequisites
Before you begin, you will need:
1. A **[WorkOS Account](https://workos.com/)** and a new **Project**.
2. An **[AuthKit](https://www.authkit.com/)** instance configured within your WorkOS project.
-3. Your FastMCP server's URL (can be localhost for development, e.g., `http://localhost:8000`).
+3. Your FastMCP server's URL (can be localhost for development, e.g., `http://127.0.0.1:8000`).
-### Step 1: AuthKit Configuration
+### Step 1: WorkOS Dashboard
-In your WorkOS Dashboard, enable AuthKit and configure the following settings:
+In the WorkOS Dashboard, go to **Connect → Configuration** and configure:
-
- Go to **Applications → Configuration** and enable **Dynamic Client Registration**. This allows MCP clients register with your application automatically.
+
+ Enable **Dynamic Client Registration** (DCR) so MCP clients can register themselves. Alternatively, enable **Client ID Metadata Document** (CIMD) if your clients support it.
+
- 
+
+ Add your FastMCP server's resource URL (e.g., `http://127.0.0.1:8000/mcp`) as a valid resource indicator.
+
+ This must exactly match what FastMCP advertises in its protected resource metadata. Start your server first and it will log the correct URL on startup — copy that value.
+
+ Without this step, AuthKit falls back to a default environment-scoped audience and audience validation will fail with a 401.
@@ -47,16 +50,18 @@ Create your FastMCP server file and use the `AuthKitProvider` to handle all the
from fastmcp import FastMCP
from fastmcp.server.auth.providers.workos import AuthKitProvider
-# The AuthKitProvider automatically discovers WorkOS endpoints
-# and configures JWT token validation
+# AuthKitProvider automatically discovers WorkOS endpoints, configures JWT
+# validation, and binds the token audience to this server's resource URL.
auth_provider = AuthKitProvider(
authkit_domain="https://your-project-12345.authkit.app",
- base_url="http://localhost:8000" # Use your actual server URL
+ base_url="http://127.0.0.1:8000", # Use your actual server URL
)
mcp = FastMCP(name="AuthKit Secured App", auth=auth_provider)
```
+When the server starts, it logs the resource URL it is validating against. Paste that URL into your Dashboard's **MCP resource indicators** list.
+
## Testing
To test your server, you can use the `fastmcp` CLI to run it locally. Assuming you've saved the above code to `server.py` (after replacing the `authkit_domain` and `base_url` with your actual values!), you can run the following command:
@@ -75,7 +80,7 @@ import asyncio
auth = OAuth(additional_client_metadata={"token_endpoint_auth_method": "none"})
async def main():
- async with Client("http://localhost:8000/mcp", auth=auth) as client:
+ async with Client("http://127.0.0.1:8000/mcp", auth=auth) as client:
assert await client.ping()
if __name__ == "__main__":
@@ -94,7 +99,7 @@ from fastmcp.server.auth.providers.workos import AuthKitProvider
# Load configuration from environment variables
auth = AuthKitProvider(
authkit_domain=os.environ.get("AUTHKIT_DOMAIN"),
- base_url=os.environ.get("BASE_URL", "https://your-server.com")
+ base_url=os.environ.get("BASE_URL", "https://your-server.com"),
)
mcp = FastMCP(name="AuthKit Secured App", auth=auth)
diff --git a/docs/integrations/azure.mdx b/docs/integrations/azure.mdx
index 4376a38ce..cba92349e 100644
--- a/docs/integrations/azure.mdx
+++ b/docs/integrations/azure.mdx
@@ -458,3 +458,85 @@ For advanced OBO scenarios, use `CurrentAccessToken()` to get the user's token,
For a complete working example of Azure OBO with FastMCP, see [Pamela Fox's blog post on OBO flow for Entra-based MCP servers](https://blog.pamelafox.org/2026/01/using-on-behalf-of-flow-for-entra-based.html).
+
+## Azure AD B2C
+
+
+
+Azure AD B2C (Business-to-Consumer) uses different endpoints, scope URIs, and
+token issuers than standard Microsoft Entra ID. The `AzureProvider.from_b2c()`
+factory handles all of these differences automatically.
+
+
+Azure AD B2C does **not** support the On-Behalf-Of (OBO) flow. If you need
+OBO for downstream API calls, use `AzureProvider` with standard Entra ID
+instead.
+
+
+### Quick Start
+
+```python server.py
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.azure import AzureProvider
+
+auth = AzureProvider.from_b2c(
+ tenant_name="mytenant",
+ policy_name="B2C_1_susi",
+ client_id="00000000-0000-0000-0000-000000000000",
+ client_secret="my-secret",
+ required_scopes=["mcp-access"],
+ base_url="https://myserver.com",
+)
+
+mcp = FastMCP("My App", auth=auth)
+```
+
+`from_b2c()` derives the following values automatically:
+
+| Derived value | Formula |
+|---|---|
+| Authority host | `{tenant_name}.b2clogin.com` |
+| Authorization endpoint | `https://{tenant_name}.b2clogin.com/{tenant_name}.onmicrosoft.com/{policy_name}/oauth2/v2.0/authorize` |
+| Token endpoint | `https://{tenant_name}.b2clogin.com/{tenant_name}.onmicrosoft.com/{policy_name}/oauth2/v2.0/token` |
+| Scope identifier URI | `https://{tenant_name}.onmicrosoft.com/{client_id}` |
+
+### Token Issuer Validation
+
+B2C access tokens carry the **tenant GUID** (not the `.onmicrosoft.com` name)
+in the `iss` claim, and the exact format varies by policy and custom-domain
+configuration. `from_b2c()` therefore **disables issuer validation by
+default**; **audience validation still enforces that tokens target the correct
+application**.
+
+Once you have confirmed a successful end-to-end login, read the actual `iss`
+value from the decoded claims and enable strict validation:
+
+```python
+auth = AzureProvider.from_b2c(
+ tenant_name="mytenant",
+ policy_name="B2C_1_susi",
+ client_id="00000000-0000-0000-0000-000000000000",
+ client_secret="my-secret",
+ required_scopes=["mcp-access"],
+ base_url="https://myserver.com",
+ token_issuer="https://mytenant.b2clogin.com/11111111-2222-3333-4444-555555555555/v2.0/",
+)
+```
+
+### Custom Domains
+
+If your B2C tenant uses a [custom domain](https://learn.microsoft.com/en-us/azure/active-directory-b2c/custom-domain)
+(e.g. `auth.mycompany.com` instead of `mytenant.b2clogin.com`), pass it via
+`custom_domain`:
+
+```python
+auth = AzureProvider.from_b2c(
+ tenant_name="mytenant",
+ policy_name="B2C_1_susi",
+ client_id="00000000-0000-0000-0000-000000000000",
+ client_secret="my-secret",
+ required_scopes=["mcp-access"],
+ base_url="https://myserver.com",
+ custom_domain="auth.mycompany.com",
+)
+```
diff --git a/docs/integrations/chatgpt.mdx b/docs/integrations/chatgpt.mdx
index 92ddb7404..23249f92c 100644
--- a/docs/integrations/chatgpt.mdx
+++ b/docs/integrations/chatgpt.mdx
@@ -92,10 +92,12 @@ The connector must be explicitly enabled in each chat session through Developer
### Skip Confirmations
-Use `annotations={"readOnlyHint": True}` to skip confirmation prompts for read-only tools:
+Use `annotations=ToolAnnotations(readOnlyHint=True)` to skip confirmation prompts for read-only tools:
```python
-@mcp.tool(annotations={"readOnlyHint": True})
+from mcp.types import ToolAnnotations
+
+@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
def get_status() -> str:
"""Check system status."""
return "All systems operational"
@@ -153,4 +155,3 @@ def fetch(id: str) -> dict:
5. Ask research questions
ChatGPT will use your `search` and `fetch` tools to find and cite relevant information.
-
diff --git a/docs/integrations/descope.mdx b/docs/integrations/descope.mdx
index dba9b3509..bfb6cd9c8 100644
--- a/docs/integrations/descope.mdx
+++ b/docs/integrations/descope.mdx
@@ -63,8 +63,8 @@ from fastmcp.server.auth.providers.descope import DescopeProvider
# The DescopeProvider automatically discovers Descope endpoints
# and configures JWT token validation
auth_provider = DescopeProvider(
- config_url=https://.../.well-known/openid-configuration, # Your MCP Server .well-known URL
- base_url=SERVER_URL, # Your server's public URL
+ config_url="https://.../.well-known/openid-configuration", # Your MCP Server .well-known URL
+ base_url=SERVER_URL, # Your server's public URL
)
# Create FastMCP server with auth
diff --git a/docs/integrations/fastapi.mdx b/docs/integrations/fastapi.mdx
index abdc1d0e7..83aa924f8 100644
--- a/docs/integrations/fastapi.mdx
+++ b/docs/integrations/fastapi.mdx
@@ -220,7 +220,7 @@ Because FastMCP's FastAPI integration is based on its [OpenAPI integration](/int
```python
# Assumes the FastAPI app from above is already defined
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
# Custom mapping rules
mcp = FastMCP.from_fastapi(
diff --git a/docs/integrations/github.mdx b/docs/integrations/github.mdx
index 72e65d3ce..d493eb1ef 100644
--- a/docs/integrations/github.mdx
+++ b/docs/integrations/github.mdx
@@ -119,7 +119,7 @@ async def main():
# Test the protected tool
result = await client.call_tool("get_user_info")
- print(f"GitHub user: {result['github_user']}")
+ print(f"GitHub user: {result.data['github_user']}")
if __name__ == "__main__":
asyncio.run(main())
diff --git a/docs/integrations/keycloak.mdx b/docs/integrations/keycloak.mdx
new file mode 100644
index 000000000..22d61f132
--- /dev/null
+++ b/docs/integrations/keycloak.mdx
@@ -0,0 +1,141 @@
+---
+title: Keycloak OAuth 🤝 FastMCP
+sidebarTitle: Keycloak
+description: Secure your FastMCP server with Keycloak OAuth
+icon: shield-check
+tag: NEW
+---
+
+import { VersionBadge } from "/snippets/version-badge.mdx"
+
+
+
+This guide shows you how to secure your FastMCP server using **Keycloak OAuth**. This integration uses the [**Remote OAuth**](/servers/auth/remote-oauth) pattern with Dynamic Client Registration (DCR), where Keycloak handles user login and your FastMCP server validates the tokens.
+
+
+**Keycloak 26.6.0 or later is required.** Earlier versions had a DCR incompatibility with MCP clients ([PR #45309](https://github.com/keycloak/keycloak/pull/45309)) that is fixed in 26.6.0.
+
+
+## Configuration
+
+### Prerequisites
+
+Before you begin, you will need:
+1. A running **[Keycloak](https://keycloak.org/)** instance (e.g., `http://localhost:8080`)
+2. A Keycloak realm with **Dynamic Client Registration** enabled and a trusted host policy that allows your server URL (e.g., `http://localhost:8000/*`)
+3. Your FastMCP server's public URL (e.g., `http://localhost:8000`)
+
+### FastMCP Configuration
+
+Create your FastMCP server and use `KeycloakAuthProvider` to handle OAuth:
+
+```python server.py
+import os
+
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
+from fastmcp.server.dependencies import get_access_token
+
+auth = KeycloakAuthProvider(
+ realm_url=os.getenv("KEYCLOAK_REALM_URL") or "http://localhost:8080/realms/myrealm",
+ base_url="http://localhost:8000",
+ # audience="http://localhost:8000", # Recommended for production
+)
+
+mcp = FastMCP("Keycloak Example Server", auth=auth)
+
+
+@mcp.tool
+async def get_access_token_claims() -> dict:
+ """Get the authenticated user's access token claims."""
+ token = get_access_token()
+ return {
+ "sub": token.claims.get("sub"),
+ "scope": token.claims.get("scope"),
+ "azp": token.claims.get("azp"),
+ }
+```
+
+
+**Production security**: Always configure the `audience` parameter in production. Without it, your server accepts tokens issued for any audience. Configure Keycloak audience mappers and set `audience` to your server's base URL to ensure tokens are specifically intended for your server.
+
+
+## Local Development
+
+Local infrastructure tooling is deliberately kept out of the FastMCP core library to keep auth integrations slim and the associated maintenance burden as low as possible. That said, Keycloak is a popular identity provider for local development and testing, so a dedicated FastMCP-compatible setup blueprint lives in the companion project [**fastmcp-keycloak-local**](https://github.com/stephaneberle9/fastmcp-keycloak-local).
+
+It provides everything needed to develop and test FastMCP servers with Keycloak OAuth locally: a Docker-based Keycloak setup with a pre-configured `fastmcp` realm (Dynamic Client Registration enabled, test user included), cross-platform start scripts, and integration guides for the MCP Inspector, Claude Desktop, and Claude Code CLI.
+
+## Testing
+
+### Running the Server
+
+```bash
+fastmcp run server.py --transport http --port 8000
+```
+
+### Testing with a Client
+
+```python client.py
+import asyncio
+from fastmcp import Client
+
+async def main():
+ async with Client("http://localhost:8000/mcp", auth="oauth") as client:
+ print("✓ Authenticated with Keycloak!")
+ result = await client.call_tool("get_access_token_claims")
+ print(f"sub: {result.data.get('sub', 'N/A')}")
+
+asyncio.run(main())
+```
+
+On first run, your browser will open to Keycloak's authorization page. After login, the client receives a token and caches it for subsequent runs.
+
+## Features
+
+### JWT Token Validation
+
+- **Signature Verification**: Validates tokens against Keycloak's JWKS endpoint
+- **Expiration Checking**: Automatically rejects expired tokens
+- **Issuer Validation**: Ensures tokens come from your specific Keycloak realm
+- **Scope Enforcement**: Verifies required OAuth scopes are present
+- **Audience Validation**: Optional validation that tokens target your server (configure `audience`)
+
+### User Claims
+
+Access user information from Keycloak JWT tokens:
+
+```python
+from fastmcp.server.dependencies import get_access_token
+
+@mcp.tool
+async def admin_only_tool() -> str:
+ """A tool only available to admin users."""
+ token = get_access_token()
+ roles = token.claims.get("realm_access", {}).get("roles", [])
+ if "admin" not in roles:
+ raise ValueError("This tool requires admin access")
+ return "Admin access granted!"
+```
+
+## Advanced Configuration
+
+### Custom Token Verifier
+
+```python
+from fastmcp.server.auth.providers.jwt import JWTVerifier
+from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
+
+custom_verifier = JWTVerifier(
+ jwks_uri="http://localhost:8080/realms/myrealm/protocol/openid-connect/certs",
+ issuer="http://localhost:8080/realms/myrealm",
+ audience="my-resource-server",
+ required_scopes=["api:read", "api:write"],
+)
+
+auth = KeycloakAuthProvider(
+ realm_url="http://localhost:8080/realms/myrealm",
+ base_url="http://localhost:8000",
+ token_verifier=custom_verifier,
+)
+```
diff --git a/docs/integrations/mcp-json-configuration.mdx b/docs/integrations/mcp-json-configuration.mdx
index b44b15e76..fec8ffc01 100644
--- a/docs/integrations/mcp-json-configuration.mdx
+++ b/docs/integrations/mcp-json-configuration.mdx
@@ -357,6 +357,98 @@ echo "$CONFIG" | jq '."CI Server".command'
# Output: "uv"
```
+### UV-Managed Project Dependencies
+
+For servers that live inside a uv-managed project (with `pyproject.toml`), use the `--project` flag to run within that project's environment:
+
+```bash
+fastmcp install mcp-json server.py --project .
+```
+
+Output:
+```json
+{
+ "My Server": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--project",
+ "/absolute/path/to/project",
+ "--with",
+ "fastmcp",
+ "fastmcp",
+ "run",
+ "/absolute/path/to/project/server.py"
+ ]
+ }
+}
+```
+
+You can also use `fastmcp.json` with a local project:
+
+```json fastmcp.json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py"
+ },
+ "environment": {
+ "project": "."
+ }
+}
+```
+
+If your server needs additional packages beyond those in `pyproject.toml`, add them via the `dependencies` array or `--with`.
+
+### Published Packages with `uvx`
+
+If your team publishes MCP servers as pip packages, you can configure clients to run them with `uvx` directly instead of `uv run`. For example, if your package is called `my-mcp-server` and provides a CLI entry point of the same name:
+
+```json
+{
+ "mcpServers": {
+ "My Server": {
+ "command": "uvx",
+ "args": ["my-mcp-server"]
+ }
+ }
+}
+```
+
+If the package name differs from the CLI command (e.g., package `weather-mcp` with command `weather-server`):
+
+```json
+{
+ "mcpServers": {
+ "Weather": {
+ "command": "uvx",
+ "args": ["--from", "weather-mcp", "weather-server"]
+ }
+ }
+}
+```
+
+You can also pin Python versions or add extra dependencies:
+
+```json
+{
+ "mcpServers": {
+ "My Server": {
+ "command": "uvx",
+ "args": [
+ "--python", "3.12",
+ "--with", "requests",
+ "my-mcp-server"
+ ]
+ }
+ }
+}
+```
+
+
+`fastmcp install mcp-json` generates `uv run` configurations for local development. For published packages, you'll typically write the `uvx` configuration manually or generate it through your own packaging workflow.
+
+
## Integration with MCP Clients
The generated configuration works with any MCP-compatible application:
diff --git a/docs/integrations/openai.mdx b/docs/integrations/openai.mdx
index 6f88193f5..94ca82b40 100644
--- a/docs/integrations/openai.mdx
+++ b/docs/integrations/openai.mdx
@@ -178,8 +178,8 @@ if __name__ == "__main__":
If you try to call the authenticated server with the same OpenAI code we wrote earlier, you'll get an error like this:
-```python
-pythonAPIStatusError: Error code: 424 - {
+```text
+APIStatusError: Error code: 424 - {
"error": {
"message": "Error retrieving tool list from MCP server: 'dice_server'. Http status code: 401 (Unauthorized)",
"type": "external_connector_error",
diff --git a/docs/integrations/openapi.mdx b/docs/integrations/openapi.mdx
index 88dc19b14..f5f2b3dfa 100644
--- a/docs/integrations/openapi.mdx
+++ b/docs/integrations/openapi.mdx
@@ -85,7 +85,7 @@ Each `RouteMap` specifies a combination of methods, patterns, and tags, as well
Here is FastMCP's default rule:
```python
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
DEFAULT_ROUTE_MAPPINGS = [
# All routes become tools
@@ -101,7 +101,7 @@ For example, prior to FastMCP 2.8.0, GET requests were automatically mapped to `
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
# Restore pre-2.8.0 semantic mapping
semantic_maps = [
@@ -124,7 +124,7 @@ Here is a more complete example that uses custom route maps to convert all `GET`
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -164,7 +164,7 @@ You can use this to remove sensitive or internal routes by targeting them specif
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -180,7 +180,7 @@ Or you can use a catch-all rule to exclude everything that your maps don't handl
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -212,7 +212,8 @@ The `route_map_fn` is called on all routes, even those that matched `MCPType.EXC
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType, HTTPRoute
+from fastmcp.server.providers.openapi import RouteMap, MCPType
+from fastmcp.utilities.openapi import HTTPRoute
def custom_route_mapper(route: HTTPRoute, mcp_type: MCPType) -> MCPType | None:
"""Advanced route type mapping."""
@@ -277,7 +278,7 @@ FastMCP provides several ways to add tags to your MCP components, allowing you t
You can add custom tags to components created from specific routes using the `mcp_tags` parameter in `RouteMap`. These tags will be applied to all components created from routes that match that particular route map.
```python
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -368,12 +369,12 @@ Your `mcp_component_fn` is expected to modify the component in-place, not to ret
```python
-from fastmcp.server.openapi import (
- HTTPRoute,
+from fastmcp.server.providers.openapi import (
OpenAPITool,
OpenAPIResource,
OpenAPIResourceTemplate,
)
+from fastmcp.utilities.openapi import HTTPRoute
def customize_components(
route: HTTPRoute,
diff --git a/docs/integrations/pydantic-ai.mdx b/docs/integrations/pydantic-ai.mdx
new file mode 100644
index 000000000..0c8ffa524
--- /dev/null
+++ b/docs/integrations/pydantic-ai.mdx
@@ -0,0 +1,137 @@
+---
+title: Pydantic AI 🤝 FastMCP
+sidebarTitle: Pydantic AI
+description: Connect FastMCP servers to Pydantic AI agents using the FastMCPToolset
+icon: message-code
+---
+
+[Pydantic AI](https://ai.pydantic.dev/) ships a [`FastMCPToolset`](https://ai.pydantic.dev/mcp/fastmcp-client/) that lets a Pydantic AI agent call tools exposed by any MCP server through the [FastMCP Client](/clients/client). Because the toolset is built on the FastMCP Client, it works with FastMCP servers as well as any other MCP server, and supports the full range of [transports](/clients/transports): in-memory, STDIO, Streamable HTTP, and SSE.
+
+This page shows how to point `FastMCPToolset` at a FastMCP server, with examples for each transport. For the toolset's full API, see the [Pydantic AI documentation](https://ai.pydantic.dev/mcp/fastmcp-client/).
+
+
+The `FastMCPToolset` currently exposes **tools** to the agent. Other MCP features such as elicitation and sampling are not yet supported through this toolset; use Pydantic AI's standard [`MCPServer`](https://ai.pydantic.dev/mcp/client/) client if you need them.
+
+
+## Install
+
+`FastMCPToolset` lives in `pydantic-ai-slim` behind the `fastmcp` optional group:
+
+```bash
+pip install "pydantic-ai-slim[fastmcp]"
+```
+
+## Create a Server
+
+Create a FastMCP server with the tools you want to expose. We'll use a single dice-rolling tool throughout this guide.
+
+```python server.py
+import random
+from fastmcp import FastMCP
+
+mcp = FastMCP(name="Dice Roller")
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ """Roll `n_dice` 6-sided dice and return the results."""
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
+```
+
+## In-Memory
+
+If your FastMCP server lives in the same process as your agent, pass the `FastMCP` instance directly. The toolset reuses an [in-memory transport](/clients/transports#in-memory-transport), which avoids a network round trip and is the fastest option for tests and embedded use.
+
+```python
+import asyncio
+import random
+from fastmcp import FastMCP
+from pydantic_ai import Agent
+from pydantic_ai.toolsets.fastmcp import FastMCPToolset
+
+mcp = FastMCP(name="Dice Roller")
+
+@mcp.tool
+def roll_dice(n_dice: int) -> list[int]:
+ return [random.randint(1, 6) for _ in range(n_dice)]
+
+toolset = FastMCPToolset(mcp)
+agent = Agent("openai:gpt-4.1", toolsets=[toolset])
+
+async def main():
+ result = await agent.run("Roll 3 dice!")
+ print(result.output)
+
+if __name__ == "__main__":
+ asyncio.run(main())
+```
+
+## Streamable HTTP
+
+For a remote FastMCP server reachable over HTTP, pass the URL as a string. The toolset infers the [Streamable HTTP transport](/clients/transports#http-transport) from the URL.
+
+```python
+from pydantic_ai import Agent
+from pydantic_ai.toolsets.fastmcp import FastMCPToolset
+
+toolset = FastMCPToolset("https://your-server-url.com/mcp")
+agent = Agent("openai:gpt-4.1", toolsets=[toolset])
+```
+
+For [SSE](/clients/transports#sse-transport), use a `/sse` URL instead.
+
+## STDIO
+
+To launch a FastMCP server as a subprocess, pass a script path and the toolset will use the [STDIO transport](/clients/transports#stdio-transport).
+
+```python
+from pydantic_ai import Agent
+from pydantic_ai.toolsets.fastmcp import FastMCPToolset
+
+toolset = FastMCPToolset("server.py")
+agent = Agent("openai:gpt-4.1", toolsets=[toolset])
+```
+
+You can also pass a [`StdioTransport`](/clients/transports#stdio-transport) directly when you need control over the command, args, or environment.
+
+## MCP Configuration
+
+To wire up multiple servers at once, pass an [MCP configuration](/integrations/mcp-json-configuration) dictionary. The toolset opens one client per server and exposes all of their tools to the agent.
+
+```python
+from pydantic_ai import Agent
+from pydantic_ai.toolsets.fastmcp import FastMCPToolset
+
+mcp_config = {
+ "mcpServers": {
+ "dice": {"command": "python", "args": ["server.py"]},
+ "weather": {"url": "https://weather.example.com/mcp"},
+ }
+}
+
+toolset = FastMCPToolset(mcp_config)
+agent = Agent("openai:gpt-4.1", toolsets=[toolset])
+```
+
+## Authentication
+
+Because `FastMCPToolset` wraps a [FastMCP `Client`](/clients/client), it inherits the client's full [authentication](/clients/auth/bearer) story. To pass credentials such as a bearer token to a remote server, build a `Client` (or `StreamableHttpTransport`) yourself and hand it to the toolset.
+
+```python
+from fastmcp import Client
+from fastmcp.client.transports import StreamableHttpTransport
+from pydantic_ai import Agent
+from pydantic_ai.toolsets.fastmcp import FastMCPToolset
+
+transport = StreamableHttpTransport(
+ url="https://your-server-url.com/mcp",
+ headers={"Authorization": "Bearer your-access-token"},
+)
+
+toolset = FastMCPToolset(Client(transport))
+agent = Agent("openai:gpt-4.1", toolsets=[toolset])
+```
+
+For OAuth flows, use FastMCP's [`OAuth` helper](/clients/auth/oauth) when constructing the `Client`. For server-side token verification, see [Token Verification](/servers/auth/token-verification).
diff --git a/docs/patterns/contrib.mdx b/docs/patterns/contrib.mdx
index d2f812f52..04ef45aff 100644
--- a/docs/patterns/contrib.mdx
+++ b/docs/patterns/contrib.mdx
@@ -12,13 +12,13 @@ FastMCP includes a `contrib` package that holds community-contributed modules. T
Contrib modules provide additional features, integrations, or patterns that complement the core FastMCP library. They offer a way for the community to share useful extensions while keeping the core library focused and maintainable.
-The available modules can be viewed in the [contrib directory](https://github.com/PrefectHQ/fastmcp/tree/main/src/fastmcp/contrib).
+The available modules can be viewed in the [contrib directory](https://github.com/PrefectHQ/fastmcp/tree/main/fastmcp_slim/fastmcp/contrib).
## Usage
To use a contrib module, import it from the `fastmcp.contrib` package:
-```python
+```python test="skip"
from fastmcp.contrib import my_module
```
@@ -32,7 +32,7 @@ from fastmcp.contrib import my_module
We welcome contributions to the `contrib` package! If you have a module that extends FastMCP in a useful way, consider contributing it:
-1. Create a new directory in `src/fastmcp/contrib/` for your module
+1. Create a new directory in `fastmcp_slim/fastmcp/contrib/` for your module
3. Add proper tests for your module in `tests/contrib/`
2. Include comprehensive documentation in a README.md file, including usage and examples, as well as any additional dependencies or installation instructions
5. Submit a pull request
diff --git a/docs/python-sdk-pages.json b/docs/python-sdk-pages.json
new file mode 100644
index 000000000..86cfabc6b
--- /dev/null
+++ b/docs/python-sdk-pages.json
@@ -0,0 +1,433 @@
+[
+ "python-sdk/fastmcp-decorators",
+ "python-sdk/fastmcp-dependencies",
+ "python-sdk/fastmcp-exceptions",
+ "python-sdk/fastmcp-mcp_config",
+ "python-sdk/fastmcp-settings",
+ "python-sdk/fastmcp-telemetry",
+ "python-sdk/fastmcp-types",
+ {
+ "group": "fastmcp.apps",
+ "pages": [
+ "python-sdk/fastmcp-apps-__init__",
+ "python-sdk/fastmcp-apps-app",
+ "python-sdk/fastmcp-apps-approval",
+ "python-sdk/fastmcp-apps-choice",
+ "python-sdk/fastmcp-apps-config",
+ "python-sdk/fastmcp-apps-file_upload",
+ "python-sdk/fastmcp-apps-form",
+ "python-sdk/fastmcp-apps-generative"
+ ]
+ },
+ {
+ "group": "fastmcp.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",
+ "python-sdk/fastmcp-cli-client",
+ "python-sdk/fastmcp-cli-discovery",
+ "python-sdk/fastmcp-cli-generate",
+ {
+ "group": "install",
+ "pages": [
+ "python-sdk/fastmcp-cli-install-__init__",
+ "python-sdk/fastmcp-cli-install-claude_code",
+ "python-sdk/fastmcp-cli-install-claude_desktop",
+ "python-sdk/fastmcp-cli-install-cursor",
+ "python-sdk/fastmcp-cli-install-gemini_cli",
+ "python-sdk/fastmcp-cli-install-goose",
+ "python-sdk/fastmcp-cli-install-mcp_json",
+ "python-sdk/fastmcp-cli-install-shared",
+ "python-sdk/fastmcp-cli-install-stdio"
+ ]
+ },
+ "python-sdk/fastmcp-cli-run",
+ "python-sdk/fastmcp-cli-tasks"
+ ]
+ },
+ {
+ "group": "fastmcp.client",
+ "pages": [
+ "python-sdk/fastmcp-client-__init__",
+ {
+ "group": "auth",
+ "pages": [
+ "python-sdk/fastmcp-client-auth-__init__",
+ "python-sdk/fastmcp-client-auth-bearer",
+ "python-sdk/fastmcp-client-auth-oauth"
+ ]
+ },
+ "python-sdk/fastmcp-client-client",
+ "python-sdk/fastmcp-client-elicitation",
+ "python-sdk/fastmcp-client-logging",
+ "python-sdk/fastmcp-client-messages",
+ {
+ "group": "mixins",
+ "pages": [
+ "python-sdk/fastmcp-client-mixins-__init__",
+ "python-sdk/fastmcp-client-mixins-prompts",
+ "python-sdk/fastmcp-client-mixins-resources",
+ "python-sdk/fastmcp-client-mixins-task_management",
+ "python-sdk/fastmcp-client-mixins-tools"
+ ]
+ },
+ "python-sdk/fastmcp-client-oauth_callback",
+ "python-sdk/fastmcp-client-progress",
+ "python-sdk/fastmcp-client-roots",
+ {
+ "group": "sampling",
+ "pages": [
+ "python-sdk/fastmcp-client-sampling-__init__",
+ {
+ "group": "handlers",
+ "pages": [
+ "python-sdk/fastmcp-client-sampling-handlers-__init__",
+ "python-sdk/fastmcp-client-sampling-handlers-anthropic",
+ "python-sdk/fastmcp-client-sampling-handlers-google_genai",
+ "python-sdk/fastmcp-client-sampling-handlers-openai"
+ ]
+ }
+ ]
+ },
+ "python-sdk/fastmcp-client-tasks",
+ "python-sdk/fastmcp-client-telemetry",
+ {
+ "group": "transports",
+ "pages": [
+ "python-sdk/fastmcp-client-transports-__init__",
+ "python-sdk/fastmcp-client-transports-base",
+ "python-sdk/fastmcp-client-transports-config",
+ "python-sdk/fastmcp-client-transports-http",
+ "python-sdk/fastmcp-client-transports-inference",
+ "python-sdk/fastmcp-client-transports-memory",
+ "python-sdk/fastmcp-client-transports-sse",
+ "python-sdk/fastmcp-client-transports-stdio"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "fastmcp.experimental",
+ "pages": [
+ "python-sdk/fastmcp-experimental-__init__",
+ {
+ "group": "sampling",
+ "pages": [
+ "python-sdk/fastmcp-experimental-sampling-__init__",
+ "python-sdk/fastmcp-experimental-sampling-handlers"
+ ]
+ },
+ {
+ "group": "transforms",
+ "pages": [
+ "python-sdk/fastmcp-experimental-transforms-__init__",
+ "python-sdk/fastmcp-experimental-transforms-code_mode"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "fastmcp.prompts",
+ "pages": [
+ "python-sdk/fastmcp-prompts-__init__",
+ "python-sdk/fastmcp-prompts-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-template",
+ "python-sdk/fastmcp-resources-types"
+ ]
+ },
+ {
+ "group": "fastmcp.server",
+ "pages": [
+ "python-sdk/fastmcp-server-__init__",
+ "python-sdk/fastmcp-server-app",
+ "python-sdk/fastmcp-server-apps",
+ {
+ "group": "auth",
+ "pages": [
+ "python-sdk/fastmcp-server-auth-__init__",
+ "python-sdk/fastmcp-server-auth-auth",
+ "python-sdk/fastmcp-server-auth-authorization",
+ "python-sdk/fastmcp-server-auth-cimd",
+ {
+ "group": "handlers",
+ "pages": [
+ "python-sdk/fastmcp-server-auth-handlers-__init__",
+ "python-sdk/fastmcp-server-auth-handlers-authorize"
+ ]
+ },
+ "python-sdk/fastmcp-server-auth-jwt_issuer",
+ "python-sdk/fastmcp-server-auth-middleware",
+ {
+ "group": "oauth_proxy",
+ "pages": [
+ "python-sdk/fastmcp-server-auth-oauth_proxy-__init__",
+ "python-sdk/fastmcp-server-auth-oauth_proxy-consent",
+ "python-sdk/fastmcp-server-auth-oauth_proxy-models",
+ "python-sdk/fastmcp-server-auth-oauth_proxy-proxy",
+ "python-sdk/fastmcp-server-auth-oauth_proxy-ui"
+ ]
+ },
+ "python-sdk/fastmcp-server-auth-oidc_proxy",
+ {
+ "group": "providers",
+ "pages": [
+ "python-sdk/fastmcp-server-auth-providers-__init__",
+ "python-sdk/fastmcp-server-auth-providers-auth0",
+ "python-sdk/fastmcp-server-auth-providers-aws",
+ "python-sdk/fastmcp-server-auth-providers-azure",
+ "python-sdk/fastmcp-server-auth-providers-clerk",
+ "python-sdk/fastmcp-server-auth-providers-debug",
+ "python-sdk/fastmcp-server-auth-providers-descope",
+ "python-sdk/fastmcp-server-auth-providers-discord",
+ "python-sdk/fastmcp-server-auth-providers-github",
+ "python-sdk/fastmcp-server-auth-providers-google",
+ "python-sdk/fastmcp-server-auth-providers-in_memory",
+ "python-sdk/fastmcp-server-auth-providers-introspection",
+ "python-sdk/fastmcp-server-auth-providers-jwt",
+ "python-sdk/fastmcp-server-auth-providers-keycloak",
+ "python-sdk/fastmcp-server-auth-providers-oci",
+ "python-sdk/fastmcp-server-auth-providers-propelauth",
+ "python-sdk/fastmcp-server-auth-providers-scalekit",
+ "python-sdk/fastmcp-server-auth-providers-supabase",
+ "python-sdk/fastmcp-server-auth-providers-workos"
+ ]
+ },
+ "python-sdk/fastmcp-server-auth-redirect_validation",
+ "python-sdk/fastmcp-server-auth-ssrf"
+ ]
+ },
+ "python-sdk/fastmcp-server-context",
+ "python-sdk/fastmcp-server-dependencies",
+ "python-sdk/fastmcp-server-elicitation",
+ "python-sdk/fastmcp-server-event_store",
+ "python-sdk/fastmcp-server-http",
+ "python-sdk/fastmcp-server-lifespan",
+ "python-sdk/fastmcp-server-low_level",
+ {
+ "group": "middleware",
+ "pages": [
+ "python-sdk/fastmcp-server-middleware-__init__",
+ "python-sdk/fastmcp-server-middleware-authorization",
+ "python-sdk/fastmcp-server-middleware-caching",
+ "python-sdk/fastmcp-server-middleware-dereference",
+ "python-sdk/fastmcp-server-middleware-error_handling",
+ "python-sdk/fastmcp-server-middleware-logging",
+ "python-sdk/fastmcp-server-middleware-middleware",
+ "python-sdk/fastmcp-server-middleware-ping",
+ "python-sdk/fastmcp-server-middleware-rate_limiting",
+ "python-sdk/fastmcp-server-middleware-response_limiting",
+ "python-sdk/fastmcp-server-middleware-timing",
+ "python-sdk/fastmcp-server-middleware-tool_injection"
+ ]
+ },
+ {
+ "group": "mixins",
+ "pages": [
+ "python-sdk/fastmcp-server-mixins-__init__",
+ "python-sdk/fastmcp-server-mixins-lifespan",
+ "python-sdk/fastmcp-server-mixins-mcp_operations",
+ "python-sdk/fastmcp-server-mixins-transport"
+ ]
+ },
+ {
+ "group": "openapi",
+ "pages": [
+ "python-sdk/fastmcp-server-openapi-__init__",
+ "python-sdk/fastmcp-server-openapi-components",
+ "python-sdk/fastmcp-server-openapi-routing",
+ "python-sdk/fastmcp-server-openapi-server"
+ ]
+ },
+ {
+ "group": "providers",
+ "pages": [
+ "python-sdk/fastmcp-server-providers-__init__",
+ "python-sdk/fastmcp-server-providers-addressing",
+ "python-sdk/fastmcp-server-providers-aggregate",
+ "python-sdk/fastmcp-server-providers-base",
+ "python-sdk/fastmcp-server-providers-fastmcp_provider",
+ "python-sdk/fastmcp-server-providers-filesystem",
+ "python-sdk/fastmcp-server-providers-filesystem_discovery",
+ {
+ "group": "local_provider",
+ "pages": [
+ "python-sdk/fastmcp-server-providers-local_provider-__init__",
+ {
+ "group": "decorators",
+ "pages": [
+ "python-sdk/fastmcp-server-providers-local_provider-decorators-__init__",
+ "python-sdk/fastmcp-server-providers-local_provider-decorators-prompts",
+ "python-sdk/fastmcp-server-providers-local_provider-decorators-resources",
+ "python-sdk/fastmcp-server-providers-local_provider-decorators-tools"
+ ]
+ },
+ "python-sdk/fastmcp-server-providers-local_provider-local_provider"
+ ]
+ },
+ {
+ "group": "openapi",
+ "pages": [
+ "python-sdk/fastmcp-server-providers-openapi-__init__",
+ "python-sdk/fastmcp-server-providers-openapi-components",
+ "python-sdk/fastmcp-server-providers-openapi-provider",
+ "python-sdk/fastmcp-server-providers-openapi-routing"
+ ]
+ },
+ "python-sdk/fastmcp-server-providers-prefab_synthesis",
+ "python-sdk/fastmcp-server-providers-proxy",
+ {
+ "group": "skills",
+ "pages": [
+ "python-sdk/fastmcp-server-providers-skills-__init__",
+ "python-sdk/fastmcp-server-providers-skills-claude_provider",
+ "python-sdk/fastmcp-server-providers-skills-directory_provider",
+ "python-sdk/fastmcp-server-providers-skills-skill_provider",
+ "python-sdk/fastmcp-server-providers-skills-vendor_providers"
+ ]
+ },
+ "python-sdk/fastmcp-server-providers-wrapped_provider"
+ ]
+ },
+ "python-sdk/fastmcp-server-proxy",
+ {
+ "group": "sampling",
+ "pages": [
+ "python-sdk/fastmcp-server-sampling-__init__",
+ "python-sdk/fastmcp-server-sampling-run",
+ "python-sdk/fastmcp-server-sampling-sampling_tool"
+ ]
+ },
+ "python-sdk/fastmcp-server-server",
+ {
+ "group": "tasks",
+ "pages": [
+ "python-sdk/fastmcp-server-tasks-__init__",
+ "python-sdk/fastmcp-server-tasks-capabilities",
+ "python-sdk/fastmcp-server-tasks-config",
+ "python-sdk/fastmcp-server-tasks-context",
+ "python-sdk/fastmcp-server-tasks-elicitation",
+ "python-sdk/fastmcp-server-tasks-handlers",
+ "python-sdk/fastmcp-server-tasks-keys",
+ "python-sdk/fastmcp-server-tasks-notifications",
+ "python-sdk/fastmcp-server-tasks-requests",
+ "python-sdk/fastmcp-server-tasks-routing",
+ "python-sdk/fastmcp-server-tasks-subscriptions"
+ ]
+ },
+ "python-sdk/fastmcp-server-telemetry",
+ {
+ "group": "transforms",
+ "pages": [
+ "python-sdk/fastmcp-server-transforms-__init__",
+ "python-sdk/fastmcp-server-transforms-catalog",
+ "python-sdk/fastmcp-server-transforms-namespace",
+ "python-sdk/fastmcp-server-transforms-prompts_as_tools",
+ "python-sdk/fastmcp-server-transforms-resources_as_tools",
+ {
+ "group": "search",
+ "pages": [
+ "python-sdk/fastmcp-server-transforms-search-__init__",
+ "python-sdk/fastmcp-server-transforms-search-base",
+ "python-sdk/fastmcp-server-transforms-search-bm25",
+ "python-sdk/fastmcp-server-transforms-search-regex"
+ ]
+ },
+ "python-sdk/fastmcp-server-transforms-tool_transform",
+ "python-sdk/fastmcp-server-transforms-version_filter",
+ "python-sdk/fastmcp-server-transforms-visibility"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "fastmcp.tools",
+ "pages": [
+ "python-sdk/fastmcp-tools-__init__",
+ "python-sdk/fastmcp-tools-base",
+ "python-sdk/fastmcp-tools-function_parsing",
+ "python-sdk/fastmcp-tools-function_tool",
+ "python-sdk/fastmcp-tools-tool_transform"
+ ]
+ },
+ {
+ "group": "fastmcp.utilities",
+ "pages": [
+ "python-sdk/fastmcp-utilities-__init__",
+ "python-sdk/fastmcp-utilities-async_utils",
+ "python-sdk/fastmcp-utilities-auth",
+ "python-sdk/fastmcp-utilities-cli",
+ "python-sdk/fastmcp-utilities-components",
+ "python-sdk/fastmcp-utilities-docstring_parsing",
+ "python-sdk/fastmcp-utilities-exceptions",
+ "python-sdk/fastmcp-utilities-http",
+ "python-sdk/fastmcp-utilities-inspect",
+ "python-sdk/fastmcp-utilities-json_schema",
+ "python-sdk/fastmcp-utilities-json_schema_type",
+ "python-sdk/fastmcp-utilities-lifespan",
+ "python-sdk/fastmcp-utilities-logging",
+ {
+ "group": "mcp_server_config",
+ "pages": [
+ "python-sdk/fastmcp-utilities-mcp_server_config-__init__",
+ {
+ "group": "v1",
+ "pages": [
+ "python-sdk/fastmcp-utilities-mcp_server_config-v1-__init__",
+ {
+ "group": "environments",
+ "pages": [
+ "python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-__init__",
+ "python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-base",
+ "python-sdk/fastmcp-utilities-mcp_server_config-v1-environments-uv"
+ ]
+ },
+ "python-sdk/fastmcp-utilities-mcp_server_config-v1-mcp_server_config",
+ {
+ "group": "sources",
+ "pages": [
+ "python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-__init__",
+ "python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-base",
+ "python-sdk/fastmcp-utilities-mcp_server_config-v1-sources-filesystem"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ "python-sdk/fastmcp-utilities-mime",
+ {
+ "group": "openapi",
+ "pages": [
+ "python-sdk/fastmcp-utilities-openapi-__init__",
+ "python-sdk/fastmcp-utilities-openapi-director",
+ "python-sdk/fastmcp-utilities-openapi-formatters",
+ "python-sdk/fastmcp-utilities-openapi-json_schema_converter",
+ "python-sdk/fastmcp-utilities-openapi-models",
+ "python-sdk/fastmcp-utilities-openapi-parser",
+ "python-sdk/fastmcp-utilities-openapi-schemas"
+ ]
+ },
+ "python-sdk/fastmcp-utilities-pagination",
+ "python-sdk/fastmcp-utilities-skills",
+ "python-sdk/fastmcp-utilities-tests",
+ "python-sdk/fastmcp-utilities-timeout",
+ "python-sdk/fastmcp-utilities-token_cache",
+ "python-sdk/fastmcp-utilities-types",
+ "python-sdk/fastmcp-utilities-ui",
+ "python-sdk/fastmcp-utilities-version_check",
+ "python-sdk/fastmcp-utilities-versions"
+ ]
+ }
+]
diff --git a/docs/python-sdk/fastmcp-apps-app.mdx b/docs/python-sdk/fastmcp-apps-app.mdx
index 04578add6..e6053277e 100644
--- a/docs/python-sdk/fastmcp-apps-app.mdx
+++ b/docs/python-sdk/fastmcp-apps-app.mdx
@@ -35,7 +35,7 @@ Usage::
## Classes
-### `FastMCPApp`
+### `FastMCPApp`
A Provider that represents an MCP application.
@@ -48,19 +48,19 @@ can find them by original name even when transforms have been applied.
**Methods:**
-#### `tool`
+#### `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) -> Any
@@ -83,19 +83,19 @@ Supports multiple calling patterns::
def save(name: str): ...
-#### `ui`
+#### `ui`
```python
ui(self, name_or_fn: F) -> F
```
-#### `ui`
+#### `ui`
```python
ui(self, name_or_fn: str | None = None) -> Callable[[F], F]
```
-#### `ui`
+#### `ui`
```python
ui(self, name_or_fn: str | AnyFunction | None = None) -> Any
@@ -119,7 +119,7 @@ Supports multiple calling patterns::
def dashboard() -> Component: ...
-#### `add_tool`
+#### `add_tool`
```python
add_tool(self, tool: Tool | Callable[..., Any]) -> Tool
@@ -130,13 +130,13 @@ Add a tool to this app programmatically.
The tool is tagged with this app's name for routing.
-#### `lifespan`
+#### `lifespan`
```python
lifespan(self) -> AsyncIterator[None]
```
-#### `run`
+#### `run`
```python
run(self, transport: Literal['stdio', 'http', 'sse', 'streamable-http'] | None = None, **kwargs: Any) -> None
diff --git a/docs/python-sdk/fastmcp-apps-file_upload.mdx b/docs/python-sdk/fastmcp-apps-file_upload.mdx
index 9705e335b..a77d9fa6a 100644
--- a/docs/python-sdk/fastmcp-apps-file_upload.mdx
+++ b/docs/python-sdk/fastmcp-apps-file_upload.mdx
@@ -40,7 +40,7 @@ For custom persistence, override the storage methods::
## Classes
-### `FileUpload`
+### `FileUpload`
A Provider that adds file upload capabilities to a server.
@@ -77,7 +77,7 @@ Example::
**Methods:**
-#### `on_store`
+#### `on_store`
```python
on_store(self, files: list[dict[str, Any]], ctx: Context) -> list[dict[str, Any]]
@@ -100,7 +100,7 @@ implementation stores files in memory, scoped by
- ``size_display``, ``uploaded_at``).
-#### `on_list`
+#### `on_list`
```python
on_list(self, ctx: Context) -> list[dict[str, Any]]
@@ -118,7 +118,7 @@ implementation returns files from the current scope.
- List of file summary dicts.
-#### `on_read`
+#### `on_read`
```python
on_read(self, name: str, ctx: Context) -> dict[str, Any]
diff --git a/docs/python-sdk/fastmcp-apps-form.mdx b/docs/python-sdk/fastmcp-apps-form.mdx
index edf6a72c9..1f7b5c478 100644
--- a/docs/python-sdk/fastmcp-apps-form.mdx
+++ b/docs/python-sdk/fastmcp-apps-form.mdx
@@ -32,7 +32,7 @@ Usage::
## Classes
-### `FormInput`
+### `FormInput`
A Provider that collects structured input via a Pydantic model.
diff --git a/docs/python-sdk/fastmcp-cli-apps_dev.mdx b/docs/python-sdk/fastmcp-cli-apps_dev.mdx
index 2f38bbc5b..2cbe0fd3e 100644
--- a/docs/python-sdk/fastmcp-cli-apps_dev.mdx
+++ b/docs/python-sdk/fastmcp-cli-apps_dev.mdx
@@ -32,7 +32,7 @@ Startup sequence
## Functions
-### `run_dev_apps`
+### `run_dev_apps`
```python
run_dev_apps(server_spec: str) -> None
diff --git a/docs/python-sdk/fastmcp-cli-cli.mdx b/docs/python-sdk/fastmcp-cli-cli.mdx
index b8bf7e0de..d396af571 100644
--- a/docs/python-sdk/fastmcp-cli-cli.mdx
+++ b/docs/python-sdk/fastmcp-cli-cli.mdx
@@ -91,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
@@ -122,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-generate.mdx b/docs/python-sdk/fastmcp-cli-generate.mdx
index ebedaa186..0d9285189 100644
--- a/docs/python-sdk/fastmcp-cli-generate.mdx
+++ b/docs/python-sdk/fastmcp-cli-generate.mdx
@@ -33,7 +33,7 @@ generate_cli_script(server_name: str, server_spec: str, transport_code: str, ext
Generate the full CLI script source code.
-### `generate_skill_content`
+### `generate_skill_content`
```python
generate_skill_content(server_name: str, cli_filename: str, tools: list[mcp.types.Tool]) -> str
@@ -43,7 +43,7 @@ generate_skill_content(server_name: str, cli_filename: str, tools: list[mcp.type
Generate a SKILL.md file for a generated CLI script.
-### `generate_cli_command`
+### `generate_cli_command`
```python
generate_cli_command(server_spec: Annotated[str, cyclopts.Parameter(help='Server URL, Python file, MCPConfig JSON, discovered name, or .js file')], output: Annotated[str, cyclopts.Parameter(help='Output file path (default: cli.py)')] = 'cli.py') -> None
diff --git a/docs/python-sdk/fastmcp-cli-install-goose.mdx b/docs/python-sdk/fastmcp-cli-install-goose.mdx
index cd2a8cc9a..af5aed24c 100644
--- a/docs/python-sdk/fastmcp-cli-install-goose.mdx
+++ b/docs/python-sdk/fastmcp-cli-install-goose.mdx
@@ -29,7 +29,7 @@ Generate a Goose deeplink for installing an MCP extension.
- A goose://extension?... deeplink URL.
-### `install_goose`
+### `install_goose`
```python
install_goose(file: Path, server_object: str | None, name: str) -> bool
@@ -49,7 +49,7 @@ Install FastMCP server in Goose via deeplink.
- True if installation was successful, False otherwise.
-### `goose_command`
+### `goose_command`
```python
goose_command(server_spec: str) -> None
diff --git a/docs/python-sdk/fastmcp-cli-run.mdx b/docs/python-sdk/fastmcp-cli-run.mdx
index 0b341c459..1b7af37a5 100644
--- a/docs/python-sdk/fastmcp-cli-run.mdx
+++ b/docs/python-sdk/fastmcp-cli-run.mdx
@@ -10,7 +10,7 @@ FastMCP run command implementation with enhanced type hints.
## Functions
-### `is_url`
+### `is_url`
```python
is_url(path: str) -> bool
@@ -20,7 +20,7 @@ is_url(path: str) -> bool
Check if a string is a URL.
-### `create_client_server`
+### `create_client_server`
```python
create_client_server(url: str) -> Any
@@ -36,7 +36,7 @@ Create a FastMCP server from a client URL.
- A FastMCP server instance
-### `create_mcp_config_server`
+### `create_mcp_config_server`
```python
create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]
@@ -46,7 +46,7 @@ create_mcp_config_server(mcp_config_path: Path) -> FastMCP[None]
Create a FastMCP server from a MCPConfig.
-### `load_mcp_server_config`
+### `load_mcp_server_config`
```python
load_mcp_server_config(config_path: Path) -> MCPServerConfig
@@ -62,7 +62,7 @@ Load a FastMCP configuration from a fastmcp.json file.
- MCPServerConfig object
-### `run_command`
+### `run_command`
```python
run_command(server_spec: str, transport: TransportType | None = None, host: str | None = None, port: int | None = None, path: str | None = None, log_level: LogLevelType | None = None, server_args: list[str] | None = None, show_banner: bool = True, use_direct_import: bool = False, skip_source: bool = False, stateless: bool = False) -> None
@@ -85,7 +85,7 @@ Run a MCP server or connect to a remote one.
- `stateless`: Whether to run in stateless mode (no session)
-### `run_module_command`
+### `run_module_command`
```python
run_module_command(module_name: str) -> None
@@ -104,7 +104,7 @@ with environment setup (e.g. ``UVEnvironment.build_command``).
- `extra_args`: Extra arguments forwarded after the module name.
-### `run_v1_server_async`
+### `run_v1_server_async`
```python
run_v1_server_async(server: FastMCP1x, host: str | None = None, port: int | None = None, transport: TransportType | None = None) -> None
@@ -120,7 +120,7 @@ Run a FastMCP 1.x server using async methods.
- `transport`: Transport protocol to use
-### `run_with_reload`
+### `run_with_reload`
```python
run_with_reload(cmd: list[str], reload_dirs: list[Path] | None = None, is_stdio: bool = False) -> None
diff --git a/docs/python-sdk/fastmcp-client-client.mdx b/docs/python-sdk/fastmcp-client-client.mdx
index efbf56f08..45158d509 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 f91e79a9b..6a859f06a 100644
--- a/docs/python-sdk/fastmcp-client-mixins-prompts.mdx
+++ b/docs/python-sdk/fastmcp-client-mixins-prompts.mdx
@@ -38,7 +38,7 @@ 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, max_pages: int = AUTO_PAGINATION_MAX_PAGES) -> list[mcp.types.Prompt]
@@ -61,7 +61,7 @@ large result sets incrementally), use list_prompts_mcp() with the cursor paramet
- `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
@@ -83,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 70f07c7e7..0f1f81d88 100644
--- a/docs/python-sdk/fastmcp-client-mixins-resources.mdx
+++ b/docs/python-sdk/fastmcp-client-mixins-resources.mdx
@@ -38,7 +38,7 @@ 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, max_pages: int = AUTO_PAGINATION_MAX_PAGES) -> list[mcp.types.Resource]
@@ -61,7 +61,7 @@ large result sets incrementally), use list_resources_mcp() with the cursor param
- `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
@@ -81,7 +81,7 @@ 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, max_pages: int = AUTO_PAGINATION_MAX_PAGES) -> list[mcp.types.ResourceTemplate]
@@ -105,7 +105,7 @@ cursor parameter.
- `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
@@ -126,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 8711bfeb8..14e4c208a 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,7 +38,7 @@ 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, max_pages: int = AUTO_PAGINATION_MAX_PAGES) -> list[mcp.types.Tool]
@@ -61,7 +61,7 @@ large result sets incrementally), use list_tools_mcp() with the cursor parameter
- `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
@@ -91,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-tasks.mdx b/docs/python-sdk/fastmcp-client-tasks.mdx
index 547d9bc92..fe2cb8b40 100644
--- a/docs/python-sdk/fastmcp-client-tasks.mdx
+++ b/docs/python-sdk/fastmcp-client-tasks.mdx
@@ -10,7 +10,7 @@ SEP-1686 client Task classes.
## Classes
-### `TaskNotificationHandler`
+### `TaskNotificationHandler`
MessageHandler that routes task status notifications to Task objects.
@@ -18,7 +18,7 @@ MessageHandler that routes task status notifications to Task objects.
**Methods:**
-#### `dispatch`
+#### `dispatch`
```python
dispatch(self, message: Message) -> None
@@ -27,7 +27,7 @@ dispatch(self, message: Message) -> None
Dispatch messages, including task status notifications.
-### `Task`
+### `Task`
Abstract base class for MCP background tasks (SEP-1686).
@@ -38,7 +38,7 @@ or executes synchronously (graceful degradation per SEP-1686).
**Methods:**
-#### `task_id`
+#### `task_id`
```python
task_id(self) -> str
@@ -47,7 +47,7 @@ task_id(self) -> str
Get the task ID.
-#### `returned_immediately`
+#### `returned_immediately`
```python
returned_immediately(self) -> bool
@@ -60,7 +60,7 @@ Check if server executed the task immediately.
- False if server accepted background execution
-#### `on_status_change`
+#### `on_status_change`
```python
on_status_change(self, callback: Callable[[GetTaskResult], None | Awaitable[None]]) -> None
@@ -78,7 +78,7 @@ Supports both sync and async callbacks (auto-detected).
Can return None (sync) or Awaitable[None] (async).
-#### `status`
+#### `status`
```python
status(self) -> GetTaskResult
@@ -90,7 +90,7 @@ If server executed immediately, returns synthetic completed status.
Otherwise queries the server for current status.
-#### `result`
+#### `result`
```python
result(self) -> TaskResultT
@@ -101,7 +101,7 @@ Wait for and return the task result.
Must be implemented by subclasses to return the appropriate result type.
-#### `wait`
+#### `wait`
```python
wait(self) -> GetTaskResult
@@ -114,8 +114,8 @@ with fallback to polling (reliable). Optimally wakes up immediately
on status changes when server sends notifications/tasks/status.
**Args:**
-- `state`: Desired state ('submitted', 'working', 'completed', 'failed').
- If None, waits for any terminal state (completed/failed)
+- `state`: Desired state ('working', 'input_required', 'completed', 'failed', 'cancelled').
+ If None, waits until the task exits the 'working' state (completed, failed, cancelled, input_required, etc.)
- `timeout`: Maximum time to wait in seconds
**Returns:**
@@ -125,7 +125,7 @@ on status changes when server sends notifications/tasks/status.
- `TimeoutError`: If desired state not reached within timeout
-#### `cancel`
+#### `cancel`
```python
cancel(self) -> None
@@ -140,7 +140,7 @@ Note: If server executed immediately (graceful degradation), this is a no-op
as there's no server-side task to cancel.
-### `ToolTask`
+### `ToolTask`
Represents a tool call that may execute in background or immediately.
@@ -151,7 +151,7 @@ or executes synchronously (graceful degradation per SEP-1686).
**Methods:**
-#### `result`
+#### `result`
```python
result(self) -> CallToolResult
@@ -166,7 +166,7 @@ Otherwise waits for background task to complete and retrieves result.
- The parsed tool result (same as call_tool returns)
-### `PromptTask`
+### `PromptTask`
Represents a prompt call that may execute in background or immediately.
@@ -177,7 +177,7 @@ or executes synchronously (graceful degradation per SEP-1686).
**Methods:**
-#### `result`
+#### `result`
```python
result(self) -> mcp.types.GetPromptResult
@@ -192,7 +192,7 @@ Otherwise waits for background task to complete and retrieves result.
- The prompt result with messages and description
-### `ResourceTask`
+### `ResourceTask`
Represents a resource read that may execute in background or immediately.
@@ -203,7 +203,7 @@ or executes synchronously (graceful degradation per SEP-1686).
**Methods:**
-#### `result`
+#### `result`
```python
result(self) -> list[mcp.types.TextResourceContents | mcp.types.BlobResourceContents]
diff --git a/docs/python-sdk/fastmcp-client-telemetry.mdx b/docs/python-sdk/fastmcp-client-telemetry.mdx
index 9a1f8d260..843d97631 100644
--- a/docs/python-sdk/fastmcp-client-telemetry.mdx
+++ b/docs/python-sdk/fastmcp-client-telemetry.mdx
@@ -10,10 +10,10 @@ Client-side telemetry helpers.
## Functions
-### `client_span`
+### `client_span`
```python
-client_span(name: str, method: str, component_key: str, session_id: str | None = None, resource_uri: str | None = None) -> Generator[Span, None, None]
+client_span(name: str, method: str, component_key: str, session_id: str | None = None, resource_uri: str | None = None, tool_name: str | None = None, prompt_name: str | None = None) -> Generator[Span, None, None]
```
diff --git a/docs/python-sdk/fastmcp-client-transports-http.mdx b/docs/python-sdk/fastmcp-client-transports-http.mdx
index a0db1401d..48a9559e3 100644
--- a/docs/python-sdk/fastmcp-client-transports-http.mdx
+++ b/docs/python-sdk/fastmcp-client-transports-http.mdx
@@ -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 a65dace46..6a78d9325 100644
--- a/docs/python-sdk/fastmcp-client-transports-sse.mdx
+++ b/docs/python-sdk/fastmcp-client-transports-sse.mdx
@@ -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-exceptions.mdx b/docs/python-sdk/fastmcp-exceptions.mdx
index 3494751f7..e662031a0 100644
--- a/docs/python-sdk/fastmcp-exceptions.mdx
+++ b/docs/python-sdk/fastmcp-exceptions.mdx
@@ -10,7 +10,7 @@ Custom exceptions for FastMCP.
## Classes
-### `FastMCPDeprecationWarning`
+### `FastMCPDeprecationWarning`
Deprecation warning for FastMCP APIs.
@@ -20,61 +20,61 @@ still apply, but FastMCP can selectively enable its own warnings
without affecting other libraries in the process.
-### `FastMCPError`
+### `FastMCPError`
Base error for FastMCP.
-### `ValidationError`
+### `ValidationError`
Error in validating parameters or return values.
-### `ResourceError`
+### `ResourceError`
Error in resource operations.
-### `ToolError`
+### `ToolError`
Error in tool operations.
-### `PromptError`
+### `PromptError`
Error in prompt operations.
-### `InvalidSignature`
+### `InvalidSignature`
Invalid signature for use with FastMCP.
-### `ClientError`
+### `ClientError`
Error in client operations.
-### `NotFoundError`
+### `NotFoundError`
Object not found.
-### `DisabledError`
+### `DisabledError`
Object is disabled.
-### `AuthorizationError`
+### `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 9dc66b06a..4b954336b 100644
--- a/docs/python-sdk/fastmcp-experimental-transforms-code_mode.mdx
+++ b/docs/python-sdk/fastmcp-experimental-transforms-code_mode.mdx
@@ -7,7 +7,7 @@ sidebarTitle: code_mode
## Classes
-### `SandboxProvider`
+### `SandboxProvider`
Interface for executing LLM-generated Python code in a sandbox.
@@ -20,13 +20,13 @@ sandbox — never with plain ``exec()``. Use ``MontySandboxProvider``
**Methods:**
-#### `run`
+#### `run`
```python
run(self, code: str) -> Any
```
-### `MontySandboxProvider`
+### `MontySandboxProvider`
Sandbox provider backed by `pydantic-monty`.
@@ -41,13 +41,13 @@ leave that limit uncapped.
**Methods:**
-#### `run`
+#### `run`
```python
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-function_prompt.mdx b/docs/python-sdk/fastmcp-prompts-function_prompt.mdx
index 92f369d78..52082d732 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
@@ -83,11 +83,8 @@ register_with_docket(self, docket: Docket) -> None
Register this prompt with docket for background execution.
-FunctionPrompt registers the underlying function, which has the user's
-Depends parameters for docket to resolve.
-
-#### `add_to_docket`
+#### `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-base.mdx b/docs/python-sdk/fastmcp-resources-base.mdx
index aab4a1dd7..d9f76a057 100644
--- a/docs/python-sdk/fastmcp-resources-base.mdx
+++ b/docs/python-sdk/fastmcp-resources-base.mdx
@@ -10,7 +10,7 @@ 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 4977f0d58..24aa2052c 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]
@@ -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
@@ -88,6 +88,3 @@ register_with_docket(self, docket: Docket) -> None
Register this resource with docket for background execution.
-FunctionResource registers the underlying function, which has the user's
-Depends parameters for docket to resolve.
-
diff --git a/docs/python-sdk/fastmcp-resources-template.mdx b/docs/python-sdk/fastmcp-resources-template.mdx
index 06d8ef8ed..6c96bacaf 100644
--- a/docs/python-sdk/fastmcp-resources-template.mdx
+++ b/docs/python-sdk/fastmcp-resources-template.mdx
@@ -34,11 +34,14 @@ Supports:
- `{var*}` - wildcard path parameter (captures multiple segments)
- `{?var1,var2}` - query parameters (ignored in path matching)
+Hyphens in parameter names are normalized to underscores in regex group
+names so that matched groups are valid Python identifiers.
+
Returns None if the template produces an invalid regex (e.g. parameter
-names with hyphens, leading digits, or duplicates from a remote server).
+names with 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
@@ -52,9 +55,23 @@ Supports RFC 6570 URI templates:
- Query params: `{?var1,var2}`
+### `expand_uri_template`
+
+```python
+expand_uri_template(uri_template: str, params: dict[str, Any]) -> str
+```
+
+
+Expand a URI template with parameters — inverse of `match_uri_template`.
+
+Supports the same RFC 6570 subset:
+- Path params: `{var}`, `{var*}`
+- Query params: `{?var1,var2}`
+
+
## Classes
-### `ResourceTemplate`
+### `ResourceTemplate`
A template for dynamically creating resources.
@@ -62,13 +79,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
@@ -77,7 +94,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
@@ -86,7 +103,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
@@ -95,7 +112,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
@@ -111,7 +128,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
@@ -123,7 +140,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
@@ -132,7 +149,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
@@ -141,7 +158,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
@@ -150,7 +167,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
@@ -159,7 +176,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
@@ -175,13 +192,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.
@@ -189,7 +206,7 @@ A template for dynamically creating resources.
**Methods:**
-#### `create_resource`
+#### `create_resource`
```python
create_resource(self, uri: str, params: dict[str, Any]) -> Resource
@@ -198,7 +215,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
@@ -207,7 +224,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
@@ -215,11 +232,8 @@ register_with_docket(self, docket: Docket) -> None
Register this template with docket for background execution.
-FunctionResourceTemplate registers the underlying function, which has the
-user's Depends parameters for docket to resolve.
-
-#### `add_to_docket`
+#### `add_to_docket`
```python
add_to_docket(self, docket: Docket, params: dict[str, Any], **kwargs: Any) -> Execution
@@ -237,7 +251,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-server-auth-auth.mdx b/docs/python-sdk/fastmcp-server-auth-auth.mdx
index def0830fd..8f7954cd9 100644
--- a/docs/python-sdk/fastmcp-server-auth-auth.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-auth.mdx
@@ -85,7 +85,7 @@ custom authentication routes.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -102,7 +102,7 @@ All auth providers must implement token verification.
- AccessToken object if valid, None if invalid or expired
-#### `set_mcp_path`
+#### `set_mcp_path`
```python
set_mcp_path(self, mcp_path: str | None) -> None
@@ -119,7 +119,7 @@ MCP endpoint path.
- `mcp_path`: The path where the MCP endpoint is mounted (e.g., "/mcp")
-#### `get_routes`
+#### `get_routes`
```python
get_routes(self, mcp_path: str | None = None) -> list[Route]
@@ -143,7 +143,7 @@ provider does not create the actual MCP endpoint route.
- List of all routes for this provider (excluding the MCP endpoint itself)
-#### `get_well_known_routes`
+#### `get_well_known_routes`
```python
get_well_known_routes(self, mcp_path: str | None = None) -> list[Route]
@@ -171,7 +171,7 @@ This is used to construct path-scoped well-known URLs.
- List of well-known discovery routes (typically mounted at root level)
-#### `get_middleware`
+#### `get_middleware`
```python
get_middleware(self) -> list
@@ -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-handlers-__init__.mdx b/docs/python-sdk/fastmcp-server-auth-handlers-__init__.mdx
new file mode 100644
index 000000000..7593775fb
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-auth-handlers-__init__.mdx
@@ -0,0 +1,8 @@
+---
+title: __init__
+sidebarTitle: __init__
+---
+
+# `fastmcp.server.auth.handlers`
+
+*This module is empty or contains only private/internal implementations.*
diff --git a/docs/python-sdk/fastmcp-server-auth-handlers-authorize.mdx b/docs/python-sdk/fastmcp-server-auth-handlers-authorize.mdx
new file mode 100644
index 000000000..f3f583027
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-auth-handlers-authorize.mdx
@@ -0,0 +1,83 @@
+---
+title: authorize
+sidebarTitle: authorize
+---
+
+# `fastmcp.server.auth.handlers.authorize`
+
+
+Enhanced authorization handler with improved error responses.
+
+This module provides an enhanced authorization handler that wraps the MCP SDK's
+AuthorizationHandler to provide better error messages when clients attempt to
+authorize with unregistered client IDs.
+
+The enhancement adds:
+- Content negotiation: HTML for browsers, JSON for API clients
+- Enhanced JSON responses with registration endpoint hints
+- Styled HTML error pages with registration links/forms
+- Link headers pointing to registration endpoints
+
+
+## Functions
+
+### `create_unregistered_client_html`
+
+```python
+create_unregistered_client_html(client_id: str, registration_endpoint: str, discovery_endpoint: str, server_name: str | None = None, server_icon_url: str | None = None, title: str = 'Client Not Registered') -> str
+```
+
+
+Create styled HTML error page for unregistered client attempts.
+
+**Args:**
+- `client_id`: The unregistered client ID that was provided
+- `registration_endpoint`: URL of the registration endpoint
+- `discovery_endpoint`: URL of the OAuth metadata discovery endpoint
+- `server_name`: Optional server name for branding
+- `server_icon_url`: Optional server icon URL
+- `title`: Page title
+
+**Returns:**
+- HTML string for the error page
+
+
+## Classes
+
+### `AuthorizationHandler`
+
+
+Authorization handler with enhanced error responses for unregistered clients.
+
+This handler extends the MCP SDK's AuthorizationHandler to provide better UX
+when clients attempt to authorize without being registered. It implements
+content negotiation to return:
+
+- HTML error pages for browser requests
+- Enhanced JSON with registration hints for API clients
+- Link headers pointing to registration endpoints
+
+This maintains OAuth 2.1 compliance (returns 400 for invalid client_id)
+while providing actionable guidance to fix the error.
+
+
+**Methods:**
+
+#### `handle`
+
+```python
+handle(self, request: Request) -> Response
+```
+
+Handle authorization request with enhanced error responses.
+
+This method extends the SDK's authorization handler and intercepts
+errors for unregistered clients to provide better error responses
+based on the client's Accept header.
+
+**Args:**
+- `request`: The authorization request
+
+**Returns:**
+- Response (redirect on success, error response on failure)
+
diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-consent.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy-consent.mdx
index 67c514ee7..72549626d 100644
--- a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-consent.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-oauth_proxy-consent.mdx
@@ -15,7 +15,7 @@ cookie management, and consent page rendering.
## Classes
-### `ConsentMixin`
+### `ConsentMixin`
Mixin class providing consent management functionality for OAuthProxy.
diff --git a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-models.mdx b/docs/python-sdk/fastmcp-server-auth-oauth_proxy-models.mdx
index a096e9e45..df32a5b2b 100644
--- a/docs/python-sdk/fastmcp-server-auth-oauth_proxy-models.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-oauth_proxy-models.mdx
@@ -13,7 +13,7 @@ This module contains all Pydantic models and constants used by the OAuth proxy.
## Classes
-### `OAuthTransaction`
+### `OAuthTransaction`
OAuth transaction state for consent flow.
@@ -22,7 +22,7 @@ Stored server-side to track active authorization flows with client context.
Includes CSRF tokens for consent protection per MCP security best practices.
-### `ClientCode`
+### `ClientCode`
Client authorization code with PKCE and upstream tokens.
@@ -31,7 +31,7 @@ Stored server-side after upstream IdP callback. Contains the upstream
tokens bound to the client's PKCE challenge for secure token exchange.
-### `UpstreamTokenSet`
+### `UpstreamTokenSet`
Stored upstream OAuth tokens from identity provider.
@@ -41,7 +41,7 @@ and stored in plaintext within this model. Encryption is handled transparently
at the storage layer via FernetEncryptionWrapper. Tokens are never exposed to MCP clients.
-### `JTIMapping`
+### `JTIMapping`
Maps FastMCP token JTI to upstream token ID.
@@ -50,7 +50,7 @@ This allows stateless JWT validation while still being able to look up
the corresponding upstream token when tools need to access upstream APIs.
-### `RefreshTokenMetadata`
+### `RefreshTokenMetadata`
Metadata for a refresh token, stored keyed by token hash.
@@ -59,7 +59,7 @@ We store only metadata (not the token itself) for security - if storage
is compromised, attackers get hashes they can't reverse into usable tokens.
-### `ProxyDCRClient`
+### `ProxyDCRClient`
Client for DCR proxy with configurable redirect URI validation.
@@ -89,7 +89,7 @@ arise from accepting arbitrary redirect URIs.
**Methods:**
-#### `validate_redirect_uri`
+#### `validate_redirect_uri`
```python
validate_redirect_uri(self, redirect_uri: AnyUrl | None) -> AnyUrl
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 2f7742f26..887622070 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
@@ -210,11 +210,13 @@ Flow:
3. Return local /consent URL; browser visits consent first
4. Consent handler redirects to upstream IdP if approved/already approved
-If consent is disabled (require_authorization_consent=False), skip the consent screen
-and redirect directly to the upstream IdP.
+If consent is disabled (require_authorization_consent=False or "external"),
+skip the consent screen and redirect directly to the upstream IdP. In
+"remember" mode, still route through /consent so the cookie lookup and
+Sec-Fetch-Site gating can run.
-#### `load_authorization_code`
+#### `load_authorization_code`
```python
load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None
@@ -226,7 +228,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 +246,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 +258,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 +275,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
@@ -293,7 +295,7 @@ 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
@@ -306,7 +308,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 a9160db17..3bcbc3cb2 100644
--- a/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-oidc_proxy.mdx
@@ -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-aws.mdx b/docs/python-sdk/fastmcp-server-auth-providers-aws.mdx
index c8d9ea795..d3201311e 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-aws.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-aws.mdx
@@ -34,12 +34,17 @@ Example:
### `AWSCognitoTokenVerifier`
-Token verifier that filters claims to Cognito-specific subset.
+Token verifier for Cognito access tokens.
+
+Cognito access tokens use a ``client_id`` claim instead of the
+standard ``aud`` claim. This subclass passes ``audience=None``
+to the parent (skipping the ``aud`` check) and validates the
+``client_id`` claim directly.
**Methods:**
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -48,7 +53,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,7 +71,7 @@ Features:
**Methods:**
-#### `get_token_verifier`
+#### `get_token_verifier`
```python
get_token_verifier(self) -> AWSCognitoTokenVerifier
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx b/docs/python-sdk/fastmcp-server-auth-providers-azure.mdx
index 6301e4c28..3eaf5fef4 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,45 @@ Setup:
**Methods:**
-#### `authorize`
+#### `from_b2c`
+
+```python
+from_b2c(cls, **kwargs: Any) -> AzureProvider
+```
+
+Create an AzureProvider pre-configured for Azure AD B2C.
+
+Derives authority host, tenant path, and identifier URI from
+`tenant_name` and `policy_name`, then delegates to the standard
+constructor. Returns a plain `AzureProvider` instance.
+
+B2C issuer validation is disabled by default (`token_issuer=None`)
+because B2C issuers embed the tenant GUID. Pass an explicit
+`token_issuer` string once you know the real `iss` value.
+
+Azure AD B2C does **not** support OBO.
+
+**Args:**
+- `tenant_name`: Short B2C tenant name without `.onmicrosoft.com`
+(e.g. `"mytenant"`).
+- `policy_name`: User-flow or custom-policy name
+(e.g. `"B2C_1_susi"`).
+- `client_id`: Application (client) ID from the B2C app registration.
+- `client_secret`: Client secret from the B2C app registration.
+- `required_scopes`: Custom API scope names without prefix
+(e.g. `["mcp-access"]`).
+- `base_url`: Public base URL of this server.
+- `custom_domain`: Custom domain for the B2C authority
+(e.g. `"auth.mycompany.com"`). Defaults to
+`{tenant_name}.b2clogin.com`.
+- `identifier_uri`: Application ID URI. Defaults to
+`https\://{tenant_name}.onmicrosoft.com/{client_id}`.
+- `token_issuer`: Expected `iss` claim. `None` (default) disables
+issuer validation.
+- `**kwargs`: Forwarded to `AzureProvider.__init__`.
+
+
+#### `authorize`
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
@@ -98,7 +136,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
@@ -117,10 +155,11 @@ calls multiple tools with the same scopes.
- A configured OnBehalfOfCredential ready for get_token() calls.
**Raises:**
+- `NotImplementedError`: If OBO is not supported (e.g. Azure AD B2C).
- `ImportError`: If azure-identity is not installed (requires fastmcp[azure]).
-#### `close_obo_credentials`
+#### `close_obo_credentials`
```python
close_obo_credentials(self) -> None
@@ -129,7 +168,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 +205,7 @@ Example::
**Methods:**
-#### `scopes_supported`
+#### `scopes_supported`
```python
scopes_supported(self) -> list[str]
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx b/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx
index c4ee4cc14..0e1e0d94e 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-in_memory.mdx
@@ -16,19 +16,19 @@ It simulates the OAuth 2.1 flow locally without external calls.
**Methods:**
-#### `get_client`
+#### `get_client`
```python
get_client(self, client_id: str) -> OAuthClientInformationFull | None
```
-#### `register_client`
+#### `register_client`
```python
register_client(self, client_info: OAuthClientInformationFull) -> None
```
-#### `authorize`
+#### `authorize`
```python
authorize(self, client: OAuthClientInformationFull, params: AuthorizationParams) -> str
@@ -38,37 +38,37 @@ Simulates user authorization and generates an authorization code.
Returns a redirect URI with the code and state.
-#### `load_authorization_code`
+#### `load_authorization_code`
```python
load_authorization_code(self, client: OAuthClientInformationFull, authorization_code: str) -> AuthorizationCode | None
```
-#### `exchange_authorization_code`
+#### `exchange_authorization_code`
```python
exchange_authorization_code(self, client: OAuthClientInformationFull, authorization_code: AuthorizationCode) -> OAuthToken
```
-#### `load_refresh_token`
+#### `load_refresh_token`
```python
load_refresh_token(self, client: OAuthClientInformationFull, refresh_token: str) -> RefreshToken | None
```
-#### `exchange_refresh_token`
+#### `exchange_refresh_token`
```python
exchange_refresh_token(self, client: OAuthClientInformationFull, refresh_token: RefreshToken, scopes: list[str]) -> OAuthToken
```
-#### `load_access_token`
+#### `load_access_token`
```python
load_access_token(self, token: str) -> AccessToken | None
```
-#### `verify_token`
+#### `verify_token`
```python
verify_token(self, token: str) -> AccessToken | None
@@ -86,7 +86,7 @@ to our existing load_access_token method.
- AccessToken object if valid, None if invalid or expired
-#### `revoke_token`
+#### `revoke_token`
```python
revoke_token(self, token: AccessToken | RefreshToken) -> None
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx b/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx
index b049a4d64..68c39df63 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-jwt.mdx
@@ -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-keycloak.mdx b/docs/python-sdk/fastmcp-server-auth-providers-keycloak.mdx
new file mode 100644
index 000000000..e3cde2e60
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-auth-providers-keycloak.mdx
@@ -0,0 +1,20 @@
+---
+title: keycloak
+sidebarTitle: keycloak
+---
+
+# `fastmcp.server.auth.providers.keycloak`
+
+
+Keycloak authentication provider for FastMCP.
+
+## Classes
+
+### `KeycloakAuthProvider`
+
+
+Keycloak authentication provider using Dynamic Client Registration (DCR).
+
+Requires Keycloak 26.6.0 or later, which includes the fix for DCR compatibility
+with MCP clients (https://github.com/keycloak/keycloak/pull/45309).
+
diff --git a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
index f93a9f0c6..d050c4534 100644
--- a/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
+++ b/docs/python-sdk/fastmcp-server-auth-providers-workos.mdx
@@ -59,7 +59,7 @@ Setup Requirements:
4. Note your Client ID and Client Secret
-### `AuthKitProvider`
+### `AuthKitProvider`
AuthKit metadata provider for DCR (Dynamic Client Registration).
@@ -82,10 +82,31 @@ IMPORTANT SETUP REQUIREMENTS:
For detailed setup instructions, see:
https://workos.com/docs/authkit/mcp/integrating/token-verification
+Token audience is bound to this server automatically: when the MCP
+mount path becomes known (typically at ``http_app()`` construction),
+``JWTVerifier.audience`` is set to the resource URL advertised in
+``.well-known/oauth-protected-resource``. Enable Resource Indicators
+(RFC 8707) in your WorkOS Dashboard and list that same URL — AuthKit
+will then mint tokens with the matching ``aud`` claim.
+
**Methods:**
-#### `get_routes`
+#### `set_mcp_path`
+
+```python
+set_mcp_path(self, mcp_path: str | None) -> None
+```
+
+Bind the default verifier's audience to this server's resource URL.
+
+AuthKit with Resource Indicators (RFC 8707) mints tokens whose ``aud``
+claim equals the resource URL the client requested — which is the URL
+we advertise in ``.well-known/oauth-protected-resource``. Binding the
+audience here keeps validation in lock-step with what clients are sent.
+
+
+#### `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 af57dd145..1787326b4 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 78371e393..2b9940ca1 100644
--- a/docs/python-sdk/fastmcp-server-context.mdx
+++ b/docs/python-sdk/fastmcp-server-context.mdx
@@ -7,7 +7,7 @@ sidebarTitle: context
## Functions
-### `set_transport`
+### `set_transport`
```python
set_transport(transport: TransportType) -> Token[TransportType | None]
@@ -17,7 +17,7 @@ set_transport(transport: TransportType) -> Token[TransportType | None]
Set the current transport type. Returns token for reset.
-### `reset_transport`
+### `reset_transport`
```python
reset_transport(token: Token[TransportType | None]) -> None
@@ -27,7 +27,7 @@ reset_transport(token: Token[TransportType | None]) -> None
Reset transport to previous value.
-### `set_context`
+### `set_context`
```python
set_context(context: Context) -> Generator[Context, None, None]
@@ -35,7 +35,7 @@ set_context(context: Context) -> Generator[Context, None, None]
## Classes
-### `LogData`
+### `LogData`
Data object for passing log arguments to client-side handlers.
@@ -44,7 +44,7 @@ This provides an interface to match the Python standard library logging,
for compatibility with structured logging.
-### `Context`
+### `Context`
Context object providing access to MCP capabilities.
@@ -99,7 +99,7 @@ The context is optional - tools that don't need it can omit the parameter.
**Methods:**
-#### `is_background_task`
+#### `is_background_task`
```python
is_background_task(self) -> bool
@@ -112,7 +112,7 @@ task-aware implementations that can pause the task and wait for
client input.
-#### `task_id`
+#### `task_id`
```python
task_id(self) -> str | None
@@ -123,7 +123,7 @@ Get the background task ID if running in a background task.
Returns None if not running in a background task context.
-#### `origin_request_id`
+#### `origin_request_id`
```python
origin_request_id(self) -> str | None
@@ -136,7 +136,7 @@ In background task mode, this is the request_id captured when the task
was submitted, if one was available.
-#### `fastmcp`
+#### `fastmcp`
```python
fastmcp(self) -> FastMCP
@@ -145,7 +145,7 @@ fastmcp(self) -> FastMCP
Get the FastMCP instance.
-#### `request_context`
+#### `request_context`
```python
request_context(self) -> RequestContext[ServerSession, Any, Request] | None
@@ -174,7 +174,7 @@ async def on_request(self, context, call_next):
```
-#### `lifespan_context`
+#### `lifespan_context`
```python
lifespan_context(self) -> dict[str, Any]
@@ -201,7 +201,7 @@ def my_tool(ctx: Context) -> str:
```
-#### `report_progress`
+#### `report_progress`
```python
report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None
@@ -218,7 +218,7 @@ Works in both foreground (MCP progress notifications) and background
- `message`: Optional status message describing current progress
-#### `list_resources`
+#### `list_resources`
```python
list_resources(self) -> list[SDKResource]
@@ -230,7 +230,7 @@ List all available resources from the server.
- List of Resource objects available on the server
-#### `list_prompts`
+#### `list_prompts`
```python
list_prompts(self) -> list[SDKPrompt]
@@ -242,7 +242,7 @@ List all available prompts from the server.
- List of Prompt objects available on the server
-#### `get_prompt`
+#### `get_prompt`
```python
get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult
@@ -258,7 +258,7 @@ Get a prompt by name with optional arguments.
- The prompt result
-#### `read_resource`
+#### `read_resource`
```python
read_resource(self, uri: str | AnyUrl) -> ResourceResult
@@ -273,7 +273,7 @@ Read a resource by URI.
- ResourceResult with contents
-#### `log`
+#### `log`
```python
log(self, message: str, level: LoggingLevel | None = None, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@@ -291,7 +291,7 @@ Messages sent to Clients are also logged to the `fastmcp.server.context.to_clien
- `extra`: Optional mapping for additional arguments
-#### `transport`
+#### `transport`
```python
transport(self) -> TransportType | None
@@ -303,7 +303,7 @@ Returns the transport type used to run this server: "stdio", "sse",
or "streamable-http". Returns None if called outside of a server context.
-#### `client_supports_extension`
+#### `client_supports_extension`
```python
client_supports_extension(self, extension_id: str) -> bool
@@ -328,7 +328,7 @@ Example::
return "text-only client"
-#### `client_id`
+#### `client_id`
```python
client_id(self) -> str | None
@@ -337,7 +337,7 @@ client_id(self) -> str | None
Get the client ID if available.
-#### `request_id`
+#### `request_id`
```python
request_id(self) -> str
@@ -348,7 +348,7 @@ Get the unique ID for this request.
Raises RuntimeError if MCP request context is not available.
-#### `session_id`
+#### `session_id`
```python
session_id(self) -> str
@@ -365,7 +365,7 @@ the same client session.
- for other transports.
-#### `session`
+#### `session`
```python
session(self) -> ServerSession
@@ -379,7 +379,7 @@ In background task mode: Returns the session stored at Context creation.
Raises RuntimeError if no session is available.
-#### `debug`
+#### `debug`
```python
debug(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@@ -390,7 +390,7 @@ Send a `DEBUG`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
-#### `info`
+#### `info`
```python
info(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@@ -401,7 +401,7 @@ Send a `INFO`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
-#### `warning`
+#### `warning`
```python
warning(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@@ -412,7 +412,7 @@ Send a `WARNING`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
-#### `error`
+#### `error`
```python
error(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None
@@ -423,7 +423,7 @@ Send a `ERROR`-level message to the connected MCP Client.
Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`.
-#### `list_roots`
+#### `list_roots`
```python
list_roots(self) -> list[Root]
@@ -432,7 +432,7 @@ list_roots(self) -> list[Root]
List the roots available to the server, as indicated by the client.
-#### `send_notification`
+#### `send_notification`
```python
send_notification(self, notification: mcp.types.ServerNotificationType) -> None
@@ -444,7 +444,7 @@ Send a notification to the client immediately.
- `notification`: An MCP notification instance (e.g., ToolListChangedNotification())
-#### `close_sse_stream`
+#### `close_sse_stream`
```python
close_sse_stream(self) -> None
@@ -462,7 +462,7 @@ Instead of holding a connection open for minutes, you can periodically close
and let the client reconnect.
-#### `sample_step`
+#### `sample_step`
```python
sample_step(self, messages: str | Sequence[str | SamplingMessage]) -> SampleStep
@@ -505,7 +505,7 @@ regardless of this setting.
- - .text: The text content (if any)
-#### `sample`
+#### `sample`
```python
sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT]
@@ -514,7 +514,7 @@ sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[
Overload: With result_type, returns SamplingResult[ResultT].
-#### `sample`
+#### `sample`
```python
sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[str]
@@ -523,7 +523,7 @@ sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[
Overload: Without result_type, returns SamplingResult[str].
-#### `sample`
+#### `sample`
```python
sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT] | SamplingResult[str]
@@ -571,43 +571,43 @@ regardless of this setting.
- - .history: All messages exchanged during sampling
-#### `elicit`
+#### `elicit`
```python
elicit(self, message: str, response_type: None) -> AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+#### `elicit`
```python
elicit(self, message: str, response_type: type[T]) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+#### `elicit`
```python
elicit(self, message: str, response_type: list[str]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+#### `elicit`
```python
elicit(self, message: str, response_type: dict[str, dict[str, str]]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+#### `elicit`
```python
elicit(self, message: str, response_type: list[list[str]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+#### `elicit`
```python
elicit(self, message: str, response_type: list[dict[str, dict[str, str]]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
```
-#### `elicit`
+#### `elicit`
```python
elicit(self, message: str, response_type: type[T] | list[str] | dict[str, dict[str, str]] | list[list[str]] | list[dict[str, dict[str, str]]] | None = None) -> AcceptedElicitation[T] | AcceptedElicitation[dict[str, Any]] | AcceptedElicitation[str] | AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation
@@ -625,18 +625,28 @@ comply. If you provide a primitive type, an object schema with a single
"value" field will be generated for the MCP interaction and
automatically deconstructed into the primitive type upon response.
-If the response_type is None, the generated schema will be that of an
-empty object in order to comply with the MCP protocol requirements.
-Clients must send an empty object ("{}")in response.
+Passing ``response_type=None`` (or omitting it) is deprecated and will
+be removed in a future version. The resulting empty-schema form-mode
+request is ambiguous and causes some clients (e.g. VS Code) to hang on
+an empty form. Pass an explicit ``response_type`` describing the data
+you want back.
**Args:**
- `message`: A human-readable message explaining what information is needed
- `response_type`: The type of the response, which should be a primitive
type or dataclass or BaseModel. If it is a primitive type, an
object schema with a single "value" field will be generated.
+- `response_title`: Optional label to display for the wrapped ``value``
+field when ``response_type`` is a scalar, Literal, Enum, or one
+of the dict/list shorthand forms. Overrides the auto-generated
+"Value" label. Raises ``TypeError`` if passed with a BaseModel,
+dataclass, or ``None`` response type (use ``Field(title=...)``
+on the model instead).
+- `response_description`: Optional description to attach to the wrapped
+``value`` field. Same scope rules as ``response_title``.
-#### `set_state`
+#### `set_state`
```python
set_state(self, key: str, value: Any) -> None
@@ -657,7 +667,7 @@ requests.
The key is automatically prefixed with the session identifier.
-#### `get_state`
+#### `get_state`
```python
get_state(self, key: str) -> Any
@@ -671,7 +681,7 @@ then falls back to the session-scoped state store.
Returns None if the key is not found.
-#### `delete_state`
+#### `delete_state`
```python
delete_state(self, key: str) -> None
@@ -682,7 +692,7 @@ Delete a value from the state store.
Removes from both request-scoped and session-scoped stores.
-#### `enable_components`
+#### `enable_components`
```python
enable_components(self) -> None
@@ -706,7 +716,7 @@ ResourceListChangedNotification, and PromptListChangedNotification.
- `match_all`: If True, matches all components regardless of other criteria.
-#### `disable_components`
+#### `disable_components`
```python
disable_components(self) -> None
@@ -730,7 +740,7 @@ ResourceListChangedNotification, and PromptListChangedNotification.
- `match_all`: If True, matches all components regardless of other criteria.
-#### `reset_visibility`
+#### `reset_visibility`
```python
reset_visibility(self) -> None
diff --git a/docs/python-sdk/fastmcp-server-dependencies.mdx b/docs/python-sdk/fastmcp-server-dependencies.mdx
index 80b9f8421..94c31f459 100644
--- a/docs/python-sdk/fastmcp-server-dependencies.mdx
+++ b/docs/python-sdk/fastmcp-server-dependencies.mdx
@@ -15,84 +15,28 @@ CurrentWorker) and background task execution require fastmcp[tasks].
## Functions
-### `get_task_context`
-
-```python
-get_task_context() -> TaskContextInfo | None
-```
-
-
-Get the current task context if running inside a background task worker.
-
-This function extracts task information from the Docket execution context.
-Returns None if not running in a task context (e.g., foreground execution).
-
-**Returns:**
-- TaskContextInfo with task_id and session_id, or None if not in a task.
-
-
-### `register_task_session`
-
-```python
-register_task_session(session_id: str, session: ServerSession) -> None
-```
-
-
-Register a session for Context access in background tasks.
-
-Called automatically when a task is submitted to Docket. The session is
-stored as a weakref so it doesn't prevent garbage collection when the
-client disconnects.
-
-**Args:**
-- `session_id`: The session identifier
-- `session`: The ServerSession instance
-
-
-### `get_task_session`
-
-```python
-get_task_session(session_id: str) -> ServerSession | None
-```
-
-
-Get a registered session by ID if still alive.
-
-**Args:**
-- `session_id`: The session identifier
-
-**Returns:**
-- The ServerSession if found and alive, None otherwise
-
-
-### `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`
+### `is_docket_available`
```python
is_docket_available() -> bool
```
-Check if pydocket is installed.
+Check if a compatible pydocket (>= 0.19.0) is installed and importable.
+
+Three things have to be true for fastmcp's task features to work:
+ 1. pydocket distribution metadata is discoverable
+ 2. its version is at least ``_MIN_DOCKET_VERSION`` (older versions are
+ missing symbols like ``docket.dependencies.current_execution``,
+ which fastmcp imports on the request hot path)
+ 3. the package actually imports — guards against broken/partial
+ installs where metadata exists but ``import docket`` blows up
+
+Any of those failing means we treat docket as unavailable and fall back
+to the no-tasks code paths instead of crashing deep inside a request.
-### `require_docket`
+### `require_docket`
```python
require_docket(feature: str) -> None
@@ -106,7 +50,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]
@@ -132,7 +76,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
@@ -142,7 +86,7 @@ get_context() -> Context
Get the current FastMCP Context instance directly.
-### `get_server`
+### `get_server`
```python
get_server() -> FastMCP
@@ -162,7 +106,7 @@ started the worker).
- `RuntimeError`: If no server in context
-### `get_http_request`
+### `get_http_request`
```python
get_http_request() -> Request
@@ -176,7 +120,7 @@ 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]
@@ -197,7 +141,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
@@ -216,7 +160,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]
@@ -236,12 +180,16 @@ Handles:
**Args:**
- `fn`: Original function with Context and/or dependencies
+- `run_in_thread`: For sync ``fn``, whether to dispatch the call to a worker
+thread after resolving dependencies. Defaults to True. Set to False
+to call ``fn`` inline on the event loop thread — required for
+thread-affinity libraries (e.g. Windows COM). Ignored for async fns.
**Returns:**
- Async wrapper function without injected parameters
-### `resolve_dependencies`
+### `resolve_dependencies`
```python
resolve_dependencies(fn: Callable[..., Any], arguments: dict[str, Any]) -> AsyncGenerator[dict[str, Any], None]
@@ -267,7 +215,7 @@ time, so all injection goes through the unified DI system.
which will be filtered out)
-### `CurrentContext`
+### `CurrentContext`
```python
CurrentContext() -> Context
@@ -286,7 +234,7 @@ current MCP operation (tool/resource/prompt call).
- `RuntimeError`: If no active context found (during resolution)
-### `OptionalCurrentContext`
+### `OptionalCurrentContext`
```python
OptionalCurrentContext() -> Context | None
@@ -296,7 +244,7 @@ OptionalCurrentContext() -> Context | None
Get the current FastMCP Context, or None when no context is active.
-### `CurrentDocket`
+### `CurrentDocket`
```python
CurrentDocket() -> Docket
@@ -316,7 +264,7 @@ automatically creates for background task scheduling.
- `ImportError`: If fastmcp[tasks] not installed
-### `CurrentWorker`
+### `CurrentWorker`
```python
CurrentWorker() -> Worker
@@ -336,7 +284,7 @@ automatically creates for background task processing.
- `ImportError`: If fastmcp[tasks] not installed
-### `CurrentFastMCP`
+### `CurrentFastMCP`
```python
CurrentFastMCP() -> FastMCP
@@ -354,7 +302,7 @@ This dependency provides access to the active FastMCP server.
- `RuntimeError`: If no server in context (during resolution)
-### `CurrentRequest`
+### `CurrentRequest`
```python
CurrentRequest() -> Request
@@ -374,7 +322,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]
@@ -392,7 +340,7 @@ transport.
- A dependency that resolves to a dictionary of header name -> value
-### `CurrentAccessToken`
+### `CurrentAccessToken`
```python
CurrentAccessToken() -> AccessToken
@@ -411,7 +359,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
@@ -436,16 +384,7 @@ without needing the full token object.
## Classes
-### `TaskContextInfo`
-
-
-Information about the current background task context.
-
-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.
@@ -456,7 +395,7 @@ and Docket's Progress (worker context).
**Methods:**
-#### `current`
+#### `current`
```python
current(self) -> int | None
@@ -465,7 +404,7 @@ current(self) -> int | None
Current progress value.
-#### `total`
+#### `total`
```python
total(self) -> int
@@ -474,7 +413,7 @@ total(self) -> int
Total/target progress value.
-#### `message`
+#### `message`
```python
message(self) -> str | None
@@ -483,7 +422,7 @@ message(self) -> str | None
Current progress message.
-#### `set_total`
+#### `set_total`
```python
set_total(self, total: int) -> None
@@ -492,7 +431,7 @@ set_total(self, total: int) -> None
Set the total/target value for progress tracking.
-#### `increment`
+#### `increment`
```python
increment(self, amount: int = 1) -> None
@@ -501,7 +440,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
@@ -510,7 +449,7 @@ set_message(self, message: str | None) -> None
Update the progress status message.
-### `InMemoryProgress`
+### `InMemoryProgress`
In-memory progress tracker for immediate tool execution.
@@ -522,25 +461,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
@@ -549,7 +488,7 @@ set_total(self, total: int) -> None
Set the total/target value for progress tracking.
-#### `increment`
+#### `increment`
```python
increment(self, amount: int = 1) -> None
@@ -558,7 +497,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
@@ -567,24 +506,22 @@ set_message(self, message: str | None) -> None
Update the progress status message.
-### `Progress`
+### `Progress`
-FastMCP Progress dependency that works in both server and worker contexts.
+Progress dependency that works in both server and worker contexts.
-Handles three execution modes:
-- In Docket worker: Uses the execution's progress (observable via Redis)
-- In FastMCP server with Docket: Falls back to in-memory progress
-- In FastMCP server without Docket: Uses in-memory progress
+In a Docket worker, delegates to the execution's Redis-backed progress
+(observable across processes). Otherwise, uses in-memory tracking.
-This allows tools to use Progress() regardless of whether they're called
-immediately or as background tasks, and regardless of whether pydocket
-is installed.
+The shared default instance acts as a stateless factory — ``__aenter__``
+creates a fresh ``Progress`` per invocation so concurrent tasks never
+share mutable state.
**Methods:**
-#### `current`
+#### `current`
```python
current(self) -> int | None
@@ -593,7 +530,7 @@ current(self) -> int | None
Current progress value.
-#### `total`
+#### `total`
```python
total(self) -> int
@@ -602,7 +539,7 @@ total(self) -> int
Total/target progress value.
-#### `message`
+#### `message`
```python
message(self) -> str | None
@@ -611,7 +548,7 @@ message(self) -> str | None
Current progress message.
-#### `set_total`
+#### `set_total`
```python
set_total(self, total: int) -> None
@@ -620,7 +557,7 @@ set_total(self, total: int) -> None
Set the total/target value for progress tracking.
-#### `increment`
+#### `increment`
```python
increment(self, amount: int = 1) -> None
@@ -629,7 +566,7 @@ increment(self, amount: int = 1) -> None
Atomically increment the current progress value.
-#### `set_message`
+#### `set_message`
```python
set_message(self, message: str | None) -> None
diff --git a/docs/python-sdk/fastmcp-server-elicitation.mdx b/docs/python-sdk/fastmcp-server-elicitation.mdx
index 85f140c6e..8b33a04c2 100644
--- a/docs/python-sdk/fastmcp-server-elicitation.mdx
+++ b/docs/python-sdk/fastmcp-server-elicitation.mdx
@@ -10,7 +10,7 @@ sidebarTitle: elicitation
### `parse_elicit_response_type`
```python
-parse_elicit_response_type(response_type: Any) -> ElicitConfig
+parse_elicit_response_type(response_type: Any, response_title: str | None = None, response_description: str | None = None) -> ElicitConfig
```
@@ -27,8 +27,15 @@ Supports multiple syntaxes:
- Scalar types (bool, int, float, str, Literal, Enum): single value
- Other types (dataclass, BaseModel): use directly
+The ``response_title`` and ``response_description`` arguments customize the
+label and description of the wrapped ``value`` property for the scalar/dict/list
+shorthand forms. They are only valid when FastMCP is wrapping the response
+type; passing them with a full BaseModel/dataclass (or ``None``) raises
+``TypeError``, because in those cases the user already controls field
+metadata via ``Field(title=..., description=...)``.
-### `handle_elicit_accept`
+
+### `handle_elicit_accept`
```python
handle_elicit_accept(config: ElicitConfig, content: Any) -> AcceptedElicitation[Any]
@@ -45,7 +52,7 @@ Handle an accepted elicitation response.
- AcceptedElicitation with the extracted/validated data
-### `get_elicitation_schema`
+### `get_elicitation_schema`
```python
get_elicitation_schema(response_type: type[T]) -> dict[str, Any]
@@ -58,7 +65,7 @@ Get the schema for an elicitation response.
- `response_type`: The type of the response
-### `validate_elicitation_json_schema`
+### `validate_elicitation_json_schema`
```python
validate_elicitation_json_schema(schema: dict[str, Any]) -> None
diff --git a/docs/python-sdk/fastmcp-server-http.mdx b/docs/python-sdk/fastmcp-server-http.mdx
index 9b4c309af..f4e5d45bc 100644
--- a/docs/python-sdk/fastmcp-server-http.mdx
+++ b/docs/python-sdk/fastmcp-server-http.mdx
@@ -7,13 +7,13 @@ sidebarTitle: http
## Functions
-### `set_http_request`
+### `set_http_request`
```python
set_http_request(request: Request) -> Generator[Request, None, None]
```
-### `create_base_app`
+### `create_base_app`
```python
create_base_app(routes: list[BaseRoute], middleware: list[Middleware], debug: bool = False, lifespan: Callable | None = None) -> StarletteWithLifespan
@@ -32,7 +32,7 @@ Create a base Starlette app with common middleware and routes.
- A Starlette application
-### `create_sse_app`
+### `create_sse_app`
```python
create_sse_app(server: FastMCP[LifespanResultT], message_path: str, sse_path: str, auth: AuthProvider | None = None, debug: bool = False, routes: list[BaseRoute] | None = None, middleware: list[Middleware] | None = None) -> StarletteWithLifespan
@@ -54,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
@@ -89,17 +89,17 @@ disconnections. Requires event_store to be set. Defaults to SDK default.
ASGI application wrapper for Streamable HTTP server transport.
-### `StarletteWithLifespan`
+### `StarletteWithLifespan`
**Methods:**
-#### `lifespan`
+#### `lifespan`
```python
lifespan(self) -> Lifespan[Starlette]
```
-### `RequestContextMiddleware`
+### `RequestContextMiddleware`
Middleware that stores each request in a ContextVar and sets transport type.
diff --git a/docs/python-sdk/fastmcp-server-low_level.mdx b/docs/python-sdk/fastmcp-server-low_level.mdx
index 7456a5940..9e188cf8c 100644
--- a/docs/python-sdk/fastmcp-server-low_level.mdx
+++ b/docs/python-sdk/fastmcp-server-low_level.mdx
@@ -55,7 +55,7 @@ Get the FastMCP instance.
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 fbf9bba15..28a6eefc5 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
@@ -146,12 +146,16 @@ this means that response caching may not be compatible with other middleware tha
Notes:
- Caches `tools/call`, `resources/read`, `prompts/get`, `tools/list`, `resources/list`, and `prompts/list` requests.
-- Cache keys are derived from method name and arguments.
+- Cache keys are derived from the method name, arguments, and the caller's
+ access token. Entries are partitioned per-token so that responses filtered
+ by per-component authorization (e.g. `auth=require_scopes(...)`) cannot
+ leak across users with different permissions. Unauthenticated callers
+ (including STDIO) share a single anonymous partition.
**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 +165,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 +175,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 +185,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 +195,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 +205,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 +215,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-error_handling.mdx b/docs/python-sdk/fastmcp-server-middleware-error_handling.mdx
index 3089f4d2c..d60c2468b 100644
--- a/docs/python-sdk/fastmcp-server-middleware-error_handling.mdx
+++ b/docs/python-sdk/fastmcp-server-middleware-error_handling.mdx
@@ -50,7 +50,7 @@ backoff to avoid overwhelming the server or external dependencies.
**Methods:**
-#### `on_request`
+#### `on_request`
```python
on_request(self, context: MiddlewareContext, call_next: CallNext) -> Any
diff --git a/docs/python-sdk/fastmcp-server-middleware-response_limiting.mdx b/docs/python-sdk/fastmcp-server-middleware-response_limiting.mdx
index 0f344100b..b897f16c3 100644
--- a/docs/python-sdk/fastmcp-server-middleware-response_limiting.mdx
+++ b/docs/python-sdk/fastmcp-server-middleware-response_limiting.mdx
@@ -10,7 +10,7 @@ Response limiting middleware for controlling tool response sizes.
## Classes
-### `ResponseLimitingMiddleware`
+### `ResponseLimitingMiddleware`
Middleware that limits the response size of tool calls.
@@ -22,7 +22,7 @@ a single TextContent block.
**Methods:**
-#### `on_call_tool`
+#### `on_call_tool`
```python
on_call_tool(self, context: MiddlewareContext[mt.CallToolRequestParams], call_next: CallNext[mt.CallToolRequestParams, ToolResult]) -> ToolResult
diff --git a/docs/python-sdk/fastmcp-server-mixins-transport.mdx b/docs/python-sdk/fastmcp-server-mixins-transport.mdx
index ad4e0b60e..0d29ed280 100644
--- a/docs/python-sdk/fastmcp-server-mixins-transport.mdx
+++ b/docs/python-sdk/fastmcp-server-mixins-transport.mdx
@@ -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-providers-addressing.mdx b/docs/python-sdk/fastmcp-server-providers-addressing.mdx
new file mode 100644
index 000000000..48b953e87
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-providers-addressing.mdx
@@ -0,0 +1,81 @@
+---
+title: addressing
+sidebarTitle: addressing
+---
+
+# `fastmcp.server.providers.addressing`
+
+
+Deterministic tool hashing for backend-tool routing and per-tool resources.
+
+Each FastMCPApp backend tool gets a deterministic hash computed from its
+app name + tool name. The hash serves two purposes:
+
+1. **Backend-tool routing.** Tools with ``"app"`` in their visibility are
+ callable via ``_``. The dispatcher parses the prefix,
+ then walks providers recursively (same pattern as the old ``get_app_tool``)
+ to find a tool whose stored hash matches.
+
+2. **Per-tool Prefab renderer URIs.** Each prefab tool gets a unique renderer
+ resource at ``ui://prefab/tool//renderer.html``. ``list_resources``
+ and ``read_resource`` synthesize these on demand from the tool's meta.
+
+The hash is computed at registration time from ``(app_name, tool_name)`` —
+both known at that moment — and stored in ``meta["fastmcp"]["_tool_hash"]``.
+Deterministic across replicas (same code → same hash), no registry walk
+needed.
+
+
+## Functions
+
+### `hash_tool`
+
+```python
+hash_tool(app_name: str, tool_name: str) -> str
+```
+
+
+Deterministic hex hash for a tool in an app.
+
+Same inputs on every replica produce the same output.
+
+
+### `hashed_backend_name`
+
+```python
+hashed_backend_name(app_name: str, tool_name: str) -> str
+```
+
+
+Format the universal name for a backend tool: ``_``.
+
+
+### `parse_hashed_backend_name`
+
+```python
+parse_hashed_backend_name(name: str) -> tuple[str, str] | None
+```
+
+
+Parse ``_`` → ``(hash, local_tool_name)`` or None.
+
+
+### `hashed_resource_uri`
+
+```python
+hashed_resource_uri(app_name: str, tool_name: str) -> str
+```
+
+
+Per-tool Prefab renderer resource URI.
+
+
+### `parse_hashed_resource_uri`
+
+```python
+parse_hashed_resource_uri(uri: str) -> str | None
+```
+
+
+Extract the hash from a Prefab renderer URI, or None.
+
diff --git a/docs/python-sdk/fastmcp-server-providers-aggregate.mdx b/docs/python-sdk/fastmcp-server-providers-aggregate.mdx
index ffc3d8dbb..36feac09c 100644
--- a/docs/python-sdk/fastmcp-server-providers-aggregate.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-aggregate.mdx
@@ -64,7 +64,7 @@ FastMCPProvider to ensure middleware is invoked correctly.
- Prompts become "namespace_promptname"
-#### `get_app_tool`
+#### `get_app_tool`
```python
get_app_tool(self, app_name: str, tool_name: str) -> Tool | None
@@ -73,7 +73,16 @@ get_app_tool(self, app_name: str, tool_name: str) -> Tool | None
Query all child providers for an app tool.
-#### `get_tasks`
+#### `get_tool_by_hash`
+
+```python
+get_tool_by_hash(self, tool_hash: str, tool_name: str) -> Tool | None
+```
+
+Query all child providers for a tool matching a hash.
+
+
+#### `get_tasks`
```python
get_tasks(self) -> Sequence[FastMCPComponent]
@@ -82,7 +91,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 715f52a0c..6d4774977 100644
--- a/docs/python-sdk/fastmcp-server-providers-base.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-base.mdx
@@ -147,7 +147,20 @@ name. Skips the transform chain entirely.
- The tool if found and tagged with the given app name, else None.
-#### `list_resources`
+#### `get_tool_by_hash`
+
+```python
+get_tool_by_hash(self, tool_hash: str, tool_name: str) -> Tool | None
+```
+
+Look up an app-visible tool by its deterministic hash.
+
+Same recursive-walk semantics as ``get_app_tool`` but matches on
+``meta["fastmcp"]["_tool_hash"]`` instead of the app name tag.
+Used by the dispatcher when receiving hashed backend-tool calls.
+
+
+#### `list_resources`
```python
list_resources(self) -> Sequence[Resource]
@@ -158,7 +171,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
@@ -177,7 +190,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]
@@ -188,7 +201,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
@@ -207,7 +220,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]
@@ -218,7 +231,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
@@ -237,7 +250,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]
@@ -252,7 +265,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]
@@ -268,7 +281,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
@@ -296,7 +309,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 ec5333e78..5be980e25 100644
--- a/docs/python-sdk/fastmcp-server-providers-fastmcp_provider.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-fastmcp_provider.mdx
@@ -18,7 +18,7 @@ executed.
## Classes
-### `FastMCPProviderTool`
+### `FastMCPProviderTool`
Tool that delegates execution to a wrapped server's middleware.
@@ -30,7 +30,7 @@ chain is executed.
**Methods:**
-#### `wrap`
+#### `wrap`
```python
wrap(cls, server: Any, tool: Tool) -> FastMCPProviderTool
@@ -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,7 @@ This ensures middleware runs when components are executed.
**Methods:**
-#### `get_app_tool`
+#### `get_app_tool`
```python
get_app_tool(self, app_name: str, tool_name: str) -> Tool | None
@@ -219,7 +219,16 @@ 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`
+#### `get_tool_by_hash`
+
+```python
+get_tool_by_hash(self, tool_hash: str, tool_name: str) -> Tool | None
+```
+
+Delegate to nested server's get_tool_by_hash, wrapping for middleware.
+
+
+#### `get_tasks`
```python
get_tasks(self) -> Sequence[FastMCPComponent]
@@ -233,7 +242,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.mdx b/docs/python-sdk/fastmcp-server-providers-filesystem.mdx
index d5b589e1e..e0cb8977c 100644
--- a/docs/python-sdk/fastmcp-server-providers-filesystem.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-filesystem.mdx
@@ -35,7 +35,7 @@ Example:
## Classes
-### `FileSystemProvider`
+### `FileSystemProvider`
Provider that discovers components from the filesystem.
diff --git a/docs/python-sdk/fastmcp-server-providers-filesystem_discovery.mdx b/docs/python-sdk/fastmcp-server-providers-filesystem_discovery.mdx
index 2a4595b89..6c7504145 100644
--- a/docs/python-sdk/fastmcp-server-providers-filesystem_discovery.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-filesystem_discovery.mdx
@@ -82,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
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 802adb6e0..f09ea31f7 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-provider.mdx b/docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx
index 5d74c19ae..037c4b7fc 100644
--- a/docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx
+++ b/docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx
@@ -21,7 +21,7 @@ spec. Each component makes HTTP calls to the described API endpoints.
**Methods:**
-#### `lifespan`
+#### `lifespan`
```python
lifespan(self) -> AsyncIterator[None]
@@ -30,7 +30,7 @@ lifespan(self) -> AsyncIterator[None]
Manage the lifecycle of the auto-created httpx client.
-#### `get_tasks`
+#### `get_tasks`
```python
get_tasks(self) -> Sequence[FastMCPComponent]
diff --git a/docs/python-sdk/fastmcp-server-providers-prefab_synthesis.mdx b/docs/python-sdk/fastmcp-server-providers-prefab_synthesis.mdx
new file mode 100644
index 000000000..c06fc8b7a
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-providers-prefab_synthesis.mdx
@@ -0,0 +1,58 @@
+---
+title: prefab_synthesis
+sidebarTitle: prefab_synthesis
+---
+
+# `fastmcp.server.providers.prefab_synthesis`
+
+
+On-demand Prefab renderer resource synthesis.
+
+Tools marked as Prefab (via ``app=True``, ``PrefabAppConfig``, etc.) carry
+a placeholder ``meta.ui.resourceUri`` and optionally a hash in
+``meta.fastmcp._tool_hash``. This module synthesizes per-tool renderer
+resources on demand at ``list_resources`` and ``read_resource`` time
+without storing or materializing anything.
+
+Each tool's resource URI is ``ui://prefab/tool//renderer.html``
+where the hash comes from the tool's own meta (set at registration from
+the app name + tool name). CSP on the resource is the tool's
+``meta.ui.csp`` merged with the renderer defaults across all four
+``*_domains`` fields.
+
+
+## Functions
+
+### `synthesize_prefab_resources`
+
+```python
+synthesize_prefab_resources(server: FastMCP) -> list[Resource]
+```
+
+
+Return fresh synthetic Prefab resources for all prefab tools. Pure.
+
+
+### `synthesize_prefab_resource_by_uri`
+
+```python
+synthesize_prefab_resource_by_uri(server: FastMCP, uri: str) -> Resource | None
+```
+
+
+Intercept a Prefab renderer URI and synthesize on demand.
+
+
+### `rewrite_tool_meta_for_wire`
+
+```python
+rewrite_tool_meta_for_wire(tool: Tool) -> Tool
+```
+
+
+Return a model_copy with the per-tool URI and CSP stripped.
+
+Reads the hash from the tool's own meta. If no hash is found,
+returns the tool unchanged. Produces a fresh copy — the original
+Tool object is untouched.
+
diff --git a/docs/python-sdk/fastmcp-server-providers-proxy.mdx b/docs/python-sdk/fastmcp-server-providers-proxy.mdx
index d612a9f37..c9f0a6b2f 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
@@ -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.
@@ -255,7 +255,7 @@ backends whose component lists change dynamically.
**Methods:**
-#### `get_tasks`
+#### `get_tasks`
```python
get_tasks(self) -> Sequence[FastMCPComponent]
@@ -268,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.
@@ -277,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.
@@ -285,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.
@@ -306,7 +306,7 @@ it to detect (and correct) staleness.
**Methods:**
-#### `clear`
+#### `clear`
```python
clear(self)
@@ -315,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 ea6d46104..09d87f4ae 100644
--- a/docs/python-sdk/fastmcp-server-sampling-run.mdx
+++ b/docs/python-sdk/fastmcp-server-sampling-run.mdx
@@ -10,7 +10,7 @@ Sampling types and helper functions for FastMCP servers.
## Functions
-### `determine_handler_mode`
+### `determine_handler_mode`
```python
determine_handler_mode(context: Context, needs_tools: bool) -> bool
@@ -30,7 +30,7 @@ Determine whether to use fallback handler or client for sampling.
- `ValueError`: If client lacks required capability and no fallback configured.
-### `call_sampling_handler`
+### `call_sampling_handler`
```python
call_sampling_handler(context: Context, messages: list[SamplingMessage]) -> CreateMessageResult | CreateMessageResultWithTools
@@ -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]
@@ -154,7 +154,7 @@ provides a final text response.
## Classes
-### `SamplingResult`
+### `SamplingResult`
Result of a sampling operation.
@@ -165,7 +165,7 @@ Result of a sampling operation.
- `history`: All messages exchanged during sampling.
-### `SampleStep`
+### `SampleStep`
Result of a single sampling call.
@@ -175,7 +175,7 @@ Represents what the LLM returned in this step plus the message history.
**Methods:**
-#### `is_tool_use`
+#### `is_tool_use`
```python
is_tool_use(self) -> bool
@@ -184,7 +184,7 @@ is_tool_use(self) -> bool
True if the LLM is requesting tool execution.
-#### `text`
+#### `text`
```python
text(self) -> str | None
@@ -193,7 +193,7 @@ text(self) -> str | None
Extract text from the response, if available.
-#### `tool_calls`
+#### `tool_calls`
```python
tool_calls(self) -> list[ToolUseContent]
diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx
index 14fcf04ca..f9ef01991 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
@@ -228,7 +228,7 @@ requested, falls back to the next-highest enabled version.
- The tool if found and enabled, None otherwise.
-#### `list_resources`
+#### `list_resources`
```python
list_resources(self) -> Sequence[Resource]
@@ -241,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
@@ -263,7 +263,7 @@ requested, falls back to the next-highest enabled version.
- The resource if found and enabled, None otherwise.
-#### `list_resource_templates`
+#### `list_resource_templates`
```python
list_resource_templates(self) -> Sequence[ResourceTemplate]
@@ -276,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
@@ -298,7 +298,7 @@ requested, falls back to the next-highest enabled version.
- The template if found and enabled, None otherwise.
-#### `list_prompts`
+#### `list_prompts`
```python
list_prompts(self) -> Sequence[Prompt]
@@ -311,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
@@ -333,19 +333,19 @@ requested, falls back to the next-highest enabled version.
- 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
@@ -375,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
@@ -416,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
@@ -458,7 +458,7 @@ return PromptResult.
- `PromptError`: If prompt rendering fails
-#### `add_tool`
+#### `add_tool`
```python
add_tool(self, tool: Tool | Callable[..., Any]) -> Tool
@@ -476,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
@@ -495,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]
@@ -563,7 +563,7 @@ server.tool(my_function, name="custom_name")
```
-#### `add_resource`
+#### `add_resource`
```python
add_resource(self, resource: Resource | Callable[..., Any]) -> Resource | ResourceTemplate
@@ -578,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
@@ -593,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]
@@ -652,7 +652,7 @@ async def get_weather(city: str) -> str:
```
-#### `add_prompt`
+#### `add_prompt`
```python
add_prompt(self, prompt: Prompt | Callable[..., Any]) -> Prompt
@@ -667,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]
@@ -756,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
@@ -803,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
@@ -844,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
@@ -873,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
@@ -897,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
@@ -915,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-capabilities.mdx b/docs/python-sdk/fastmcp-server-tasks-capabilities.mdx
index 03b1102dd..8e6ada03a 100644
--- a/docs/python-sdk/fastmcp-server-tasks-capabilities.mdx
+++ b/docs/python-sdk/fastmcp-server-tasks-capabilities.mdx
@@ -10,7 +10,7 @@ SEP-1686 task capabilities declaration.
## Functions
-### `get_task_capabilities`
+### `get_task_capabilities`
```python
get_task_capabilities() -> ServerTasksCapability | None
@@ -22,7 +22,11 @@ Return the SEP-1686 task capabilities.
Returns task capabilities as a first-class ServerCapabilities field,
declaring support for list, cancel, and request operations per SEP-1686.
-Returns None if pydocket is not installed (no task support).
+Returns None if a compatible pydocket is not installed (no task support).
+Uses the canonical ``is_docket_available()`` check so that capability
+advertisement and handler registration stay in sync — otherwise a server
+with an old transitive pydocket would advertise task support and then
+return "method not found" when clients invoked it.
Note: prompts/resources are passed via extra_data since the SDK types
don't include them yet (FastMCP supports them ahead of the spec).
diff --git a/docs/python-sdk/fastmcp-server-tasks-context.mdx b/docs/python-sdk/fastmcp-server-tasks-context.mdx
new file mode 100644
index 000000000..42f679ce7
--- /dev/null
+++ b/docs/python-sdk/fastmcp-server-tasks-context.mdx
@@ -0,0 +1,192 @@
+---
+title: context
+sidebarTitle: context
+---
+
+# `fastmcp.server.tasks.context`
+
+
+Task context and scoping for background task execution.
+
+Determines authorization scope (``get_task_scope``), manages the context
+snapshot that is captured at task submission and restored in workers
+(``TaskContextSnapshot``), and maintains in-process registries for live
+sessions and servers.
+
+
+## Functions
+
+### `get_task_scope`
+
+```python
+get_task_scope() -> str | None
+```
+
+
+Get the authorization scope for task isolation.
+
+Returns the raw scope identifier for the current access token, or
+``None`` when no auth context is present (anonymous tasks).
+
+The scope is composed as ``client_id|sub`` when the token carries a
+``sub`` claim — necessary for fixed-OAuth servers where ``client_id`` is
+shared across all users — and falls back to ``client_id`` alone for
+DCR/CIMD flows where the client identity is already per-user.
+
+Encoding for Redis/Docket keys happens at the boundary in ``keys.py``;
+this function returns the raw value.
+
+
+### `get_task_context`
+
+```python
+get_task_context() -> TaskContextInfo | None
+```
+
+
+Get the current task context if running inside a background task worker.
+
+This function extracts task information from the Docket execution context.
+Returns None if not running in a task context (e.g., foreground execution).
+
+**Returns:**
+- TaskContextInfo with task_id and task_scope, or None if not in a task.
+
+
+### `get_task_session_id`
+
+```python
+get_task_session_id() -> str | None
+```
+
+
+Get the session_id for the current background task, if available.
+
+Reads the cached snapshot set by the worker-level restore dependency.
+Returns None if not in a task context or the snapshot wasn't restored.
+
+
+### `restore_task_snapshot`
+
+```python
+restore_task_snapshot(key: str = TaskKey()) -> None
+```
+
+
+Worker-level Docket dependency that restores the task-context snapshot.
+
+Runs before each fastmcp-owned task, populating the snapshot ContextVar
+so user code — and any task-scoped dependency like ``_CurrentContext`` —
+sees a ready snapshot without touching Redis itself. All Redis I/O
+goes through Docket's async client, so cluster URLs and the memory://
+backend work transparently (#3897). Failures are non-fatal: the task
+still runs, and sync helpers return ``None`` as they would have before
+the snapshot was captured.
+
+
+### `register_task_session`
+
+```python
+register_task_session(session_id: str, session: ServerSession) -> None
+```
+
+
+Register a session for in-process background task access.
+
+Called automatically when a task is submitted to Docket. The session is
+stored as a weakref so it doesn't prevent garbage collection when the
+client disconnects.
+
+
+### `get_task_session`
+
+```python
+get_task_session(session_id: str) -> ServerSession | None
+```
+
+
+Get a registered session by ID if still alive.
+
+Returns None in distributed workers where the session lives in another
+process — callers must handle this gracefully.
+
+
+### `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 so that background workers can resolve
+the correct (child) server for mounted tasks.
+
+
+### `get_task_server`
+
+```python
+get_task_server(task_id: str) -> FastMCP | None
+```
+
+
+Get the registered server for a background task, if still alive.
+
+
+## Classes
+
+### `TaskContextInfo`
+
+
+Information about the current background task context.
+
+Returned by ``get_task_context()`` when running inside a Docket worker.
+Contains identifiers needed to communicate with the MCP session.
+
+
+### `TaskContextSnapshot`
+
+
+All context data snapshotted at task-submission time.
+
+Stored as a single Redis key per task, restored once in the worker.
+
+
+**Methods:**
+
+#### `capture`
+
+```python
+capture(cls) -> TaskContextSnapshot
+```
+
+Capture current context for background task execution.
+
+
+#### `from_json`
+
+```python
+from_json(cls, raw: str | bytes) -> TaskContextSnapshot
+```
+
+Deserialize from JSON stored in Redis.
+
+
+#### `to_json`
+
+```python
+to_json(self) -> str
+```
+
+Serialize to JSON for Redis storage.
+
+
+#### `save`
+
+```python
+save(self, docket: Docket, task_scope: str | None, task_id: str, ttl_seconds: int) -> None
+```
+
+Store this snapshot as a single Redis key.
+
diff --git a/docs/python-sdk/fastmcp-server-tasks-elicitation.mdx b/docs/python-sdk/fastmcp-server-tasks-elicitation.mdx
index cc6f3dea2..3914d207c 100644
--- a/docs/python-sdk/fastmcp-server-tasks-elicitation.mdx
+++ b/docs/python-sdk/fastmcp-server-tasks-elicitation.mdx
@@ -23,7 +23,7 @@ internal APIs for background task coordination.
## Functions
-### `elicit_for_task`
+### `elicit_for_task`
```python
elicit_for_task(task_id: str, session: ServerSession | None, message: str, schema: dict[str, Any], fastmcp: FastMCP) -> mcp.types.ElicitResult
@@ -50,10 +50,10 @@ in a Docket worker context where there's no active MCP request.
- `McpError`: If the elicitation request fails
-### `relay_elicitation`
+### `relay_elicitation`
```python
-relay_elicitation(session: ServerSession, session_id: str, task_id: str, elicitation: dict[str, Any], fastmcp: FastMCP) -> None
+relay_elicitation(session: ServerSession, task_scope: str | None, task_id: str, elicitation: dict[str, Any], fastmcp: FastMCP) -> None
```
@@ -66,16 +66,16 @@ response to Redis so the blocked worker can resume.
**Args:**
- `session`: MCP ServerSession
-- `session_id`: Session identifier
+- `task_scope`: Authorization scope for Redis key construction
- `task_id`: Background task ID
- `elicitation`: Elicitation metadata (message, requestedSchema)
- `fastmcp`: FastMCP server instance
-### `handle_task_input`
+### `handle_task_input`
```python
-handle_task_input(task_id: str, session_id: str, action: str, content: dict[str, Any] | None, fastmcp: FastMCP) -> bool
+handle_task_input(task_id: str, task_scope: str | None, action: str, content: dict[str, Any] | None, fastmcp: FastMCP) -> bool
```
@@ -86,7 +86,7 @@ request from a background task.
**Args:**
- `task_id`: The background task ID
-- `session_id`: The MCP session ID
+- `task_scope`: Authorization scope for Redis key construction
- `action`: The elicitation action ("accept", "decline", "cancel")
- `content`: The response content (for "accept" action)
- `fastmcp`: The FastMCP server instance
diff --git a/docs/python-sdk/fastmcp-server-tasks-handlers.mdx b/docs/python-sdk/fastmcp-server-tasks-handlers.mdx
index e7b1ed35e..fd3659421 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-keys.mdx b/docs/python-sdk/fastmcp-server-tasks-keys.mdx
index a274d3c1d..a852fadd9 100644
--- a/docs/python-sdk/fastmcp-server-tasks-keys.mdx
+++ b/docs/python-sdk/fastmcp-server-tasks-keys.mdx
@@ -6,34 +6,40 @@ sidebarTitle: keys
# `fastmcp.server.tasks.keys`
-Task key management for SEP-1686 background tasks.
+Docket and Redis key encoding for background tasks.
-Task keys encode security scoping and metadata in the Docket key format:
- `{session_id}:{client_task_id}:{task_type}:{component_identifier}`
+The compound Docket task key embeds the auth boundary so that the parser can
+reject cross-scope access without consulting Redis. Authenticated and
+anonymous tasks live in disjoint keyspaces:
-This format provides:
-- Session-based security scoping (prevents cross-session access)
-- Task type identification (tool/prompt/resource)
-- Component identification (name or URI for result conversion)
+ auth:{enc_scope}:{client_task_id}:{task_type}:{enc_identifier}
+ anon:{client_task_id}:{task_type}:{enc_identifier}
+
+The same `auth/anon` partition is used for the per-task Redis prefix
+(``fastmcp:task:auth:{enc_scope}`` vs ``fastmcp:task:anon``) — see
+``task_redis_prefix``.
+
+``task_scope`` is the raw scope identifier (typically derived from
+``client_id`` or ``client_id|sub``); encoding happens once, at the boundary,
+in this module.
## Functions
-### `build_task_key`
+### `build_task_key`
```python
-build_task_key(session_id: str, client_task_id: str, task_type: str, component_identifier: str) -> str
+build_task_key(task_scope: str | None, client_task_id: str, task_type: str, component_identifier: str) -> str
```
Build Docket task key with embedded metadata.
-Format: `{session_id}:{client_task_id}:{task_type}:{component_identifier}`
-
-The component_identifier is URI-encoded to handle special characters (colons, slashes, etc.).
+When ``task_scope`` is ``None`` the task is anonymous and lives in the
+``anon`` keyspace. Otherwise it lives under ``auth:{enc_scope}``.
**Args:**
-- `session_id`: Session ID for security scoping
+- `task_scope`: Raw authorization scope, or ``None`` for anonymous tasks
- `client_task_id`: Client-provided task ID
- `task_type`: Type of task ("tool", "prompt", "resource")
- `component_identifier`: Tool name, prompt name, or resource URI
@@ -43,16 +49,18 @@ The component_identifier is URI-encoded to handle special characters (colons, sl
**Examples:**
->>> build_task_key("session123", "task456", "tool", "my_tool")
-'session123:task456:tool:my_tool'
->>> build_task_key("session123", "task456", "resource", "file://data.txt")
-'session123:task456:resource:file%3A%2F%2Fdata.txt'
+>>> build_task_key("client-a", "task456", "tool", "my_tool")
+'auth:client-a:task456:tool:my_tool'
+>>> build_task_key(None, "task456", "tool", "my_tool")
+'anon:task456:tool:my_tool'
+>>> build_task_key("client-a", "task456", "resource", "file://data.txt")
+'auth:client-a:task456:resource:file%3A%2F%2Fdata.txt'
-### `parse_task_key`
+### `parse_task_key`
```python
-parse_task_key(task_key: str) -> dict[str, str]
+parse_task_key(task_key: str) -> TaskKeyParts
```
@@ -62,17 +70,21 @@ Parse Docket task key to extract metadata.
- `task_key`: Encoded task key from Docket
**Returns:**
-- Dict with keys: session_id, client_task_id, task_type, component_identifier
+- Dict with keys: ``task_scope`` (``str | None``), ``client_task_id``,
+- ``task_type``, ``component_identifier``.
+
+**Raises:**
+- `ValueError`: If the key has an unrecognized tag or wrong segment count.
**Examples:**
->>> parse_task_key("session123:task456:tool:my_tool")
-`{'session_id': 'session123', 'client_task_id': 'task456', 'task_type': 'tool', 'component_identifier': 'my_tool'}`
->>> parse_task_key("session123:task456:resource:file%3A%2F%2Fdata.txt")
-`{'session_id': 'session123', 'client_task_id': 'task456', 'task_type': 'resource', 'component_identifier': 'file://data.txt'}`
+>>> parse_task_key("auth:client-a:task456:tool:my_tool")
+`{'task_scope': 'client-a', 'client_task_id': 'task456', 'task_type': 'tool', 'component_identifier': 'my_tool'}`
+>>> parse_task_key("anon:task456:tool:my_tool")
+`{'task_scope': None, 'client_task_id': 'task456', 'task_type': 'tool', 'component_identifier': 'my_tool'}`
-### `get_client_task_id_from_key`
+### `get_client_task_id_from_key`
```python
get_client_task_id_from_key(task_key: str) -> str
@@ -85,5 +97,37 @@ Extract just the client task ID from a task key.
- `task_key`: Full encoded task key
**Returns:**
-- Client-provided task ID (second segment)
+- Client-provided task ID
+
+**Examples:**
+
+>>> get_client_task_id_from_key("auth:client-a:task456:tool:my_tool")
+'task456'
+>>> get_client_task_id_from_key("anon:task456:tool:my_tool")
+'task456'
+
+
+### `task_redis_prefix`
+
+```python
+task_redis_prefix(task_scope: str | None) -> str
+```
+
+
+Return the Redis key prefix that owns a given scope.
+
+Authenticated tasks live under ``fastmcp:task:auth:{enc_scope}``;
+anonymous tasks live under ``fastmcp:task:anon``. Callers append
+``f":{task_id}:..."`` to compose the final key.
+
+
+## Classes
+
+### `TaskKeyParts`
+
+
+Decoded segments of a Docket task key.
+
+``task_scope`` is ``None`` for anonymous tasks, the raw scope string
+otherwise.
diff --git a/docs/python-sdk/fastmcp-server-tasks-notifications.mdx b/docs/python-sdk/fastmcp-server-tasks-notifications.mdx
index 217b4b6ac..441760f34 100644
--- a/docs/python-sdk/fastmcp-server-tasks-notifications.mdx
+++ b/docs/python-sdk/fastmcp-server-tasks-notifications.mdx
@@ -67,7 +67,7 @@ This loop:
- `fastmcp`: FastMCP server instance (for elicitation relay)
-### `ensure_subscriber_running`
+### `ensure_subscriber_running`
```python
ensure_subscriber_running(session_id: str, session: ServerSession, docket: Docket, fastmcp: FastMCP) -> None
@@ -86,7 +86,7 @@ Safe to call multiple times for the same session.
- `fastmcp`: FastMCP server instance (for elicitation relay)
-### `stop_subscriber`
+### `stop_subscriber`
```python
stop_subscriber(session_id: str) -> None
@@ -102,7 +102,7 @@ for delivery if client reconnects (with TTL expiration).
- `session_id`: Session identifier
-### `get_subscriber_count`
+### `get_subscriber_count`
```python
get_subscriber_count() -> int
diff --git a/docs/python-sdk/fastmcp-server-tasks-requests.mdx b/docs/python-sdk/fastmcp-server-tasks-requests.mdx
index a8b31a13d..64ac4a263 100644
--- a/docs/python-sdk/fastmcp-server-tasks-requests.mdx
+++ b/docs/python-sdk/fastmcp-server-tasks-requests.mdx
@@ -16,7 +16,7 @@ This module requires fastmcp[tasks] (pydocket). It is only imported when docket
## Functions
-### `tasks_get_handler`
+### `tasks_get_handler`
```python
tasks_get_handler(server: FastMCP, params: dict[str, Any]) -> GetTaskResult
@@ -33,7 +33,7 @@ Handle MCP 'tasks/get' request (SEP-1686).
- Task status response with spec-compliant fields
-### `tasks_result_handler`
+### `tasks_result_handler`
```python
tasks_result_handler(server: FastMCP, params: dict[str, Any]) -> Any
@@ -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-telemetry.mdx b/docs/python-sdk/fastmcp-server-telemetry.mdx
index 09d2b0d76..6e0b44ba9 100644
--- a/docs/python-sdk/fastmcp-server-telemetry.mdx
+++ b/docs/python-sdk/fastmcp-server-telemetry.mdx
@@ -10,7 +10,7 @@ Server-side telemetry helpers.
## Functions
-### `get_auth_span_attributes`
+### `get_auth_span_attributes`
```python
get_auth_span_attributes() -> dict[str, str]
@@ -20,7 +20,7 @@ get_auth_span_attributes() -> dict[str, str]
Get auth attributes for the current request, if authenticated.
-### `get_session_span_attributes`
+### `get_session_span_attributes`
```python
get_session_span_attributes() -> dict[str, str]
@@ -30,10 +30,10 @@ get_session_span_attributes() -> dict[str, str]
Get session attributes for the current request.
-### `server_span`
+### `server_span`
```python
-server_span(name: str, method: str, server_name: str, component_type: str, component_key: str, resource_uri: str | None = None) -> Generator[Span, None, None]
+server_span(name: str, method: str, server_name: str, component_type: str, component_key: str, resource_uri: str | None = None, tool_name: str | None = None, prompt_name: str | None = None) -> Generator[Span, None, None]
```
@@ -42,10 +42,10 @@ Create a SERVER span with standard MCP attributes and auth context.
Automatically records any exception on the span and sets error status.
-### `delegate_span`
+### `delegate_span`
```python
-delegate_span(name: str, provider_type: str, component_key: str) -> Generator[Span, None, None]
+delegate_span(name: str, provider_type: str, component_key: str, method: str | None = None) -> Generator[Span, None, None]
```
diff --git a/docs/python-sdk/fastmcp-tools-base.mdx b/docs/python-sdk/fastmcp-tools-base.mdx
index 4f8d362ea..0123949a2 100644
--- a/docs/python-sdk/fastmcp-tools-base.mdx
+++ b/docs/python-sdk/fastmcp-tools-base.mdx
@@ -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 6264ef5d3..2b453173f 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 d18d659f8..d7194a7aa 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
@@ -22,26 +22,36 @@ Standalone decorator to mark a function as an MCP tool.
Returns the original function with metadata attached. Register with a server
using mcp.add_tool().
+**Args:**
+- `run_in_thread`: Applies to sync tool functions only. When True (default),
+the sync function is dispatched to a worker thread so it does not
+block the event loop. Set to False to run the function inline on the
+event loop thread — useful for libraries with thread affinity
+(e.g. Windows COM via `uiautomation`/`comtypes`/`pywin32`, `tkinter`,
+some GPU/driver bindings). Ignored for async functions. Cannot be
+combined with `timeout` on a sync function\: inline calls have no
+cancellation checkpoints, so the timeout would be a silent no-op.
+
## Classes
-### `DecoratedTool`
+### `DecoratedTool`
Protocol for functions decorated with @tool.
-### `ToolMeta`
+### `ToolMeta`
Metadata attached to functions by the @tool decorator.
-### `FunctionTool`
+### `FunctionTool`
**Methods:**
-#### `from_function`
+#### `from_function`
```python
from_function(cls, fn: Callable[..., Any]) -> FunctionTool
@@ -57,7 +67,7 @@ individual parameters must not be passed.
Cannot be used together with metadata parameter.
-#### `run`
+#### `run`
```python
run(self, arguments: dict[str, Any]) -> ToolResult
@@ -66,7 +76,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
@@ -74,13 +84,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. The function is wrapped to
-eagerly restore HTTP headers from Redis so that get_http_request()
-works even without explicit dependency injection.
+Registers the raw function so Docket sees and resolves ALL
+dependencies — both FastMCP's (CurrentContext, Progress) and
+Docket-native ones (Retry, Timeout, ConcurrencyLimit).
-#### `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 24f6270aa..9ba17c0e6 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
@@ -227,17 +227,14 @@ argument names.
- `version`: New version for the tool. Defaults to parent tool's version.
- `title`: New title for the tool. Defaults to parent tool's title.
- `transform_args`: Optional transformations for parent tool arguments.
-Only specified arguments are transformed, others pass through unchanged\:
-- Simple rename (str)
-- Complex transformation (rename/description/default/drop) (ArgTransform)
-- Drop the argument (None)
+Only specified arguments are transformed, others pass through unchanged.
+Use ArgTransform for rename, description, default, or hide operations.
- `description`: New description. Defaults to parent's description.
- `tags`: New tags. Defaults to parent's tags.
- `annotations`: New annotations. Defaults to parent's annotations.
- `output_schema`: Control output schema for structured outputs\:
- None (default)\: Inherit from transform_fn if available, then parent tool
- dict\: Use custom output schema
-- False\: Disable output schema and structured outputs
- `serializer`: Deprecated. Return ToolResult from your tools for full control over serialization.
- `meta`: Control meta information\:
- NotSet (default)\: Inherit from parent tool
@@ -293,7 +290,7 @@ async def custom_output(**kwargs) -> ToolResult:
```
-### `ToolTransformConfig`
+### `ToolTransformConfig`
Provides a way to transform a tool.
@@ -301,7 +298,7 @@ Provides a way to transform a tool.
**Methods:**
-#### `apply`
+#### `apply`
```python
apply(self, tool: Tool) -> TransformedTool
diff --git a/docs/python-sdk/fastmcp-utilities-async_utils.mdx b/docs/python-sdk/fastmcp-utilities-async_utils.mdx
index 75c6edd47..d91c4e9ba 100644
--- a/docs/python-sdk/fastmcp-utilities-async_utils.mdx
+++ b/docs/python-sdk/fastmcp-utilities-async_utils.mdx
@@ -10,7 +10,7 @@ Async utilities for FastMCP.
## Functions
-### `is_coroutine_function`
+### `is_coroutine_function`
```python
is_coroutine_function(fn: Any) -> bool
@@ -24,7 +24,7 @@ Check if a callable is a coroutine function, unwrapping functools.partial.
This helper unwraps any layers of ``partial`` before checking.
-### `call_sync_fn_in_threadpool`
+### `call_sync_fn_in_threadpool`
```python
call_sync_fn_in_threadpool(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> Any
@@ -37,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 e45129a28..ab05e9d29 100644
--- a/docs/python-sdk/fastmcp-utilities-components.mdx
+++ b/docs/python-sdk/fastmcp-utilities-components.mdx
@@ -64,8 +64,15 @@ The @ suffix is ALWAYS present to enable unambiguous parsing of keys
Subclasses should override this to use their specific identifier.
Base implementation uses name.
+Prefer `.key` over ad-hoc `name or uri or uri_template` logic for any
+cross-component identity work (dedupe, grouping, collision detection,
+lookup tables). It encodes type, identifier, and version, so variants
+of the same component don't falsely collide with each other, and
+cross-type identifiers (e.g. a tool and a resource both named "foo")
+can't clash.
-#### `get_meta`
+
+#### `get_meta`
```python
get_meta(self) -> dict[str, Any]
@@ -80,7 +87,7 @@ Returns a dict that always includes a `fastmcp` key containing:
Internal keys (prefixed with `_`) are stripped from the fastmcp namespace.
-#### `enable`
+#### `enable`
```python
enable(self) -> None
@@ -89,7 +96,7 @@ enable(self) -> None
Removed in 3.0. Use server.enable(keys=[...]) instead.
-#### `disable`
+#### `disable`
```python
disable(self) -> None
@@ -98,7 +105,7 @@ disable(self) -> None
Removed in 3.0. Use server.disable(keys=[...]) instead.
-#### `copy`
+#### `copy`
```python
copy(self) -> Self
@@ -107,7 +114,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 +126,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 +143,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-docstring_parsing.mdx b/docs/python-sdk/fastmcp-utilities-docstring_parsing.mdx
new file mode 100644
index 000000000..c3fff9eea
--- /dev/null
+++ b/docs/python-sdk/fastmcp-utilities-docstring_parsing.mdx
@@ -0,0 +1,39 @@
+---
+title: docstring_parsing
+sidebarTitle: docstring_parsing
+---
+
+# `fastmcp.utilities.docstring_parsing`
+
+
+Extract descriptions from function docstrings.
+
+Uses griffelib to parse Google, NumPy, and Sphinx-style docstrings. The
+interface is intentionally narrow — a single function returning a
+`ParsedDocstring` — so the implementation can be swapped without touching
+callers.
+
+
+## Functions
+
+### `parse_docstring`
+
+```python
+parse_docstring(fn: Callable[..., Any]) -> ParsedDocstring
+```
+
+
+Parse a function's docstring into a summary and parameter descriptions.
+
+Tries Google, NumPy, and Sphinx parsers in order, using the first one that
+successfully extracts parameter descriptions. If none do, returns the full
+docstring as the description with no parameter descriptions.
+
+
+## Classes
+
+### `ParsedDocstring`
+
+
+The extracted description and per-parameter descriptions from a docstring.
+
diff --git a/docs/python-sdk/fastmcp-utilities-json_schema.mdx b/docs/python-sdk/fastmcp-utilities-json_schema.mdx
index 86ca44f9e..4372c9ab9 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]
@@ -40,7 +40,7 @@ schemas from untrusted servers.
- when no longer needed
-### `resolve_root_ref`
+### `resolve_root_ref`
```python
resolve_root_ref(schema: dict[str, Any]) -> dict[str, Any]
@@ -62,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-json_schema_type.mdx b/docs/python-sdk/fastmcp-utilities-json_schema_type.mdx
index b1137e7de..d505b28b7 100644
--- a/docs/python-sdk/fastmcp-utilities-json_schema_type.mdx
+++ b/docs/python-sdk/fastmcp-utilities-json_schema_type.mdx
@@ -22,6 +22,24 @@ for validation with Pydantic. It supports:
- Enums and constants
- Union types
+## Unsupported regex patterns
+
+Pydantic uses a Rust-based regex engine that does not support all regex
+features found in real-world JSON Schemas (particularly those from AWS,
+Azure, and other large OpenAPI providers). Unsupported constructs include
+lookahead/lookbehind assertions (`(?!...)`, `(?<=...)`), Unicode property
+escapes (`\p{Graph}`, `\p{Print}`), and very large compiled patterns.
+
+When a `pattern` constraint cannot be compiled, `json_schema_to_type`
+degrades gracefully:
+
+1. The pattern is **dropped** from the Pydantic `StringConstraints` so
+ the type will not raise a `SchemaError`.
+2. A `UserWarning` is emitted with the unsupported pattern.
+3. The original pattern is preserved in the type metadata as
+ `x-unsupported-pattern` (visible via `TypeAdapter(T).json_schema()`).
+4. Other constraints (`minLength`, `maxLength`) are still enforced.
+
Example:
```python
schema = {
@@ -42,17 +60,18 @@ Example:
## Functions
-### `json_schema_to_type`
+### `json_schema_to_type`
```python
-json_schema_to_type(schema: Mapping[str, Any], name: str | None = None) -> type
+json_schema_to_type(schema: Mapping[str, Any] | bool, name: str | None = None) -> type
```
Convert JSON schema to appropriate Python type with validation.
**Args:**
-- `schema`: A JSON Schema dictionary defining the type structure and validation rules
+- `schema`: A JSON Schema dictionary defining the type structure and validation rules.
+Boolean schemas are also accepted (``True`` = any type, ``False`` = unsatisfiable).
- `name`: Optional name for object schemas. Only allowed when schema type is "object".
If not provided for objects, name will be inferred from schema's "title"
property or default to "Root".
@@ -107,4 +126,4 @@ class Name:
## Classes
-### `JSONSchema`
+### `JSONSchema`
diff --git a/docs/python-sdk/fastmcp-utilities-openapi-director.mdx b/docs/python-sdk/fastmcp-utilities-openapi-director.mdx
index 1d0687637..8fc5e2216 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-openapi-json_schema_converter.mdx b/docs/python-sdk/fastmcp-utilities-openapi-json_schema_converter.mdx
index abd1d6869..ad596a5c9 100644
--- a/docs/python-sdk/fastmcp-utilities-openapi-json_schema_converter.mdx
+++ b/docs/python-sdk/fastmcp-utilities-openapi-json_schema_converter.mdx
@@ -16,7 +16,7 @@ for our specific use case.
## Functions
-### `convert_openapi_schema_to_json_schema`
+### `convert_openapi_schema_to_json_schema`
```python
convert_openapi_schema_to_json_schema(schema: dict[str, Any], openapi_version: str | None = None, remove_read_only: bool = False, remove_write_only: bool = False, convert_one_of_to_any_of: bool = True) -> dict[str, Any]
@@ -43,7 +43,7 @@ This is a clean, systematic approach that:
- JSON Schema-compatible dictionary
-### `convert_schema_definitions`
+### `convert_schema_definitions`
```python
convert_schema_definitions(schema_definitions: dict[str, Any] | None, openapi_version: str | None = None, **kwargs) -> dict[str, Any]
diff --git a/docs/python-sdk/fastmcp-utilities-openapi-schemas.mdx b/docs/python-sdk/fastmcp-utilities-openapi-schemas.mdx
index fad88ee5f..47ee1e699 100644
--- a/docs/python-sdk/fastmcp-utilities-openapi-schemas.mdx
+++ b/docs/python-sdk/fastmcp-utilities-openapi-schemas.mdx
@@ -10,7 +10,7 @@ Schema manipulation utilities for OpenAPI operations.
## Functions
-### `clean_schema_for_display`
+### `clean_schema_for_display`
```python
clean_schema_for_display(schema: JsonSchema | None) -> JsonSchema | None
@@ -20,7 +20,7 @@ clean_schema_for_display(schema: JsonSchema | None) -> JsonSchema | None
Clean up a schema dictionary for display by removing internal/complex fields.
-### `extract_output_schema_from_responses`
+### `extract_output_schema_from_responses`
```python
extract_output_schema_from_responses(responses: dict[str, ResponseInfo], schema_definitions: dict[str, Any] | None = None, openapi_version: str | None = None) -> dict[str, Any] | None
diff --git a/docs/servers/auth/authentication.mdx b/docs/servers/auth/authentication.mdx
index 9a26df138..d37c57f36 100644
--- a/docs/servers/auth/authentication.mdx
+++ b/docs/servers/auth/authentication.mdx
@@ -161,7 +161,7 @@ The implementation provides all required OAuth endpoints including authorization
```python
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.oauth import MyOAuthProvider
+from fastmcp.server.auth import OAuthProvider
auth = MyOAuthProvider(
user_store=your_user_database,
diff --git a/docs/servers/auth/oauth-proxy.mdx b/docs/servers/auth/oauth-proxy.mdx
index 19f5c2cee..aff1267ff 100644
--- a/docs/servers/auth/oauth-proxy.mdx
+++ b/docs/servers/auth/oauth-proxy.mdx
@@ -117,6 +117,12 @@ mcp = FastMCP(name="My Server", auth=auth)
This URL is used to construct OAuth callback URLs and operational endpoints. When mounting under a path prefix, include that prefix in `base_url`. Use `issuer_url` separately to specify where auth server metadata is located (typically at root level).
+
+ Optional public base URL for the protected resource metadata and token audience.
+
+ Use this when your OAuth callbacks and operational endpoints need to live under one public URL, but the protected MCP resource should be advertised under another. FastMCP will still append the MCP mount path (for example, `/mcp`) to this base URL.
+
+
Path for OAuth callbacks. Must match the redirect URI configured in your OAuth
application
@@ -165,6 +171,14 @@ mcp = FastMCP(name="My Server", auth=auth)
provider doesn't support PKCE
+
+ Whether to forward RFC 8707 `resource` parameters from MCP clients to the
+ upstream OAuth provider. When enabled, the proxy includes the resource indicator
+ in authorization requests, allowing providers that support RFC 8707 to scope
+ tokens to specific resources. Disable for providers that reject unknown
+ parameters.
+
+
Token endpoint authentication method for the upstream OAuth server. Controls
how the proxy authenticates when exchanging authorization codes and refresh
@@ -283,14 +297,20 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
-
- Whether to require user consent before authorizing MCP clients. When enabled (default), users see a consent screen that displays which client is requesting access, preventing [confused deputy attacks](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#confused-deputy-problem) by ensuring users explicitly approve new clients.
+
+ Consent screen behavior for authorization requests. The consent page displays which client is requesting access, defending against [confused deputy and AS-in-the-middle attacks](#confused-deputy-attacks) by requiring explicit user approval.
- **Default behavior (True):**
- Users see a consent screen on first authorization. Consent choices are remembered via signed cookies, so users only need to approve each client once. This protects against malicious clients impersonating the user.
+ **`True` (default) — always prompt:**
+ Users see the consent screen on every authorization. Strongest protection against AS-in-the-middle attacks where a malicious MCP server redirects the victim's browser into a legitimate proxy and relies on a previously-remembered approval to silently complete the flow.
- **Disabling consent (False):**
- Authorization proceeds directly to the upstream provider without user confirmation. Only use this for local development or testing environments where the security trade-off is acceptable.
+ **`"remember"` — silent consent on return:**
+ Users see the consent screen on first authorization; subsequent flows from the same browser for the same `(client_id, redirect_uri)` are silently approved via a signed cookie. Cross-site navigations (detected via `Sec-Fetch-Site`) fall back to the prompt. `Sec-Fetch-Site` is a browser-level heuristic rather than a protocol guarantee: an attacker who finds a way to initiate a non-cross-site navigation (XSS on a sibling origin, a same-site redirect chain, etc.) can reach the silent-consent path. `True` does not depend on this signal. See [Confused Deputy Attacks](#confused-deputy-attacks) for the underlying attack class.
+
+ **`"external"` — delegate to upstream:**
+ Skip the built-in consent page; consent is collected by the upstream IdP or a custom login page referenced via `upstream_authorization_endpoint`. No security warning is logged.
+
+ **`False` — disable entirely:**
+ Authorization proceeds directly to the upstream provider without any consent UI. Logs a security warning. Only for local development or testing.
```python
# Development/testing only - skip consent screen
@@ -298,6 +318,12 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
...,
require_authorization_consent=False # ⚠️ Security warning: only for local/testing
)
+
+ # Convenience mode - silent consent on return visits (less safe than True)
+ auth = OAuthProxy(
+ ...,
+ require_authorization_consent="remember",
+ )
```
@@ -386,7 +412,7 @@ auth = OAuthProxy(
)
```
-The proxy also automatically forwards RFC 8707 `resource` parameters from MCP clients to upstream providers that support them.
+The proxy also forwards RFC 8707 `resource` parameters from MCP clients to upstream providers that support them. This is enabled by default via the `forward_resource` parameter. Disable it for providers that reject unknown parameters.
## OAuth Flow
@@ -625,7 +651,7 @@ The OAuth proxy works by bridging DCR clients to traditional auth providers, whi
FastMCP's OAuth proxy defends against confused deputy attacks with two layers of protection:
-**Consent screen.** Before any authorization happens, you see a consent page showing the client's details, redirect URI, and requested scopes. This gives you the opportunity to review and deny suspicious requests. Once you approve a client, it's remembered so you don't see the consent page again for that client. The consent mechanism is implemented with CSRF tokens and cryptographically signed cookies to prevent tampering.
+**Consent screen.** Before any authorization happens, you see a consent page showing the client's details, redirect URI, and requested scopes. This gives you the opportunity to review and deny suspicious requests. By default (`require_authorization_consent=True`), the page is shown on every flow, which is the strongest protection. Setting `require_authorization_consent="remember"` approves previously-approved `(client_id, redirect_uri)` pairs silently on return visits, trading some protection for UX (see below). The consent mechanism is implemented with CSRF tokens and cryptographically signed cookies to prevent tampering.

@@ -633,6 +659,12 @@ The consent page automatically displays your server's name, icon, and website UR
**Browser-session binding.** When you approve consent (or when a previously-approved client auto-approves), the proxy sets a cryptographically signed cookie that binds your browser session to the authorization flow. When the identity provider redirects back to the proxy's callback, the proxy verifies that this cookie is present and matches the expected transaction. A different browser — such as a victim who was sent the authorization URL by an attacker — won't have this cookie, and the callback will be rejected with a 403 error. This prevents the attack even when the identity provider skips the consent page for previously-authorized applications.
+#### AS-in-the-middle variant
+
+A related attack works even with browser-session binding in place: a malicious MCP server advertises its own authorization server, which redirects the victim's browser into the legitimate proxy's `/authorize` endpoint. Because the victim's browser carries both the prior-approval cookie and the newly-issued session-binding cookie throughout, both layers pass. The defense is the consent prompt itself: if consent is shown (`require_authorization_consent=True`), the victim sees the benign MCP server's name on the consent page — which doesn't match the malicious server they thought they were connecting to — and can deny.
+
+`require_authorization_consent="remember"` adds a `Sec-Fetch-Site` check to keep this path safe for legitimate return flows (the attack navigation lands as `cross-site` and falls back to the prompt), but this is a browser-level heuristic. For the strongest defense, leave `require_authorization_consent=True`.
+
**Learn more:**
- [MCP Security Best Practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#confused-deputy-problem) - Official specification guidance
- [Confused Deputy Attacks Explained](https://den.dev/blog/mcp-confused-deputy-api-management/) - Detailed walkthrough by Den Delimarsky
diff --git a/docs/servers/auth/oidc-proxy.mdx b/docs/servers/auth/oidc-proxy.mdx
index 73ab43e73..d33cd611a 100644
--- a/docs/servers/auth/oidc-proxy.mdx
+++ b/docs/servers/auth/oidc-proxy.mdx
@@ -79,6 +79,12 @@ mcp = FastMCP(name="My Server", auth=auth)
Public URL of your FastMCP server (e.g., `https://your-server.com`)
+
+ Optional public base URL for the protected resource metadata and token audience.
+
+ Use this when your OAuth callbacks and operational endpoints need to live under one public URL, but the protected MCP resource should be advertised under another. FastMCP will still append the MCP mount path (for example, `/mcp`) to this base URL.
+
+
Strict flag for configuration validation. When True, requires all OIDC
mandatory fields.
@@ -192,8 +198,8 @@ auth = OIDCProxy(
-
- Whether to require user consent before authorizing MCP clients. When enabled (default), users see a consent screen that displays which client is requesting access. See [OAuthProxy documentation](/servers/auth/oauth-proxy#confused-deputy-attacks) for details on confused deputy attack protection.
+
+ Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (consent handled by upstream IdP or custom page), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs.
diff --git a/docs/servers/context.mdx b/docs/servers/context.mdx
index a10161baf..d442743ab 100644
--- a/docs/servers/context.mdx
+++ b/docs/servers/context.mdx
@@ -277,6 +277,43 @@ mcp = FastMCP("distributed-app", session_state_store=RedisStore(...))
Any backend compatible with the [py-key-value-aio](https://github.com/strawgate/py-key-value) `AsyncKeyValue` protocol works. See [Storage Backends](/servers/storage-backends) for more options including Redis, DynamoDB, and MongoDB.
+#### State and Mounted Servers
+
+Each `FastMCP` instance has its own session state store. When you `mount()` a child server, state set on the parent is not visible to tools on the child, and vice versa:
+
+```python
+from fastmcp import FastMCP, Context
+from fastmcp.server.middleware import Middleware, MiddlewareContext
+
+parent = FastMCP("Parent")
+child = FastMCP("Child")
+parent.mount(child, namespace="child")
+
+class Stasher(Middleware):
+ async def on_call_tool(self, context: MiddlewareContext, call_next):
+ await context.fastmcp_context.set_state("user", "alice")
+ return await call_next(context)
+
+parent.add_middleware(Stasher())
+
+@child.tool
+async def whoami(ctx: Context) -> str:
+ return await ctx.get_state("user") or "unknown" # returns "unknown"
+```
+
+To share state across the mount boundary, pass the same store to both servers:
+
+```python
+from key_value.aio.stores.memory import MemoryStore
+
+store = MemoryStore()
+parent = FastMCP("Parent", session_state_store=store)
+child = FastMCP("Child", session_state_store=store)
+parent.mount(child, namespace="child")
+```
+
+Alternatively, state set with `serializable=False` lives on the request context and is inherited by mounted children automatically — use it when the value is request-scoped and does not need to persist across tool calls.
+
#### State During Initialization
State set during `on_initialize` middleware persists to subsequent tool calls when using the same session object (STDIO, SSE, single-server HTTP). For distributed/serverless HTTP deployments where different machines handle init and tool calls, state is isolated by the `mcp-session-id` header.
diff --git a/docs/servers/elicitation.mdx b/docs/servers/elicitation.mdx
index 600a3d2fe..923e704c6 100644
--- a/docs/servers/elicitation.mdx
+++ b/docs/servers/elicitation.mdx
@@ -156,6 +156,28 @@ async def pick_a_boolean(ctx: Context) -> str:
```
+#### Customizing the Field Label
+
+
+
+When FastMCP wraps a scalar, `Literal`, `Enum`, or one of the constrained-option shorthands, the wrapper's `value` property is labelled `"Value"` by default — and some clients (including VS Code) render that label directly in the UI. Pass `response_title` and `response_description` to override it:
+
+```python
+@mcp.tool
+async def confirm_purchase(ctx: Context) -> str:
+ result = await ctx.elicit(
+ "Buy 1x Baguette?",
+ response_type=bool,
+ response_title="Confirm purchase",
+ response_description="Approve this transaction?",
+ )
+ if result.action == "accept":
+ return "Purchased" if result.data else "Declined"
+ return "No response"
+```
+
+These arguments only apply when FastMCP is adding the wrapper. For structured responses (`BaseModel`, dataclass, `TypedDict`), set the metadata on the individual fields via `Field(title=..., description=...)` — passing `response_title` or `response_description` alongside a model type raises `TypeError`.
+
### No Response
Sometimes, the goal of an elicitation is to simply get a user to approve or reject an action. Pass `None` as the response type to indicate that no data is expected. The `data` field will be `None` when the user accepts.
diff --git a/docs/servers/middleware.mdx b/docs/servers/middleware.mdx
index 1a18d89e3..37713a8d7 100644
--- a/docs/servers/middleware.mdx
+++ b/docs/servers/middleware.mdx
@@ -84,6 +84,8 @@ parent.mount(child, namespace="child")
Requests to `child_tool` flow through the parent's `AuthMiddleware` first, then through the child's `LoggingMiddleware`.
+Middleware-stored state does not automatically cross mount boundaries. If `AuthMiddleware` on the parent calls `ctx.set_state("user_id", ...)`, a tool on the child server calling `ctx.get_state("user_id")` will get `None` — each `FastMCP` instance owns its own session state store. To share state across the mount, either pass the same `session_state_store` to both servers or use `serializable=False` for request-scoped values. See [State and Mounted Servers](/servers/context#state-and-mounted-servers) for details.
+
## Hooks
Rather than processing every message identically, FastMCP provides specialized hooks at different levels of specificity. Multiple hooks fire for a single request, going from general to specific:
diff --git a/docs/servers/prompts.mdx b/docs/servers/prompts.mdx
index bdfefc24e..b5cf2f6e9 100644
--- a/docs/servers/prompts.mdx
+++ b/docs/servers/prompts.mdx
@@ -54,7 +54,7 @@ def generate_code_request(language: str, task_description: str) -> list[Message]
* **Parameters:** The function parameters define the inputs needed to generate the prompt.
* **Inferred Metadata:** By default:
* Prompt Name: Taken from the function name (`ask_about_topic`).
- * Prompt Description: Taken from the function's docstring.
+ * Prompt Description: Taken from the summary of the function's docstring. If the docstring includes parameter descriptions (Google, NumPy, or Sphinx style), they populate each prompt argument's description in the MCP protocol (see [Argument Descriptions](#argument-descriptions)).
Functions with `*args` or `**kwargs` are not supported as prompts. This restriction exists because FastMCP needs to generate a complete parameter schema for the MCP protocol, which isn't possible with variable argument lists.
@@ -88,7 +88,7 @@ def data_analysis_prompt(
- Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose
+ Provides the description exposed via MCP. If set, the function's docstring is ignored for the prompt description, though docstring-derived argument descriptions still apply (see [Argument Descriptions](#argument-descriptions)).
@@ -201,6 +201,28 @@ Good choices: `list[int]`, `dict[str, str]`, `float`, `bool`
Avoid: Complex Pydantic models, deeply nested structures, custom classes
+### Argument Descriptions
+
+
+
+FastMCP parses your function's docstring to extract the prompt description and per-argument descriptions. Google, NumPy, and Sphinx styles are all supported:
+
+```python
+@mcp.prompt
+def analyze_data(dataset: str, method: str = "summary") -> str:
+ """Generate an analysis prompt for a dataset.
+
+ Args:
+ dataset: URI or identifier of the dataset to analyze.
+ method: Type of analysis to perform (summary, detailed, etc).
+ """
+ return f"Please perform a '{method}' analysis on {dataset}."
+```
+
+The free-form text above the `Args` section — whether a single line or multiple paragraphs — becomes the prompt description, and each argument's docstring entry becomes the description on the corresponding `PromptArgument` in the MCP protocol. Sections like `Returns`, `Raises`, and `Example` are excluded from the description but otherwise ignored.
+
+If an argument already has an explicit description — via `Annotated[x, "..."]` or `Field(description=...)` — that description takes precedence over the docstring. This makes it safe to adopt docstring-based descriptions incrementally: existing annotations keep working, and docstrings fill in the gaps.
+
### Return Values
Prompt functions must return one of these types:
@@ -262,7 +284,7 @@ Message(["item1", "item2"])
`PromptResult` gives you explicit control over prompt responses: multiple messages, roles, and metadata at both the message and result level.
-```python
+```python test="skip"
from fastmcp import FastMCP
from fastmcp.prompts import PromptResult, Message
diff --git a/docs/servers/providers/filesystem.mdx b/docs/servers/providers/filesystem.mdx
index a5b798faa..353a671d5 100644
--- a/docs/servers/providers/filesystem.mdx
+++ b/docs/servers/providers/filesystem.mdx
@@ -34,13 +34,13 @@ from pathlib import Path
from fastmcp import FastMCP
from fastmcp.server.providers import FileSystemProvider
-mcp = FastMCP("MyServer", providers=[FileSystemProvider(Path(__file__).parent / "mcp")])
+mcp = FastMCP("MyServer", providers=[FileSystemProvider(Path(__file__).parent / "components")])
```
-In your `mcp/` directory, create Python files with decorated functions.
+In your `components/` directory, create Python files with decorated functions.
```python
-# mcp/tools/greet.py
+# components/tools/greet.py
from fastmcp.tools import tool
@tool
@@ -114,7 +114,7 @@ The decorator supports: `uri` (required), `name`, `title`, `description`, `icons
Mark a function as a prompt template.
-```python
+```python test="skip"
from fastmcp.prompts import prompt
@prompt
@@ -139,7 +139,7 @@ The decorator supports: `name`, `title`, `description`, `icons`, `tags`, and `me
The directory structure is purely organizational. The provider recursively scans all `.py` files regardless of which subdirectory they're in. Subdirectories like `tools/`, `resources/`, and `prompts/` are optional conventions that help you organize code.
```
-mcp/
+components/
├── tools/
│ ├── greeting.py # @tool functions
│ └── calculator.py # @tool functions
@@ -152,7 +152,7 @@ mcp/
You can also put all components in a single file or organize by feature rather than type.
```
-mcp/
+components/
├── user_management.py # @tool, @resource, @prompt for users
├── billing.py # @tool, @resource for billing
└── analytics.py # @tool for analytics
@@ -176,9 +176,9 @@ The provider follows these rules when scanning:
If your directory contains an `__init__.py` file, the provider imports files as proper Python package members. This means relative imports work correctly within your components directory.
```python
-# mcp/__init__.py exists
+# components/__init__.py exists
-# mcp/tools/greeting.py
+# components/tools/greeting.py
from ..helpers import format_name # Relative imports work
@tool
@@ -197,7 +197,7 @@ from pathlib import Path
from fastmcp.server.providers import FileSystemProvider
-provider = FileSystemProvider(Path(__file__).parent / "mcp", reload=True)
+provider = FileSystemProvider(Path(__file__).parent / "components", reload=True)
```
With `reload=True`, the provider:
@@ -227,7 +227,7 @@ A complete example is available in the repository at `examples/filesystem-provid
```
examples/filesystem-provider/
├── server.py # Server entry point
-└── mcp/
+└── components/
├── tools/
│ ├── greeting.py # greet, farewell tools
│ └── calculator.py # add, multiply tools
@@ -246,7 +246,7 @@ from fastmcp import FastMCP
from fastmcp.server.providers import FileSystemProvider
provider = FileSystemProvider(
- root=Path(__file__).parent / "mcp",
+ root=Path(__file__).parent / "components",
reload=True,
)
diff --git a/docs/servers/server.mdx b/docs/servers/server.mdx
index 6b192a3b0..b85ed034e 100644
--- a/docs/servers/server.mdx
+++ b/docs/servers/server.mdx
@@ -123,6 +123,12 @@ These parameters control how your server presents itself to clients.
List of icon representations for your server. See [Icons](/servers/icons) for details
+
+
+
+
+ Arbitrary experimental capabilities to advertise in the MCP `initialize` response. Use this to declare cross-server interop conventions or draft extensions that follow the MCP spec's `experimental` field. Keys are capability names; values are free-form dicts. FastMCP's built-in derived capabilities (`tools`, `resources`, etc.) are unaffected — this only populates `capabilities.experimental`
+
### Composition
diff --git a/docs/servers/storage-backends.mdx b/docs/servers/storage-backends.mdx
index e743b5dab..d13ce176d 100644
--- a/docs/servers/storage-backends.mdx
+++ b/docs/servers/storage-backends.mdx
@@ -248,7 +248,7 @@ The [FastMCP Client](/clients/client) uses storage for persisting OAuth tokens l
```python
from pathlib import Path
-from fastmcp.client.auth import OAuthClientProvider
+from fastmcp.client.auth import OAuth
from key_value.aio.stores.filetree import (
FileTreeStore,
FileTreeV1KeySanitizationStrategy,
@@ -263,7 +263,7 @@ token_storage = FileTreeStore(
collection_sanitization_strategy=FileTreeV1CollectionSanitizationStrategy(token_dir),
)
-oauth_provider = OAuthClientProvider(
+oauth_provider = OAuth(
mcp_url="https://your-mcp-server.com/mcp/sse",
token_storage=token_storage
)
diff --git a/docs/servers/telemetry.mdx b/docs/servers/telemetry.mdx
index 1055dc0c0..aed7308db 100644
--- a/docs/servers/telemetry.mdx
+++ b/docs/servers/telemetry.mdx
@@ -61,14 +61,14 @@ The server creates spans for each operation using [MCP semantic conventions](htt
| Span Name | Description |
|-----------|-------------|
| `tools/call {name}` | Tool execution (e.g., `tools/call get_weather`) |
-| `resources/read {uri}` | Resource read (e.g., `resources/read config://database`) |
+| `resources/read` | Resource read (URI in `mcp.resource.uri` attribute, not span name) |
| `prompts/get {name}` | Prompt render (e.g., `prompts/get greeting`) |
For mounted servers, an additional `delegate {name}` span shows the delegation to the child server.
### Client Spans
-The FastMCP client creates spans for outgoing requests with the same naming pattern (`tools/call {name}`, `resources/read {uri}`, `prompts/get {name}`).
+The FastMCP client creates spans for outgoing requests with the same naming pattern (`tools/call {name}`, `resources/read`, `prompts/get {name}`).
### Span Hierarchy
@@ -176,6 +176,73 @@ async def complex_operation(input: str) -> str:
return result
```
+### Where custom spans help most
+
+Custom spans are most useful around work that is expensive or hard to debug:
+
+- External calls such as databases, vector stores, HTTP APIs, or queue operations
+- Multi-step tool logic where one stage dominates latency
+- Prompt or resource generation that fans out to other systems
+- Sampling calls made from inside a tool via `ctx.sample(...)`
+
+Avoid wrapping every small helper function or simple in-memory transformation. That usually adds noise without making traces easier to interpret.
+
+### Recommended naming and attributes
+
+- Use `{tool_name}.{operation}` or `{resource_name}.{operation}` for child spans such as `search.fetch`, `search.rank`, or `docs.render`
+- Add attributes that explain workload shape, such as counts, sizes, cache hits, or IDs
+- Do not record secrets, prompts with sensitive user data, or raw tokens as span attributes
+- Let exceptions propagate unless you have a specific recovery path; FastMCP's server spans already mark failures and record exceptions
+
+### Instrumenting tools, prompts, and resources
+
+```python
+from fastmcp import FastMCP
+from fastmcp.telemetry import get_tracer
+
+mcp = FastMCP("my-server")
+
+@mcp.tool
+async def search(query: str) -> str:
+ tracer = get_tracer()
+
+ with tracer.start_as_current_span("search.fetch") as span:
+ span.set_attribute("search.query_length", len(query))
+ results = await fetch_results(query)
+ span.set_attribute("search.result_count", len(results))
+
+ with tracer.start_as_current_span("search.rank"):
+ ranked = rank_results(results)
+
+ return format_results(ranked)
+
+@mcp.prompt
+async def summarize_prompt(topic: str) -> str:
+ tracer = get_tracer()
+ with tracer.start_as_current_span("summarize_prompt.render") as span:
+ span.set_attribute("prompt.topic_length", len(topic))
+ return f"Summarize the latest updates about {topic}."
+
+@mcp.resource("docs://{slug}")
+async def docs_resource(slug: str) -> str:
+ tracer = get_tracer()
+ with tracer.start_as_current_span("docs_resource.load") as span:
+ span.set_attribute("docs.slug", slug)
+ return await load_doc(slug)
+```
+
+### Sampling calls inside tools
+
+If your tool uses `ctx.sample(...)`, keep the LLM work nested under the tool span so traces show both application logic and model latency together.
+
+For providers with their own OTEL integrations, prefer enabling that instrumentation rather than manually creating a span around every model call. For example, if you use Google GenAI, `logfire.instrument_google_genai()` will emit child spans with token and request metadata under the active FastMCP tool span.
+
+### Exporter choices
+
+- For local debugging, `ConsoleSpanExporter` or `otel-desktop-viewer` gives quick feedback with minimal setup
+- For shared environments, use OTLP exporters to backends like Logfire, Jaeger, Tempo, Datadog, or New Relic
+- If traces are too noisy, tune sampling in your OpenTelemetry SDK instead of removing FastMCP instrumentation
+
## Error Handling
When errors occur, spans are automatically marked with error status and the exception is recorded:
@@ -186,21 +253,16 @@ def risky_operation() -> str:
raise ValueError("Something went wrong")
# The span will have:
-# - status = ERROR
+# - status = ERROR with exception message as description
+# - error.type = "tool_error" (or exception class name for non-tool errors)
# - exception event with stack trace
```
## Attributes Reference
-### RPC Semantic Conventions
-
-Standard [RPC semantic conventions](https://opentelemetry.io/docs/specs/semconv/rpc/rpc-spans/):
-
-| Attribute | Value |
-|-----------|-------|
-| `rpc.system` | `"mcp"` |
-| `rpc.service` | Server name |
-| `rpc.method` | MCP protocol method |
+
+**Migrating from v3.1 or earlier:** The `rpc.system`, `rpc.service`, and `rpc.method` span attributes were removed in favor of the [MCP semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/) listed below. If you have dashboards or alerts keyed on those `rpc.*` attributes, update them to use `mcp.method.name` and the `fastmcp.*` attributes instead.
+
### MCP Semantic Conventions
@@ -211,6 +273,9 @@ FastMCP implements the [OpenTelemetry MCP semantic conventions](https://opentele
| `mcp.method.name` | The MCP method being called (`tools/call`, `resources/read`, `prompts/get`) |
| `mcp.session.id` | Session identifier for the MCP connection |
| `mcp.resource.uri` | The resource URI (for resource operations) |
+| `gen_ai.tool.name` | Tool name (on `tools/call` spans) |
+| `gen_ai.prompt.name` | Prompt name (on `prompts/get` spans) |
+| `error.type` | Error classification (`tool_error` for ToolError, otherwise exception class name) |
### Auth Attributes
diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx
index 91d96d1be..862066bc7 100644
--- a/docs/servers/tools.mdx
+++ b/docs/servers/tools.mdx
@@ -36,7 +36,7 @@ def add(a: int, b: int) -> int:
When this tool is registered, FastMCP automatically:
- Uses the function name (`add`) as the tool name.
-- Uses the function's docstring (`Adds two integer numbers...`) as the tool description.
+- Parses the function's docstring for the tool description and, if present, per-parameter descriptions (see [Docstring Descriptions](#docstring-descriptions)).
- Generates an input schema based on the function's parameters and type annotations.
- Handles parameter validation and error reporting.
@@ -70,7 +70,7 @@ def search_products_implementation(query: str, category: str | None = None) -> l
- Provides the description exposed via MCP. If set, the function's docstring is ignored for this purpose
+ Provides the description exposed via MCP. If set, the function's docstring is ignored for the tool description, though docstring-derived parameter descriptions still apply (see [Docstring Descriptions](#docstring-descriptions)).
@@ -132,6 +132,10 @@ def search_products_implementation(query: str, category: str | None = None) -> l
Optional JSON schema for the tool's output. When provided, the tool must return structured output matching this schema. If not provided, FastMCP automatically generates a schema from the function's return type annotation. See [Output Schemas](#output-schemas) for details.
+
+
+ Applies to sync tool functions only. When `True` (default), sync functions are dispatched to a thread pool so they don't block the event loop. Set to `False` to run the function inline on the event loop thread — useful for libraries with thread affinity like Windows COM (`pywin32`, `uiautomation`, `comtypes`), `tkinter`, or certain GPU/driver bindings. Ignored for async functions, which always run on the event loop. See [Thread affinity](#thread-affinity) for details.
+
### Using with Methods
@@ -175,6 +179,28 @@ def slow_tool(x: int) -> int:
For I/O-bound operations like network requests or database queries, async tools are still preferred since they're more efficient than threadpool dispatch. Use sync tools when working with synchronous libraries or for simple operations where the threading overhead doesn't matter.
+### Thread affinity
+
+This section applies to sync tools only. Async tools already run on the event loop and are not affected.
+
+Some libraries bind state to the thread they're first used from and break when called from a different thread. The most common case is Windows COM — libraries like `uiautomation`, `comtypes`, and parts of `pywin32` require `CoInitialize` to have been called on the current thread, and worker-pool threads don't initialize COM by default. Similar constraints apply to `tkinter`, some GPU bindings (CUDA contexts), and certain hardware drivers.
+
+For these cases, pass `run_in_thread=False` so FastMCP invokes the sync function inline on the event loop thread instead of dispatching it to a worker:
+
+```python
+import uiautomation as auto
+
+@mcp.tool(run_in_thread=False)
+def list_windows() -> list[str]:
+ """List desktop windows via Windows UI Automation (COM)."""
+ desktop = auto.GetRootControl()
+ return [w.Name for w in desktop.GetChildren()[:5]]
+```
+
+The tradeoff is that the event loop is blocked for the duration of the call — other in-flight requests wait until the tool returns. Keep `run_in_thread=False` reserved for tools that genuinely need thread affinity, and prefer short-running calls in that path.
+
+Inline sync calls have no cancellation checkpoints, so `timeout` cannot interrupt them. Combining `timeout` with `run_in_thread=False` on a sync function is rejected at registration — drop one or the other.
+
## Arguments
By default, FastMCP converts Python functions into MCP tools by inspecting the function's signature and type annotations. This allows you to use standard Python type annotations for your tools. In general, the framework strives to "just work": idiomatic Python behaviors like parameter defaults and type annotations are automatically translated into MCP schemas. However, there are a number of ways to customize the behavior of your tools.
@@ -289,6 +315,33 @@ The default flexible validation mode is recommended for most use cases as it han
You can provide additional metadata about parameters in several ways:
+#### Docstring Descriptions
+
+
+
+FastMCP parses your function's docstring to extract both the tool description and per-parameter descriptions. Google, NumPy, and Sphinx docstring styles are all supported — the parser tries each and uses whichever finds parameter descriptions:
+
+```python
+@mcp.tool
+def process_image(
+ image_url: str,
+ resize: bool = False,
+ width: int = 800,
+) -> dict:
+ """Process an image with optional resizing.
+
+ Args:
+ image_url: URL of the image to process.
+ resize: Whether to resize the image.
+ width: Target width in pixels.
+ """
+ # Implementation...
+```
+
+The free-form text above the `Args` section — whether a single line or multiple paragraphs — becomes the tool description, and each parameter's docstring entry becomes the description for that parameter in the generated schema. Sections like `Returns`, `Raises`, and `Example` are excluded from the description but otherwise ignored.
+
+If a parameter already has an explicit description — via `Annotated[x, "..."]` or `Field(description=...)` — that description takes precedence over the docstring. This makes it safe to adopt docstring-based descriptions incrementally: existing annotations keep working, and docstrings fill in the gaps.
+
#### Simple String Descriptions
@@ -888,15 +941,17 @@ Annotations serve several purposes in client applications:
- Describing the safety profile of tools (destructive vs. non-destructive)
- Signaling if tools interact with external systems
-You can add annotations to a tool using the `annotations` parameter in the `@mcp.tool` decorator:
+You can add annotations to a tool using the `annotations` parameter in the `@mcp.tool` decorator. FastMCP accepts either a plain dict or `ToolAnnotations`; the examples below use `ToolAnnotations` for consistency and stronger editor/type support.
```python
+from mcp.types import ToolAnnotations
+
@mcp.tool(
- annotations={
- "title": "Calculate Sum",
- "readOnlyHint": True,
- "openWorldHint": False
- }
+ annotations=ToolAnnotations(
+ title="Calculate Sum",
+ readOnlyHint=True,
+ openWorldHint=False,
+ )
)
def calculate_sum(a: float, b: float) -> float:
"""Add two numbers together."""
@@ -932,7 +987,7 @@ from mcp.types import ToolAnnotations
mcp = FastMCP("Data Server")
-@mcp.tool(annotations={"readOnlyHint": True})
+@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
def get_user(user_id: str) -> dict:
"""Retrieve user information by ID."""
return {"id": user_id, "name": "Alice"}
@@ -954,7 +1009,7 @@ def update_user(user_id: str, name: str) -> dict:
"""Update user information."""
return {"id": user_id, "name": name, "updated": True}
-@mcp.tool(annotations={"destructiveHint": True})
+@mcp.tool(annotations=ToolAnnotations(destructiveHint=True))
def delete_user(user_id: str) -> dict:
"""Permanently delete a user account."""
return {"deleted": user_id}
diff --git a/docs/servers/transforms/namespace.mdx b/docs/servers/transforms/namespace.mdx
index 715bebd39..fdb0d1c7f 100644
--- a/docs/servers/transforms/namespace.mdx
+++ b/docs/servers/transforms/namespace.mdx
@@ -3,7 +3,6 @@ title: Namespace Transform
sidebarTitle: Namespace
description: Prefix component names to prevent conflicts
icon: tag
-tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
diff --git a/docs/servers/transforms/tool-transformation.mdx b/docs/servers/transforms/tool-transformation.mdx
index 393b47b5a..a50513f87 100644
--- a/docs/servers/transforms/tool-transformation.mdx
+++ b/docs/servers/transforms/tool-transformation.mdx
@@ -3,7 +3,6 @@ title: Tool Transformation
sidebarTitle: Tool Transformation
description: Modify tool schemas - rename, reshape arguments, and customize behavior
icon: wrench
-tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
diff --git a/docs/servers/transforms/transforms.mdx b/docs/servers/transforms/transforms.mdx
index 4008f86b4..4347b2f18 100644
--- a/docs/servers/transforms/transforms.mdx
+++ b/docs/servers/transforms/transforms.mdx
@@ -3,7 +3,6 @@ title: Transforms Overview
sidebarTitle: Overview
description: Modify components as they flow through your server
icon: wand-magic-sparkles
-tag: NEW
---
import { VersionBadge } from '/snippets/version-badge.mdx'
diff --git a/docs/snippets/prefab-pin-warning.mdx b/docs/snippets/prefab-pin-warning.mdx
new file mode 100644
index 000000000..098181fba
--- /dev/null
+++ b/docs/snippets/prefab-pin-warning.mdx
@@ -0,0 +1,3 @@
+
+[Prefab](https://prefab.prefect.io) is under active development with frequent breaking changes. FastMCP sets a minimum `prefab-ui` version but does not pin an upper bound — **pin `prefab-ui` to a specific version in your own dependencies** before deploying.
+
diff --git a/docs/tutorials/rest-api.mdx b/docs/tutorials/rest-api.mdx
index a0857aca9..90872c950 100644
--- a/docs/tutorials/rest-api.mdx
+++ b/docs/tutorials/rest-api.mdx
@@ -152,7 +152,7 @@ Here’s how you can add custom route maps to turn `GET` requests into `Resource
```python api_server_with_resources.py {3, 37-42}
import httpx
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
# Create an HTTP client for the target API
diff --git a/docs/v2-navigation.json b/docs/v2-navigation.json
new file mode 100644
index 000000000..17865edfd
--- /dev/null
+++ b/docs/v2-navigation.json
@@ -0,0 +1,197 @@
+{
+ "dropdowns": [
+ {
+ "dropdown": "Documentation",
+ "groups": [
+ {
+ "group": "Get Started",
+ "pages": [
+ "v2/getting-started/welcome",
+ "v2/getting-started/installation",
+ "v2/getting-started/quickstart",
+ "v2/updates"
+ ]
+ },
+ {
+ "group": "Servers",
+ "pages": [
+ "v2/servers/server",
+ {
+ "group": "Core Components",
+ "icon": "toolbox",
+ "pages": [
+ "v2/servers/tools",
+ "v2/servers/resources",
+ "v2/servers/prompts"
+ ]
+ },
+ {
+ "group": "Advanced Features",
+ "icon": "stars",
+ "pages": [
+ "v2/servers/composition",
+ "v2/servers/context",
+ "v2/servers/elicitation",
+ "v2/servers/icons",
+ "v2/servers/logging",
+ "v2/servers/middleware",
+ "v2/servers/progress",
+ "v2/servers/proxy",
+ "v2/servers/sampling",
+ "v2/servers/storage-backends",
+ "v2/servers/tasks"
+ ]
+ },
+ {
+ "group": "Authentication",
+ "icon": "shield-check",
+ "pages": [
+ "v2/servers/auth/authentication",
+ "v2/servers/auth/token-verification",
+ "v2/servers/auth/remote-oauth",
+ "v2/servers/auth/oauth-proxy",
+ "v2/servers/auth/oidc-proxy",
+ "v2/servers/auth/full-oauth-server"
+ ]
+ },
+ {
+ "group": "Deployment",
+ "icon": "rocket",
+ "pages": [
+ "v2/deployment/running-server",
+ "v2/deployment/http",
+ "deployment/prefect-horizon",
+ "v2/deployment/server-configuration"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Clients",
+ "pages": [
+ {
+ "group": "Essentials",
+ "icon": "cube",
+ "pages": [
+ "v2/clients/client",
+ "v2/clients/transports"
+ ]
+ },
+ {
+ "group": "Core Operations",
+ "icon": "handshake",
+ "pages": [
+ "v2/clients/tools",
+ "v2/clients/resources",
+ "v2/clients/prompts"
+ ]
+ },
+ {
+ "group": "Advanced Features",
+ "icon": "stars",
+ "pages": [
+ "v2/clients/elicitation",
+ "v2/clients/logging",
+ "v2/clients/progress",
+ "v2/clients/sampling",
+ "v2/clients/tasks",
+ "v2/clients/messages",
+ "v2/clients/roots"
+ ]
+ },
+ {
+ "group": "Authentication",
+ "icon": "user-shield",
+ "pages": [
+ "v2/clients/auth/oauth",
+ "v2/clients/auth/bearer"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Integrations",
+ "pages": [
+ {
+ "group": "Authentication",
+ "icon": "key",
+ "pages": [
+ "v2/integrations/auth0",
+ "v2/integrations/authkit",
+ "v2/integrations/aws-cognito",
+ "v2/integrations/azure",
+ "v2/integrations/descope",
+ "v2/integrations/discord",
+ "v2/integrations/github",
+ "v2/integrations/google",
+ "v2/integrations/oci",
+ "v2/integrations/scalekit",
+ "v2/integrations/supabase",
+ "v2/integrations/workos"
+ ]
+ },
+ {
+ "group": "Authorization",
+ "icon": "shield-check",
+ "pages": [
+ "v2/integrations/eunomia-authorization",
+ "v2/integrations/permit"
+ ]
+ },
+ {
+ "group": "AI Assistants",
+ "icon": "robot",
+ "pages": [
+ "v2/integrations/chatgpt",
+ "v2/integrations/claude-code",
+ "v2/integrations/claude-desktop",
+ "v2/integrations/cursor",
+ "v2/integrations/gemini-cli",
+ "v2/integrations/mcp-json-configuration"
+ ]
+ },
+ {
+ "group": "AI SDKs",
+ "icon": "code",
+ "pages": [
+ "v2/integrations/anthropic",
+ "v2/integrations/gemini",
+ "v2/integrations/openai"
+ ]
+ },
+ {
+ "group": "API Integration",
+ "icon": "globe",
+ "pages": [
+ "v2/integrations/fastapi",
+ "v2/integrations/openapi"
+ ]
+ }
+ ]
+ },
+ {
+ "group": "Patterns",
+ "pages": [
+ "v2/patterns/tool-transformation",
+ "v2/patterns/decorating-methods",
+ "v2/patterns/cli",
+ "v2/patterns/contrib",
+ "v2/patterns/testing"
+ ]
+ },
+ {
+ "group": "Development",
+ "pages": [
+ "v2/development/contributing",
+ "v2/development/tests",
+ "v2/development/releases",
+ "v2/development/upgrade-guide",
+ "v2/changelog"
+ ]
+ }
+ ],
+ "icon": "book"
+ }
+ ],
+ "version": "v2.14.5"
+}
diff --git a/docs/v2/clients/sampling.mdx b/docs/v2/clients/sampling.mdx
index 1bfea5b18..a709b1761 100644
--- a/docs/v2/clients/sampling.mdx
+++ b/docs/v2/clients/sampling.mdx
@@ -254,5 +254,5 @@ Install the Anthropic handler with `pip install fastmcp[anthropic]`.
Tool execution happens on the server side. The client's role is to pass tools to the LLM and return the LLM's response (which may include tool use requests). The server then executes the tools and may send follow-up sampling requests with tool results.
-To implement a custom sampling handler, see the [handler source code](https://github.com/PrefectHQ/fastmcp/tree/main/src/fastmcp/client/sampling/handlers) as a reference.
+To implement a custom sampling handler, see the [handler source code](https://github.com/PrefectHQ/fastmcp/tree/main/fastmcp_slim/fastmcp/client/sampling/handlers) as a reference.
\ No newline at end of file
diff --git a/docs/v2/deployment/http.mdx b/docs/v2/deployment/http.mdx
index eafa773b3..fa91c65e0 100644
--- a/docs/v2/deployment/http.mdx
+++ b/docs/v2/deployment/http.mdx
@@ -650,17 +650,17 @@ FASTMCP_STATELESS_HTTP=true uvicorn app:app --host 0.0.0.0 --port 8000 --workers
Production deployments should never hardcode sensitive information like API keys or authentication tokens. Instead, use environment variables to configure your server at runtime. This keeps your code secure and makes it easy to deploy the same code to different environments with different configurations.
-Here's an example using bearer token authentication (though OAuth is recommended for production):
+Here's an example using static token authentication for development (OAuth is recommended for production):
```python
import os
from fastmcp import FastMCP
-from fastmcp.server.auth import BearerTokenAuth
+from fastmcp.server.auth import StaticTokenVerifier
# Read configuration from environment
auth_token = os.environ.get("MCP_AUTH_TOKEN")
if auth_token:
- auth = BearerTokenAuth(token=auth_token)
+ auth = StaticTokenVerifier(tokens={auth_token: {"sub": "admin", "client_id": "cli"}})
mcp = FastMCP("Production Server", auth=auth)
else:
mcp = FastMCP("Production Server")
diff --git a/docs/v2/development/tests.mdx b/docs/v2/development/tests.mdx
index 6a9973fe8..4653368be 100644
--- a/docs/v2/development/tests.mdx
+++ b/docs/v2/development/tests.mdx
@@ -33,7 +33,7 @@ Tests should complete in under 1 second unless marked as integration tests. This
### Test Organization
-Our test organization mirrors the `src/` directory structure, creating a predictable mapping between code and tests. When you're working on `src/fastmcp/server/auth.py`, you'll find its tests in `tests/server/test_auth.py`. In rare cases tests are split further - for example, the OpenAPI tests are so comprehensive they're split across multiple files.
+Our test organization mirrors the source package structure, creating a predictable mapping between code and tests. When you're working on `fastmcp_slim/fastmcp/server/auth.py`, you'll find its tests in `tests/server/test_auth.py`. In rare cases tests are split further - for example, the OpenAPI tests are so comprehensive they're split across multiple files.
### Test Markers
@@ -393,4 +393,4 @@ just docs
mintlify dev
```
-The local server watches for changes and automatically refreshes. This preview catches formatting issues and helps you see documentation as users will experience it.
\ No newline at end of file
+The local server watches for changes and automatically refreshes. This preview catches formatting issues and helps you see documentation as users will experience it.
diff --git a/docs/v2/development/upgrade-guide.mdx b/docs/v2/development/upgrade-guide.mdx
index 2e98b4a02..93b314829 100644
--- a/docs/v2/development/upgrade-guide.mdx
+++ b/docs/v2/development/upgrade-guide.mdx
@@ -19,11 +19,11 @@ The experimental OpenAPI parser is now the standard implementation. The legacy p
**If you were using the experimental parser:** Update your imports from the experimental module to the standard location:
-```python Before
+```python test="skip" Before
from fastmcp.experimental.server.openapi import FastMCPOpenAPI, RouteMap, MCPType
```
-```python After
+```python test="skip" After
from fastmcp.server.openapi import FastMCPOpenAPI, RouteMap, MCPType
```
@@ -36,7 +36,7 @@ The following deprecated features have been removed in v2.14.0:
**BearerAuthProvider** (deprecated in v2.11):
-```python Before
+```python test="skip" Before
from fastmcp.server.auth.providers.bearer import BearerAuthProvider
```
@@ -47,7 +47,7 @@ from fastmcp.server.auth.providers.jwt import JWTVerifier
**Context.get_http_request()** (deprecated in v2.2.11):
-```python Before
+```python test="skip" Before
request = context.get_http_request()
```
@@ -59,7 +59,7 @@ request = get_http_request()
**Top-level Image import** (deprecated in v2.8.1):
-```python Before
+```python test="skip" Before
from fastmcp import Image
```
diff --git a/docs/v2/getting-started/quickstart.mdx b/docs/v2/getting-started/quickstart.mdx
index 678d8cd5e..117efcd0a 100644
--- a/docs/v2/getting-started/quickstart.mdx
+++ b/docs/v2/getting-started/quickstart.mdx
@@ -119,7 +119,7 @@ Note that:
## Deploy to Prefect Horizon
-[Prefect Horizon](https://horizon.prefect.io) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides managed hosting, authentication, access control, and observability for MCP servers.
+[Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) is the enterprise MCP platform built by the FastMCP team at [Prefect](https://www.prefect.io). It provides managed hosting, authentication, access control, and observability for MCP servers.
Horizon is **free for personal projects** and offers enterprise governance for teams.
@@ -128,7 +128,7 @@ Horizon is **free for personal projects** and offers enterprise governance for t
To deploy your server, you'll need a [GitHub account](https://github.com). Once you have one, you can deploy your server in three steps:
1. Push your `my_server.py` file to a GitHub repository
-2. Sign in to [Prefect Horizon](https://horizon.prefect.io) with your GitHub account
+2. Sign in to [Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) with your GitHub account
3. Create a new project from your repository and enter `my_server.py:mcp` as the server entrypoint
That's it! Horizon will build and deploy your server, making it available at a URL like `https://your-project.fastmcp.app/mcp`. You can chat with it to test its functionality, or connect to it from any LLM client that supports the MCP protocol.
diff --git a/docs/v2/getting-started/welcome.mdx b/docs/v2/getting-started/welcome.mdx
index c00788f72..b8213d484 100644
--- a/docs/v2/getting-started/welcome.mdx
+++ b/docs/v2/getting-started/welcome.mdx
@@ -73,7 +73,7 @@ FastMCP handles all the complex protocol details so you can focus on building. I
🔍 **Complete**: Everything for production — enterprise auth (Google, GitHub, Azure, Auth0, WorkOS), deployment tools, testing frameworks, client libraries, and more
-FastMCP provides the shortest path from idea to production. Deploy locally, to the cloud with [Prefect Horizon](https://horizon.prefect.io) (free for personal projects), or to your own infrastructure.
+FastMCP provides the shortest path from idea to production. Deploy locally, to the cloud with [Prefect Horizon](https://horizon.prefect.io?utm_source=gofastmcp&utm_medium=docs) (free for personal projects), or to your own infrastructure.
**This documentation reflects FastMCP's `main` branch**, meaning it always reflects the latest development version. Features are generally marked with version badges (e.g. `New in version: 2.13.1`) to indicate when they were introduced. Note that this may include features that are not yet released.
@@ -96,7 +96,7 @@ from fastmcp import Client
async def main():
async with Client("https://gofastmcp.com/mcp") as client:
result = await client.call_tool(
- name="SearchFastMcp",
+ name="search_fast_mcp",
arguments={"query": "deploy a FastMCP server"}
)
print(result)
diff --git a/docs/v2/integrations/anthropic.mdx b/docs/v2/integrations/anthropic.mdx
index 7490bcf35..7d2d38dc1 100644
--- a/docs/v2/integrations/anthropic.mdx
+++ b/docs/v2/integrations/anthropic.mdx
@@ -181,7 +181,7 @@ if __name__ == "__main__":
If you try to call the authenticated server with the same Anthropic code we wrote earlier, you'll get an error indicating that the server rejected the request because it's not authenticated.
-```python
+```text
Error code: 400 - {
"type": "error",
"error": {
diff --git a/docs/v2/integrations/chatgpt.mdx b/docs/v2/integrations/chatgpt.mdx
index 18028c69d..d40855551 100644
--- a/docs/v2/integrations/chatgpt.mdx
+++ b/docs/v2/integrations/chatgpt.mdx
@@ -93,10 +93,12 @@ The connector must be explicitly enabled in each chat session through Developer
### Skip Confirmations
-Use `annotations={"readOnlyHint": True}` to skip confirmation prompts for read-only tools:
+Use `annotations=ToolAnnotations(readOnlyHint=True)` to skip confirmation prompts for read-only tools:
```python
-@mcp.tool(annotations={"readOnlyHint": True})
+from mcp.types import ToolAnnotations
+
+@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
def get_status() -> str:
"""Check system status."""
return "All systems operational"
@@ -154,4 +156,3 @@ def fetch(id: str) -> dict:
5. Ask research questions
ChatGPT will use your `search` and `fetch` tools to find and cite relevant information.
-
diff --git a/docs/v2/integrations/descope.mdx b/docs/v2/integrations/descope.mdx
index abba9069d..14bade5f4 100644
--- a/docs/v2/integrations/descope.mdx
+++ b/docs/v2/integrations/descope.mdx
@@ -64,8 +64,8 @@ from fastmcp.server.auth.providers.descope import DescopeProvider
# The DescopeProvider automatically discovers Descope endpoints
# and configures JWT token validation
auth_provider = DescopeProvider(
- config_url=https://.../.well-known/openid-configuration, # Your MCP Server .well-known URL
- base_url=SERVER_URL, # Your server's public URL
+ config_url="https://.../.well-known/openid-configuration", # Your MCP Server .well-known URL
+ base_url=SERVER_URL, # Your server's public URL
)
# Create FastMCP server with auth
diff --git a/docs/v2/integrations/fastapi.mdx b/docs/v2/integrations/fastapi.mdx
index 3737345f1..67d5d06de 100644
--- a/docs/v2/integrations/fastapi.mdx
+++ b/docs/v2/integrations/fastapi.mdx
@@ -216,7 +216,7 @@ Because FastMCP's FastAPI integration is based on its [OpenAPI integration](/v2/
```python
# Assumes the FastAPI app from above is already defined
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
# Custom mapping rules
mcp = FastMCP.from_fastapi(
diff --git a/docs/v2/integrations/mcp-json-configuration.mdx b/docs/v2/integrations/mcp-json-configuration.mdx
index a9b758fb7..b85bf9e4f 100644
--- a/docs/v2/integrations/mcp-json-configuration.mdx
+++ b/docs/v2/integrations/mcp-json-configuration.mdx
@@ -357,6 +357,98 @@ echo "$CONFIG" | jq '."CI Server".command'
# Output: "uv"
```
+### UV-Managed Project Dependencies
+
+For servers that live inside a uv-managed project (with `pyproject.toml`), use the `--project` flag to run within that project's environment:
+
+```bash
+fastmcp install mcp-json server.py --project .
+```
+
+Output:
+```json
+{
+ "My Server": {
+ "command": "uv",
+ "args": [
+ "run",
+ "--project",
+ "/absolute/path/to/project",
+ "--with",
+ "fastmcp",
+ "fastmcp",
+ "run",
+ "/absolute/path/to/project/server.py"
+ ]
+ }
+}
+```
+
+You can also use `fastmcp.json` with a local project:
+
+```json fastmcp.json
+{
+ "$schema": "https://gofastmcp.com/public/schemas/fastmcp.json/v1.json",
+ "source": {
+ "path": "server.py"
+ },
+ "environment": {
+ "project": "."
+ }
+}
+```
+
+If your server needs additional packages beyond those in `pyproject.toml`, add them via the `dependencies` array or `--with`.
+
+### Published Packages with `uvx`
+
+If your team publishes MCP servers as pip packages, you can configure clients to run them with `uvx` directly instead of `uv run`. For example, if your package is called `my-mcp-server` and provides a CLI entry point of the same name:
+
+```json
+{
+ "mcpServers": {
+ "My Server": {
+ "command": "uvx",
+ "args": ["my-mcp-server"]
+ }
+ }
+}
+```
+
+If the package name differs from the CLI command (e.g., package `weather-mcp` with command `weather-server`):
+
+```json
+{
+ "mcpServers": {
+ "Weather": {
+ "command": "uvx",
+ "args": ["--from", "weather-mcp", "weather-server"]
+ }
+ }
+}
+```
+
+You can also pin Python versions or add extra dependencies:
+
+```json
+{
+ "mcpServers": {
+ "My Server": {
+ "command": "uvx",
+ "args": [
+ "--python", "3.12",
+ "--with", "requests",
+ "my-mcp-server"
+ ]
+ }
+ }
+}
+```
+
+
+`fastmcp install mcp-json` generates `uv run` configurations for local development. For published packages, you'll typically write the `uvx` configuration manually or generate it through your own packaging workflow.
+
+
## Integration with MCP Clients
The generated configuration works with any MCP-compatible application:
diff --git a/docs/v2/integrations/openai.mdx b/docs/v2/integrations/openai.mdx
index 63b5e28b4..af41528f9 100644
--- a/docs/v2/integrations/openai.mdx
+++ b/docs/v2/integrations/openai.mdx
@@ -178,8 +178,8 @@ if __name__ == "__main__":
If you try to call the authenticated server with the same OpenAI code we wrote earlier, you'll get an error like this:
-```python
-pythonAPIStatusError: Error code: 424 - {
+```text
+APIStatusError: Error code: 424 - {
"error": {
"message": "Error retrieving tool list from MCP server: 'dice_server'. Http status code: 401 (Unauthorized)",
"type": "external_connector_error",
diff --git a/docs/v2/integrations/openapi.mdx b/docs/v2/integrations/openapi.mdx
index 8662a0c05..0fd38cbd0 100644
--- a/docs/v2/integrations/openapi.mdx
+++ b/docs/v2/integrations/openapi.mdx
@@ -81,7 +81,7 @@ Each `RouteMap` specifies a combination of methods, patterns, and tags, as well
Here is FastMCP's default rule:
```python
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
DEFAULT_ROUTE_MAPPINGS = [
# All routes become tools
@@ -97,7 +97,7 @@ For example, prior to FastMCP 2.8.0, GET requests were automatically mapped to `
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
# Restore pre-2.8.0 semantic mapping
semantic_maps = [
@@ -120,7 +120,7 @@ Here is a more complete example that uses custom route maps to convert all `GET`
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -160,7 +160,7 @@ You can use this to remove sensitive or internal routes by targeting them specif
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -176,7 +176,7 @@ Or you can use a catch-all rule to exclude everything that your maps don't handl
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -208,7 +208,8 @@ The `route_map_fn` is called on all routes, even those that matched `MCPType.EXC
```python
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType, HTTPRoute
+from fastmcp.server.providers.openapi import RouteMap, MCPType
+from fastmcp.utilities.openapi import HTTPRoute
def custom_route_mapper(route: HTTPRoute, mcp_type: MCPType) -> MCPType | None:
"""Advanced route type mapping."""
@@ -273,7 +274,7 @@ FastMCP provides several ways to add tags to your MCP components, allowing you t
You can add custom tags to components created from specific routes using the `mcp_tags` parameter in `RouteMap`. These tags will be applied to all components created from routes that match that particular route map.
```python
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
mcp = FastMCP.from_openapi(
openapi_spec=spec,
@@ -364,12 +365,12 @@ Your `mcp_component_fn` is expected to modify the component in-place, not to ret
```python
-from fastmcp.server.openapi import (
- HTTPRoute,
+from fastmcp.server.providers.openapi import (
OpenAPITool,
OpenAPIResource,
OpenAPIResourceTemplate,
)
+from fastmcp.utilities.openapi import HTTPRoute
def customize_components(
route: HTTPRoute,
diff --git a/docs/v2/patterns/contrib.mdx b/docs/v2/patterns/contrib.mdx
index d2f812f52..04ef45aff 100644
--- a/docs/v2/patterns/contrib.mdx
+++ b/docs/v2/patterns/contrib.mdx
@@ -12,13 +12,13 @@ FastMCP includes a `contrib` package that holds community-contributed modules. T
Contrib modules provide additional features, integrations, or patterns that complement the core FastMCP library. They offer a way for the community to share useful extensions while keeping the core library focused and maintainable.
-The available modules can be viewed in the [contrib directory](https://github.com/PrefectHQ/fastmcp/tree/main/src/fastmcp/contrib).
+The available modules can be viewed in the [contrib directory](https://github.com/PrefectHQ/fastmcp/tree/main/fastmcp_slim/fastmcp/contrib).
## Usage
To use a contrib module, import it from the `fastmcp.contrib` package:
-```python
+```python test="skip"
from fastmcp.contrib import my_module
```
@@ -32,7 +32,7 @@ from fastmcp.contrib import my_module
We welcome contributions to the `contrib` package! If you have a module that extends FastMCP in a useful way, consider contributing it:
-1. Create a new directory in `src/fastmcp/contrib/` for your module
+1. Create a new directory in `fastmcp_slim/fastmcp/contrib/` for your module
3. Add proper tests for your module in `tests/contrib/`
2. Include comprehensive documentation in a README.md file, including usage and examples, as well as any additional dependencies or installation instructions
5. Submit a pull request
diff --git a/docs/v2/servers/auth/authentication.mdx b/docs/v2/servers/auth/authentication.mdx
index c42bfa8bf..c6b829bfe 100644
--- a/docs/v2/servers/auth/authentication.mdx
+++ b/docs/v2/servers/auth/authentication.mdx
@@ -161,7 +161,7 @@ The implementation provides all required OAuth endpoints including authorization
```python
from fastmcp import FastMCP
-from fastmcp.server.auth.providers.oauth import MyOAuthProvider
+from fastmcp.server.auth import OAuthProvider
auth = MyOAuthProvider(
user_store=your_user_database,
diff --git a/docs/v2/servers/auth/oauth-proxy.mdx b/docs/v2/servers/auth/oauth-proxy.mdx
index b9bc03430..eef3bce1c 100644
--- a/docs/v2/servers/auth/oauth-proxy.mdx
+++ b/docs/v2/servers/auth/oauth-proxy.mdx
@@ -115,6 +115,12 @@ mcp = FastMCP(name="My Server", auth=auth)
This URL is used to construct OAuth callback URLs and operational endpoints. When mounting under a path prefix, include that prefix in `base_url`. Use `issuer_url` separately to specify where auth server metadata is located (typically at root level).
+
+ Optional public base URL for the protected resource metadata and token audience.
+
+ Use this when your OAuth callbacks and operational endpoints need to live under one public URL, but the protected MCP resource should be advertised under another. FastMCP will still append the MCP mount path (for example, `/mcp`) to this base URL.
+
+
Path for OAuth callbacks. Must match the redirect URI configured in your OAuth
application
@@ -281,14 +287,20 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
-
- Whether to require user consent before authorizing MCP clients. When enabled (default), users see a consent screen that displays which client is requesting access, preventing [confused deputy attacks](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#confused-deputy-problem) by ensuring users explicitly approve new clients.
+
+ Consent screen behavior for authorization requests. The consent page displays which client is requesting access, defending against [confused deputy and AS-in-the-middle attacks](#confused-deputy-attacks) by requiring explicit user approval.
- **Default behavior (True):**
- Users see a consent screen on first authorization. Consent choices are remembered via signed cookies, so users only need to approve each client once. This protects against malicious clients impersonating the user.
+ **`True` (default) — always prompt:**
+ Users see the consent screen on every authorization. Strongest protection against AS-in-the-middle attacks where a malicious MCP server redirects the victim's browser into a legitimate proxy and relies on a previously-remembered approval to silently complete the flow.
- **Disabling consent (False):**
- Authorization proceeds directly to the upstream provider without user confirmation. Only use this for local development or testing environments where the security trade-off is acceptable.
+ **`"remember"` — silent consent on return:**
+ Users see the consent screen on first authorization; subsequent flows from the same browser for the same `(client_id, redirect_uri)` are silently approved via a signed cookie. Cross-site navigations (detected via `Sec-Fetch-Site`) fall back to the prompt. `Sec-Fetch-Site` is a browser-level heuristic rather than a protocol guarantee: an attacker who finds a way to initiate a non-cross-site navigation (XSS on a sibling origin, a same-site redirect chain, etc.) can reach the silent-consent path. `True` does not depend on this signal. See [Confused Deputy Attacks](#confused-deputy-attacks) for the underlying attack class.
+
+ **`"external"` — delegate to upstream:**
+ Skip the built-in consent page; consent is collected by the upstream IdP or a custom login page referenced via `upstream_authorization_endpoint`. No security warning is logged.
+
+ **`False` — disable entirely:**
+ Authorization proceeds directly to the upstream provider without any consent UI. Logs a security warning. Only for local development or testing.
```python
# Development/testing only - skip consent screen
@@ -296,6 +308,12 @@ auth = OAuthProxy(..., client_storage=MemoryStore())
...,
require_authorization_consent=False # ⚠️ Security warning: only for local/testing
)
+
+ # Convenience mode - silent consent on return visits (less safe than True)
+ auth = OAuthProxy(
+ ...,
+ require_authorization_consent="remember",
+ )
```
@@ -548,13 +566,17 @@ The OAuth proxy works by bridging DCR clients to traditional auth providers, whi
#### Mitigation
-FastMCP's OAuth proxy requires you to explicitly consent whenever any new or unrecognized client attempts to connect to your server. Before any authorization happens, you see a consent page showing the client's details, redirect URI, and requested scopes. This gives you the opportunity to review and deny suspicious requests. Once you approve a client, it's remembered so you don't see the consent page again for that client. The consent mechanism is implemented with CSRF tokens and cryptographically signed cookies to prevent tampering.
+FastMCP's OAuth proxy requires you to explicitly consent whenever a client attempts to connect to your server. Before any authorization happens, you see a consent page showing the client's details, redirect URI, and requested scopes. This gives you the opportunity to review and deny suspicious requests. By default (`require_authorization_consent=True`), the page is shown on every flow, which is the strongest protection. Setting `require_authorization_consent="remember"` approves previously-approved `(client_id, redirect_uri)` pairs silently on return visits, trading some protection for UX (see below). The consent mechanism is implemented with CSRF tokens and cryptographically signed cookies to prevent tampering.

The consent page automatically displays your server's name, icon, and website URL, if available. These visual identifiers help users confirm they're authorizing the correct server.
+#### AS-in-the-middle variant
+A related attack works by positioning a malicious authorization server between an MCP client and a legitimate proxy: a malicious MCP server advertises its own authorization server, which redirects the victim's browser into the legitimate proxy's `/authorize` endpoint. Because the victim's browser carries the prior-approval cookie throughout, a `"remember"`-mode proxy would silently complete the flow. The defense is the consent prompt itself: if consent is shown (`require_authorization_consent=True`), the victim sees the benign MCP server's name on the consent page — which doesn't match the malicious server they thought they were connecting to — and can deny.
+
+`require_authorization_consent="remember"` adds a `Sec-Fetch-Site` check to keep this path safe for legitimate return flows (the attack navigation lands as `cross-site` and falls back to the prompt), but this is a browser-level heuristic. For the strongest defense, leave `require_authorization_consent=True`.
**Learn more:**
- [MCP Security Best Practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices#confused-deputy-problem) - Official specification guidance
diff --git a/docs/v2/servers/auth/oidc-proxy.mdx b/docs/v2/servers/auth/oidc-proxy.mdx
index 0b3a21d71..750298298 100644
--- a/docs/v2/servers/auth/oidc-proxy.mdx
+++ b/docs/v2/servers/auth/oidc-proxy.mdx
@@ -79,6 +79,12 @@ mcp = FastMCP(name="My Server", auth=auth)
Public URL of your FastMCP server (e.g., `https://your-server.com`)
+
+ Optional public base URL for the protected resource metadata and token audience.
+
+ Use this when your OAuth callbacks and operational endpoints need to live under one public URL, but the protected MCP resource should be advertised under another. FastMCP will still append the MCP mount path (for example, `/mcp`) to this base URL.
+
+
Strict flag for configuration validation. When True, requires all OIDC
mandatory fields.
@@ -192,8 +198,8 @@ auth = OIDCProxy(
-
- Whether to require user consent before authorizing MCP clients. When enabled (default), users see a consent screen that displays which client is requesting access. See [OAuthProxy documentation](/v2/servers/auth/oauth-proxy#confused-deputy-attacks) for details on confused deputy attack protection.
+
+ Consent screen behavior for authorization requests. Accepts `True` (default; always prompt — strongest protection), `"remember"` (silent consent on return visits via signed cookie, gated by `Sec-Fetch-Site` to block AS-in-the-middle attacks), `"external"` (consent handled by upstream IdP or custom page), or `False` (disable entirely; local/testing only). See the [OAuthProxy documentation](/v2/servers/auth/oauth-proxy) for full details on each mode and the security trade-offs.
diff --git a/docs/v2/servers/proxy.mdx b/docs/v2/servers/proxy.mdx
index c60b628f7..b5ceb4b97 100644
--- a/docs/v2/servers/proxy.mdx
+++ b/docs/v2/servers/proxy.mdx
@@ -52,7 +52,7 @@ The recommended way to create a proxy is using `ProxyClient`, which provides ful
```python
from fastmcp import FastMCP
-from fastmcp.server.proxy import ProxyClient
+from fastmcp.server.providers.proxy import ProxyClient
# Create a proxy with full MCP feature support
proxy = FastMCP.as_proxy(
@@ -86,7 +86,7 @@ FastMCP proxies provide session isolation to ensure safe concurrent operations.
When you pass a disconnected client (which is the normal case), each request gets its own isolated backend session:
```python
-from fastmcp.server.proxy import ProxyClient
+from fastmcp.server.providers.proxy import ProxyClient
# Each request creates a fresh backend session (recommended)
proxy = FastMCP.as_proxy(ProxyClient("backend_server.py"))
@@ -121,7 +121,7 @@ A common use case is bridging transports - exposing a server running on one tran
```python
from fastmcp import FastMCP
-from fastmcp.server.proxy import ProxyClient
+from fastmcp.server.providers.proxy import ProxyClient
# Bridge remote SSE server to local stdio
remote_proxy = FastMCP.as_proxy(
@@ -164,7 +164,7 @@ if __name__ == "__main__":
- **Progress**: Forwards progress notifications during long operations
```python
-from fastmcp.server.proxy import ProxyClient
+from fastmcp.server.providers.proxy import ProxyClient
# ProxyClient automatically handles all these features
backend = ProxyClient("advanced_backend.py")
@@ -304,7 +304,7 @@ Internally, `FastMCP.as_proxy()` uses the `FastMCPProxy` class. You generally do
### Direct Usage
```python
-from fastmcp.server.proxy import FastMCPProxy, ProxyClient
+from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient
# Provide a client factory for explicit session control
def create_client():
diff --git a/docs/v2/servers/storage-backends.mdx b/docs/v2/servers/storage-backends.mdx
index cd14ab80e..25b8580b0 100644
--- a/docs/v2/servers/storage-backends.mdx
+++ b/docs/v2/servers/storage-backends.mdx
@@ -236,13 +236,13 @@ middleware = ResponseCachingMiddleware(cache_storage=namespaced_store)
The [FastMCP Client](/v2/clients/client) uses storage for persisting OAuth tokens locally. By default, tokens are stored in memory:
```python
-from fastmcp.client.auth import OAuthClientProvider
+from fastmcp.client.auth import OAuth
from key_value.aio.stores.disk import DiskStore
# Store tokens on disk for persistence across restarts
token_storage = DiskStore(directory="~/.local/share/fastmcp/tokens")
-oauth_provider = OAuthClientProvider(
+oauth_provider = OAuth(
mcp_url="https://your-mcp-server.com/mcp/sse",
token_storage=token_storage
)
diff --git a/docs/v2/servers/tools.mdx b/docs/v2/servers/tools.mdx
index 1ec8d62be..3cdcf3be5 100644
--- a/docs/v2/servers/tools.mdx
+++ b/docs/v2/servers/tools.mdx
@@ -792,15 +792,17 @@ Annotations serve several purposes in client applications:
- Describing the safety profile of tools (destructive vs. non-destructive)
- Signaling if tools interact with external systems
-You can add annotations to a tool using the `annotations` parameter in the `@mcp.tool` decorator:
+You can add annotations to a tool using the `annotations` parameter in the `@mcp.tool` decorator. FastMCP accepts either a plain dict or `ToolAnnotations`; the examples below use `ToolAnnotations` for consistency and stronger editor/type support.
```python
+from mcp.types import ToolAnnotations
+
@mcp.tool(
- annotations={
- "title": "Calculate Sum",
- "readOnlyHint": True,
- "openWorldHint": False
- }
+ annotations=ToolAnnotations(
+ title="Calculate Sum",
+ readOnlyHint=True,
+ openWorldHint=False,
+ )
)
def calculate_sum(a: float, b: float) -> float:
"""Add two numbers together."""
@@ -836,7 +838,7 @@ from mcp.types import ToolAnnotations
mcp = FastMCP("Data Server")
-@mcp.tool(annotations={"readOnlyHint": True})
+@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
def get_user(user_id: str) -> dict:
"""Retrieve user information by ID."""
return {"id": user_id, "name": "Alice"}
@@ -858,7 +860,7 @@ def update_user(user_id: str, name: str) -> dict:
"""Update user information."""
return {"id": user_id, "name": name, "updated": True}
-@mcp.tool(annotations={"destructiveHint": True})
+@mcp.tool(annotations=ToolAnnotations(destructiveHint=True))
def delete_user(user_id: str) -> dict:
"""Permanently delete a user account."""
return {"deleted": user_id}
diff --git a/docs/v2/tutorials/rest-api.mdx b/docs/v2/tutorials/rest-api.mdx
index 362eb0026..6524a2335 100644
--- a/docs/v2/tutorials/rest-api.mdx
+++ b/docs/v2/tutorials/rest-api.mdx
@@ -152,7 +152,7 @@ Here’s how you can add custom route maps to turn `GET` requests into `Resource
```python api_server_with_resources.py {3, 37-42}
import httpx
from fastmcp import FastMCP
-from fastmcp.server.openapi import RouteMap, MCPType
+from fastmcp.server.providers.openapi import RouteMap, MCPType
# Create an HTTP client for the target API
diff --git a/examples/auth/authkit/README.md b/examples/auth/authkit/README.md
new file mode 100644
index 000000000..8c4b8a6aa
--- /dev/null
+++ b/examples/auth/authkit/README.md
@@ -0,0 +1,36 @@
+# AuthKit Example
+
+Protects a FastMCP server with WorkOS AuthKit. The server binds the JWT
+`aud` claim to its own resource URL automatically — you just paste that same
+URL into the WorkOS Dashboard as a resource indicator.
+
+## WorkOS Dashboard setup
+
+In the WorkOS Dashboard for your project, go to **Connect → Configuration** and:
+
+1. Under **MCP Auth**, enable **Dynamic Client Registration** (or **Client ID
+ Metadata Document** if your MCP client supports it).
+2. Under **MCP resource indicators**, add `http://127.0.0.1:8000/mcp` as a
+ valid resource indicator.
+
+## Running
+
+1. Set your AuthKit domain:
+
+ ```bash
+ export AUTHKIT_DOMAIN="https://your-app.authkit.app"
+ ```
+
+2. Start the server. It logs the resource URL it's validating against —
+ that's the URL that must match your dashboard resource indicator:
+
+ ```bash
+ python server.py
+ ```
+
+3. In another terminal, run the client. Your browser will open for AuthKit
+ authentication:
+
+ ```bash
+ python client.py
+ ```
diff --git a/examples/auth/authkit/client.py b/examples/auth/authkit/client.py
new file mode 100644
index 000000000..562637bec
--- /dev/null
+++ b/examples/auth/authkit/client.py
@@ -0,0 +1,33 @@
+"""OAuth client example for connecting to FastMCP servers.
+
+This example demonstrates how to connect to an OAuth-protected FastMCP server.
+
+To run:
+ python client.py
+"""
+
+import asyncio
+
+from fastmcp.client import Client
+from fastmcp.client.auth import OAuth
+
+SERVER_URL = "http://127.0.0.1:8000/mcp"
+
+
+async def main():
+ # AuthKit defaults DCR clients to client_secret_basic, which conflicts
+ # with how MCP SDKs send credentials. Force "none" to register as a
+ # public client and avoid token exchange errors.
+ auth = OAuth(additional_client_metadata={"token_endpoint_auth_method": "none"})
+ async with Client(SERVER_URL, auth=auth) 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}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/examples/auth/authkit/server.py b/examples/auth/authkit/server.py
new file mode 100644
index 000000000..7611ccddf
--- /dev/null
+++ b/examples/auth/authkit/server.py
@@ -0,0 +1,34 @@
+"""AuthKit server example for FastMCP.
+
+Demonstrates an MCP server secured by WorkOS AuthKit. FastMCP binds the JWT
+audience to this server's resource URL automatically; you configure the same
+URL as an MCP resource indicator in the WorkOS Dashboard.
+
+Required environment variables:
+- AUTHKIT_DOMAIN: Your AuthKit domain (e.g., "https://your-app.authkit.app")
+
+To run:
+ python server.py
+"""
+
+import os
+
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.workos import AuthKitProvider
+
+auth = AuthKitProvider(
+ authkit_domain=os.getenv("AUTHKIT_DOMAIN") or "",
+ base_url="http://127.0.0.1:8000",
+)
+
+mcp = FastMCP("AuthKit Example Server", auth=auth)
+
+
+@mcp.tool
+def echo(message: str) -> str:
+ """Echo the provided message."""
+ return message
+
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
diff --git a/examples/auth/aws_oauth/README.md b/examples/auth/aws_oauth/README.md
index 9abff838c..c4e25b1f8 100644
--- a/examples/auth/aws_oauth/README.md
+++ b/examples/auth/aws_oauth/README.md
@@ -10,7 +10,7 @@ Demonstrates FastMCP server protection with AWS Cognito OAuth.
- Create an App Client in your User Pool
- Configure the App Client settings:
- Enable "Authorization code grant" flow
- - Add Callback URL: `http://localhost:8000/auth/callback`
+ - Add Callback URL: `http://127.0.0.1:8000/auth/callback`
- Configure OAuth scopes (at minimum: `openid`)
- Note your User Pool ID, App Client ID, Client Secret, and Cognito Domain Prefix
diff --git a/examples/auth/aws_oauth/client.py b/examples/auth/aws_oauth/client.py
index 4043e6d4f..afcf54fd1 100644
--- a/examples/auth/aws_oauth/client.py
+++ b/examples/auth/aws_oauth/client.py
@@ -10,7 +10,7 @@ import asyncio
from fastmcp.client import Client
-SERVER_URL = "http://localhost:8000/mcp"
+SERVER_URL = "http://127.0.0.1:8000/mcp"
async def main():
diff --git a/examples/auth/aws_oauth/requirements.txt b/examples/auth/aws_oauth/requirements.txt
index 9c7f15cd1..044c95a70 100644
--- a/examples/auth/aws_oauth/requirements.txt
+++ b/examples/auth/aws_oauth/requirements.txt
@@ -1,2 +1,2 @@
fastmcp
-python-dotenv
\ No newline at end of file
+python-dotenv
diff --git a/examples/auth/aws_oauth/server.py b/examples/auth/aws_oauth/server.py
index dfe596a83..261164391 100644
--- a/examples/auth/aws_oauth/server.py
+++ b/examples/auth/aws_oauth/server.py
@@ -31,7 +31,7 @@ auth = AWSCognitoProvider(
or "eu-central-1",
client_id=os.getenv("FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_ID") or "",
client_secret=os.getenv("FASTMCP_SERVER_AUTH_AWS_COGNITO_CLIENT_SECRET") or "",
- base_url="http://localhost:8000",
+ base_url="http://127.0.0.1:8000",
# redirect_path="/custom/callback"
)
diff --git a/examples/auth/azure_oauth/README.md b/examples/auth/azure_oauth/README.md
index ba0757ca7..98d9ae756 100644
--- a/examples/auth/azure_oauth/README.md
+++ b/examples/auth/azure_oauth/README.md
@@ -10,7 +10,7 @@ This example demonstrates how to use the Azure OAuth provider with FastMCP serve
2. Click "New registration" and configure:
- Name: Your app name
- Supported account types: Choose based on your needs
- - Redirect URI: `http://localhost:8000/auth/callback` (Web platform)
+ - Redirect URI: `http://127.0.0.1:8000/auth/callback` (Web platform)
3. After creation, go to "Certificates & secrets" → "New client secret"
4. Note these values from the Overview page:
- Application (client) ID
diff --git a/examples/auth/azure_oauth/server.py b/examples/auth/azure_oauth/server.py
index d214389aa..e0c9e799e 100644
--- a/examples/auth/azure_oauth/server.py
+++ b/examples/auth/azure_oauth/server.py
@@ -24,7 +24,7 @@ auth = AzureProvider(
client_secret=os.getenv("FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET") or "",
tenant_id=os.getenv("FASTMCP_SERVER_AUTH_AZURE_TENANT_ID")
or "", # Required for single-tenant apps - get from Azure Portal
- base_url="http://localhost:8000",
+ base_url="http://127.0.0.1:8000",
required_scopes=["read"],
# required_scopes is automatically loaded from FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES
# At least one scope is required - use unprefixed scope names from your Azure App (e.g., ["read", "write"])
diff --git a/examples/auth/clerk_oauth/README.md b/examples/auth/clerk_oauth/README.md
index 84d2b44b1..9a79ff566 100644
--- a/examples/auth/clerk_oauth/README.md
+++ b/examples/auth/clerk_oauth/README.md
@@ -9,7 +9,7 @@ Demonstrates FastMCP server protection with Clerk OAuth.
- Create or select an application
- Go to Developers > OAuth Applications
- Create an OAuth application
- - Add Authorized redirect URI: `http://localhost:8000/auth/callback`
+ - Add Authorized redirect URI: `http://127.0.0.1:8000/auth/callback`
- Copy the Client ID and Client Secret
- Note your instance domain (e.g., `saving-primate-16.clerk.accounts.dev`)
diff --git a/examples/auth/clerk_oauth/server.py b/examples/auth/clerk_oauth/server.py
index 74b1e4687..e7d080734 100644
--- a/examples/auth/clerk_oauth/server.py
+++ b/examples/auth/clerk_oauth/server.py
@@ -21,7 +21,7 @@ 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",
+ base_url="http://127.0.0.1:8000",
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
# Optional: specify required scopes (defaults to ["openid", "email", "profile"])
# required_scopes=["openid", "email", "profile", "public_metadata"],
diff --git a/examples/auth/discord_oauth/README.md b/examples/auth/discord_oauth/README.md
index 74217f833..e757ad84b 100644
--- a/examples/auth/discord_oauth/README.md
+++ b/examples/auth/discord_oauth/README.md
@@ -8,7 +8,7 @@ Demonstrates FastMCP server protection with Discord OAuth.
- Go to https://discord.com/developers/applications
- Click "New Application" and give it a name
- Go to OAuth2 in the left sidebar
- - Add a Redirect URL: `http://localhost:8000/auth/callback`
+ - Add a Redirect URL: `http://127.0.0.1:8000/auth/callback`
- Copy the Client ID and Client Secret
2. Set environment variables:
diff --git a/examples/auth/discord_oauth/server.py b/examples/auth/discord_oauth/server.py
index 424c97bdb..1e109b76a 100644
--- a/examples/auth/discord_oauth/server.py
+++ b/examples/auth/discord_oauth/server.py
@@ -18,7 +18,7 @@ from fastmcp.server.auth.providers.discord import DiscordProvider
auth = DiscordProvider(
client_id=os.getenv("FASTMCP_SERVER_AUTH_DISCORD_CLIENT_ID") or "",
client_secret=os.getenv("FASTMCP_SERVER_AUTH_DISCORD_CLIENT_SECRET") or "",
- base_url="http://localhost:8000",
+ base_url="http://127.0.0.1:8000",
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
)
diff --git a/examples/auth/github_oauth/README.md b/examples/auth/github_oauth/README.md
index 557ba7774..dcd5c2205 100644
--- a/examples/auth/github_oauth/README.md
+++ b/examples/auth/github_oauth/README.md
@@ -6,7 +6,7 @@ Demonstrates FastMCP server protection with GitHub OAuth.
1. Create a GitHub OAuth App:
- Go to GitHub Settings > Developer settings > OAuth Apps
- - Set Authorization callback URL to: `http://localhost:8000/auth/callback`
+ - Set Authorization callback URL to: `http://127.0.0.1:8000/auth/callback`
- Copy the Client ID and Client Secret
2. Set environment variables:
diff --git a/examples/auth/github_oauth/client.py b/examples/auth/github_oauth/client.py
index 7158583bc..8722a547c 100644
--- a/examples/auth/github_oauth/client.py
+++ b/examples/auth/github_oauth/client.py
@@ -10,7 +10,7 @@ import asyncio
from fastmcp.client import Client, OAuth
-SERVER_URL = "http://localhost:8000/mcp"
+SERVER_URL = "http://127.0.0.1:8000/mcp"
async def main():
diff --git a/examples/auth/github_oauth/server.py b/examples/auth/github_oauth/server.py
index 1f88c6977..e93d6f01a 100644
--- a/examples/auth/github_oauth/server.py
+++ b/examples/auth/github_oauth/server.py
@@ -18,7 +18,7 @@ from fastmcp.server.auth.providers.github import GitHubProvider
auth = GitHubProvider(
client_id=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID") or "",
client_secret=os.getenv("FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET") or "",
- base_url="http://localhost:8000",
+ base_url="http://127.0.0.1:8000",
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
)
diff --git a/examples/auth/google_oauth/README.md b/examples/auth/google_oauth/README.md
index 869718344..82bcd8696 100644
--- a/examples/auth/google_oauth/README.md
+++ b/examples/auth/google_oauth/README.md
@@ -9,7 +9,7 @@ Demonstrates FastMCP server protection with Google OAuth.
- Create or select a project
- Go to APIs & Services > Credentials
- Create OAuth 2.0 Client ID (Web application)
- - Add Authorized redirect URI: `http://localhost:8000/auth/callback`
+ - Add Authorized redirect URI: `http://127.0.0.1:8000/auth/callback`
- Copy the Client ID and Client Secret
2. Set environment variables:
diff --git a/examples/auth/google_oauth/server.py b/examples/auth/google_oauth/server.py
index 2a5b1c7df..2043ed6c3 100644
--- a/examples/auth/google_oauth/server.py
+++ b/examples/auth/google_oauth/server.py
@@ -18,7 +18,7 @@ from fastmcp.server.auth.providers.google import GoogleProvider
auth = GoogleProvider(
client_id=os.getenv("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_ID") or "",
client_secret=os.getenv("FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET") or "",
- base_url="http://localhost:8000",
+ base_url="http://127.0.0.1:8000",
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
# Optional: specify required scopes
# required_scopes=["openid", "https://www.googleapis.com/auth/userinfo.email"],
diff --git a/examples/auth/keycloak_oauth/README.md b/examples/auth/keycloak_oauth/README.md
new file mode 100644
index 000000000..ba6b95bf4
--- /dev/null
+++ b/examples/auth/keycloak_oauth/README.md
@@ -0,0 +1,29 @@
+# Keycloak OAuth Example
+
+Demonstrates FastMCP server protection with Keycloak OAuth.
+
+**Requires Keycloak 26.6.0 or later** with Dynamic Client Registration enabled.
+
+## Setup
+
+1. Configure a Keycloak realm with Dynamic Client Registration enabled and a trusted host policy for your server URL (e.g. `http://127.0.0.1:8000/*`).
+
+2. Set environment variables:
+
+ ```bash
+ export KEYCLOAK_REALM_URL="http://localhost:8080/realms/your-realm"
+ ```
+
+3. Run the server:
+
+ ```bash
+ python server.py
+ ```
+
+4. In another terminal, run the client:
+
+ ```bash
+ python client.py
+ ```
+
+The client will open your browser for Keycloak authentication.
diff --git a/examples/auth/keycloak_oauth/client.py b/examples/auth/keycloak_oauth/client.py
new file mode 100644
index 000000000..4992abbab
--- /dev/null
+++ b/examples/auth/keycloak_oauth/client.py
@@ -0,0 +1,33 @@
+"""OAuth client example for connecting to a Keycloak-protected FastMCP server.
+
+To run:
+ python client.py
+"""
+
+import asyncio
+
+from fastmcp import Client
+
+SERVER_URL = "http://127.0.0.1:8000/mcp"
+
+
+async def main():
+ async with Client(SERVER_URL, auth="oauth") as client:
+ assert await client.ping()
+ print("Successfully authenticated!")
+
+ tools = await client.list_tools()
+ print(f"Available tools ({len(tools)}):")
+ for tool in tools:
+ print(f" - {tool.name}: {tool.description}")
+
+ print("Calling protected tool: get_access_token_claims")
+ result = await client.call_tool("get_access_token_claims")
+ claims = result.data
+ print(f" sub: {claims.get('sub', 'N/A')}")
+ print(f" scope: {claims.get('scope', 'N/A')}")
+ print(f" azp: {claims.get('azp', 'N/A')}")
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/examples/auth/keycloak_oauth/server.py b/examples/auth/keycloak_oauth/server.py
new file mode 100644
index 000000000..7b4653103
--- /dev/null
+++ b/examples/auth/keycloak_oauth/server.py
@@ -0,0 +1,44 @@
+"""Keycloak OAuth server example for FastMCP.
+
+This example demonstrates how to protect a FastMCP server with Keycloak OAuth.
+
+Required: Keycloak 26.6.0 or later with Dynamic Client Registration enabled.
+
+To run:
+ KEYCLOAK_REALM_URL=https://your-keycloak.com/realms/myrealm python server.py
+"""
+
+import os
+
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.keycloak import KeycloakAuthProvider
+from fastmcp.server.dependencies import get_access_token
+
+auth = KeycloakAuthProvider(
+ realm_url=os.getenv("KEYCLOAK_REALM_URL") or "http://localhost:8080/realms/fastmcp",
+ base_url="http://127.0.0.1:8000",
+ # audience="http://127.0.0.1:8000", # Recommended for production
+)
+
+mcp = FastMCP("Keycloak Example Server", auth=auth)
+
+
+@mcp.tool
+def echo(message: str) -> str:
+ """Echo the provided message."""
+ return message
+
+
+@mcp.tool
+async def get_access_token_claims() -> dict:
+ """Get the authenticated user's access token claims."""
+ token = get_access_token()
+ return {
+ "sub": token.claims.get("sub"),
+ "scope": token.claims.get("scope"),
+ "azp": token.claims.get("azp"),
+ }
+
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000)
diff --git a/examples/auth/mounted/README.md b/examples/auth/mounted/README.md
index 5810dab4c..2bf213094 100644
--- a/examples/auth/mounted/README.md
+++ b/examples/auth/mounted/README.md
@@ -4,12 +4,12 @@ This example demonstrates mounting multiple OAuth-protected MCP servers in a sin
## URL Structure
-- **GitHub MCP**: `http://localhost:8000/api/mcp/github/mcp`
-- **Google MCP**: `http://localhost:8000/api/mcp/google/mcp`
+- **GitHub MCP**: `http://127.0.0.1:8000/api/mcp/github/mcp`
+- **Google MCP**: `http://127.0.0.1:8000/api/mcp/google/mcp`
Discovery endpoints (RFC 8414 path-aware):
-- **GitHub**: `http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/github`
-- **Google**: `http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/google`
+- **GitHub**: `http://127.0.0.1:8000/.well-known/oauth-authorization-server/api/mcp/github`
+- **Google**: `http://127.0.0.1:8000/.well-known/oauth-authorization-server/api/mcp/google`
## Setup
@@ -23,8 +23,8 @@ export FASTMCP_SERVER_AUTH_GOOGLE_CLIENT_SECRET="your-google-client-secret"
```
Configure redirect URIs in each provider's developer console (note the `/api/mcp/{provider}` prefix since the servers are mounted):
-- GitHub: `http://localhost:8000/api/mcp/github/auth/callback/github`
-- Google: `http://localhost:8000/api/mcp/google/auth/callback/google`
+- GitHub: `http://127.0.0.1:8000/api/mcp/github/auth/callback/github`
+- Google: `http://127.0.0.1:8000/api/mcp/google/auth/callback/google`
## Running
diff --git a/examples/auth/mounted/server.py b/examples/auth/mounted/server.py
index 24aefdd59..c5b3af593 100644
--- a/examples/auth/mounted/server.py
+++ b/examples/auth/mounted/server.py
@@ -5,10 +5,10 @@ application, each with its own provider. It showcases RFC 8414 path-aware discov
where each server has its own authorization server metadata endpoint.
URL structure:
-- GitHub MCP: http://localhost:8000/api/mcp/github/mcp
-- Google MCP: http://localhost:8000/api/mcp/google/mcp
-- GitHub discovery: http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/github
-- Google discovery: http://localhost:8000/.well-known/oauth-authorization-server/api/mcp/google
+- GitHub MCP: http://127.0.0.1:8000/api/mcp/github/mcp
+- Google MCP: http://127.0.0.1:8000/api/mcp/google/mcp
+- GitHub discovery: http://127.0.0.1:8000/.well-known/oauth-authorization-server/api/mcp/github
+- Google discovery: http://127.0.0.1:8000/.well-known/oauth-authorization-server/api/mcp/google
Required environment variables:
- FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID: Your GitHub OAuth app client ID
@@ -31,7 +31,7 @@ from fastmcp.server.auth.providers.github import GitHubProvider
from fastmcp.server.auth.providers.google import GoogleProvider
# Configuration
-ROOT_URL = "http://localhost:8000"
+ROOT_URL = "http://127.0.0.1:8000"
API_PREFIX = "/api/mcp"
# --- GitHub OAuth Server ---
diff --git a/examples/auth/oci_oauth/README.md b/examples/auth/oci_oauth/README.md
new file mode 100644
index 000000000..5b831770d
--- /dev/null
+++ b/examples/auth/oci_oauth/README.md
@@ -0,0 +1,51 @@
+# Oracle (OCI IAM (Identity Domain)) OAuth Example
+
+This example demonstrates how to use the OCI IAM OAuth provider with FastMCP servers.
+
+## Setup
+
+### 1. OCI App Registration
+
+1. Login to OCI console (https://cloud.oracle.com for OCI commercial cloud).
+2. From "Identity & Security" menu, open Domains page.
+3. On the Domains list page, select the domain in which you want to create MCP server OAuth client. If you need help finding the list page for the domain, see [Listing Identity Domains.](https://docs.oracle.com/en-us/iaas/Content/Identity/domains/to-view-identity-domains.htm#view-identity-domains).
+4. On the details page, select Integrated applications. A list of applications in the domain is displayed.
+5. Select Add application.
+6. In the Add application window, select Confidential Application.
+7. Select Launch workflow.
+8. In the Add application details page, Enter name and description and create the application.
+9. Once the Integrated Application is created, Click on "OAuth configuration" tab.
+10. Click on "Edit OAuth configuration" button.
+11. Configure the application as OAuth client by selecting "Configure this application as a client now" radio button.
+12. Select "Authorization code" grant type. If you are planning to use the same OAuth client application for token exchange, select "Client credentials" grant type as well. In the sample, we will use the same client.
+13. For Authorization grant type, select redirect URL. In most cases, this will be the MCP server URL followed by "/auth/callback". For example http://localhost:8000/auth/callback
+14. Click on "Submit" button to update OAuth configuration for the client application.
+15. Make sure to Activate the client application.
+16. Note down client ID and client secret for the application. You'll use these values when configuring the OCIProvider in the MCP server.
+
+For details instructions with screenshots, please refer to [FastMCP OCI Provider Documentation](https://gofastmcp.com/integrations/oci).
+
+### 2. Set Environment Variables
+
+```bash
+# Required
+FASTMCP_SERVER_AUTH_IDCS_CLIENT_ID=your-application-client-id
+FASTMCP_SERVER_AUTH_IDCS_CLIENT_SECRET=your-client-secret-value
+FASTMCP_SERVER_AUTH_IDCS_DOMAIN=your-iam-domain-url # IDCS domain URL for example idcs-abscasdwdac3432rdwsda.identity.oraclecloud.com
+```
+
+### 3. Run the Example
+
+Start the server:
+
+```bash
+python server.py
+```
+
+Test with client:
+
+```bash
+python client.py
+```
+
+When you run the client, it will open a browser on your machine to login to OCI IAM domain.
diff --git a/examples/auth/oci_oauth/client.py b/examples/auth/oci_oauth/client.py
new file mode 100644
index 000000000..d7f2b760a
--- /dev/null
+++ b/examples/auth/oci_oauth/client.py
@@ -0,0 +1,32 @@
+"""OAuth client example for connecting to FastMCP servers.
+
+This example demonstrates how to connect to an OAuth-protected FastMCP server.
+
+To run:
+ python client.py
+"""
+
+import asyncio
+
+from fastmcp.client import Client
+
+SERVER_URL = "http://localhost:8000/mcp"
+
+
+async def main():
+ try:
+ async with Client(SERVER_URL, auth="oauth") as client:
+ assert await client.ping()
+ print("✅ Successfully authenticated!")
+
+ tools = await client.list_tools()
+ print(f"🔧 Available tools ({len(tools)}):")
+ for tool in tools:
+ print(f" - {tool.name}: {tool.description}")
+ except Exception as e:
+ print(f"❌ Authentication failed: {e}")
+ raise
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/examples/auth/oci_oauth/server.py b/examples/auth/oci_oauth/server.py
new file mode 100644
index 000000000..e1b09f827
--- /dev/null
+++ b/examples/auth/oci_oauth/server.py
@@ -0,0 +1,38 @@
+"""Oracle OCI IAM OAuth server example for FastMCP.
+
+This example demonstrates how to protect a FastMCP server with Oracle OCI IAM OAuth.
+
+Required environment variables:
+- FASTMCP_SERVER_AUTH_IDCS_CLIENT_ID: Your IDCS OAuth Application clientID
+- FASTMCP_SERVER_AUTH_IDCS_CLIENT_SECRET: Your IDCS client secret
+- FASTMCP_SERVER_AUTH_IDCS_DOMAIN: IDCS domain URL for example idcs-abscasdwdac3432rdwsda.identity.oraclecloud.com
+
+To run:
+ python server.py
+"""
+
+import os
+
+from fastmcp import FastMCP
+from fastmcp.server.auth.providers.oci import OCIProvider
+
+auth = OCIProvider(
+ client_id=os.getenv("FASTMCP_SERVER_AUTH_IDCS_CLIENT_ID") or "",
+ client_secret=os.getenv("FASTMCP_SERVER_AUTH_IDCS_CLIENT_SECRET") or "",
+ config_url=f"https://{os.getenv('FASTMCP_SERVER_AUTH_IDCS_DOMAIN')}/.well-known/openid-configuration"
+ or "",
+ base_url="http://localhost:8000",
+ # redirect_path="/auth/callback", # Default path - change if using a different callback URL
+)
+
+mcp = FastMCP("OCI OAuth Example Server", auth=auth)
+
+
+@mcp.tool
+def echo(message: str) -> str:
+ """Echo the provided message."""
+ return message
+
+
+if __name__ == "__main__":
+ mcp.run(transport="http", port=8000, host="localhost")
diff --git a/examples/auth/propelauth_oauth/README.md b/examples/auth/propelauth_oauth/README.md
index 575ea7314..aa5b10ecf 100644
--- a/examples/auth/propelauth_oauth/README.md
+++ b/examples/auth/propelauth_oauth/README.md
@@ -36,7 +36,7 @@ Create a `.env` file:
PROPELAUTH_AUTH_URL=https://auth.yourdomain.com
PROPELAUTH_INTROSPECTION_CLIENT_ID=your-client-id
PROPELAUTH_INTROSPECTION_CLIENT_SECRET=your-client-secret
-BASE_URL=http://localhost:8000/
+BASE_URL=http://127.0.0.1:8000/
# Optional: additional scopes tokens must include (comma-separated)
# PROPELAUTH_REQUIRED_SCOPES=read:user_data
```
@@ -50,7 +50,7 @@ Start the server:
uv run python server.py
```
-The server will start on `http://localhost:8000/mcp` with PropelAuth OAuth authentication enabled.
+The server will start on `http://127.0.0.1:8000/mcp` with PropelAuth OAuth authentication enabled.
Test with client:
diff --git a/examples/auth/propelauth_oauth/server.py b/examples/auth/propelauth_oauth/server.py
index 8401882aa..ab1661d22 100644
--- a/examples/auth/propelauth_oauth/server.py
+++ b/examples/auth/propelauth_oauth/server.py
@@ -9,7 +9,7 @@ Required environment variables:
Optional:
- PROPELAUTH_REQUIRED_SCOPES: Comma-separated scopes tokens must include
-- BASE_URL: Public URL where the FastMCP server is exposed (defaults to `http://localhost:8000/`)
+- BASE_URL: Public URL where the FastMCP server is exposed (defaults to `http://127.0.0.1:8000/`)
To run:
python server.py
@@ -29,7 +29,7 @@ auth = PropelAuthProvider(
auth_url=os.environ["PROPELAUTH_AUTH_URL"],
introspection_client_id=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_ID"],
introspection_client_secret=os.environ["PROPELAUTH_INTROSPECTION_CLIENT_SECRET"],
- base_url=os.getenv("BASE_URL", "http://localhost:8000/"),
+ base_url=os.getenv("BASE_URL", "http://127.0.0.1:8000/"),
)
mcp = FastMCP("PropelAuth OAuth Example Server", auth=auth)
diff --git a/examples/auth/scalekit_oauth/README.md b/examples/auth/scalekit_oauth/README.md
index c241d76f7..d16b81c37 100644
--- a/examples/auth/scalekit_oauth/README.md
+++ b/examples/auth/scalekit_oauth/README.md
@@ -24,7 +24,7 @@ Create a `.env` file:
# Required Scalekit credentials
SCALEKIT_ENVIRONMENT_URL=
SCALEKIT_RESOURCE_ID= # res_926EXAMPLE5878
-BASE_URL=http://localhost:8000/
+BASE_URL=http://127.0.0.1:8000/
# Optional: additional scopes tokens must include (comma-separated)
# SCALEKIT_REQUIRED_SCOPES=read,write
```
@@ -38,7 +38,7 @@ Start the server:
uv run python server.py
```
-The server will start on `http://localhost:8000/mcp` with Scalekit OAuth authentication enabled.
+The server will start on `http://127.0.0.1:8000/mcp` with Scalekit OAuth authentication enabled.
Test with client:
diff --git a/examples/auth/scalekit_oauth/server.py b/examples/auth/scalekit_oauth/server.py
index 68cef23b5..09d4f5959 100644
--- a/examples/auth/scalekit_oauth/server.py
+++ b/examples/auth/scalekit_oauth/server.py
@@ -8,7 +8,7 @@ Required environment variables:
Optional:
- SCALEKIT_REQUIRED_SCOPES: Comma-separated scopes tokens must include
-- BASE_URL: Public URL where the FastMCP server is exposed (defaults to `http://localhost:8000/`)
+- BASE_URL: Public URL where the FastMCP server is exposed (defaults to `http://127.0.0.1:8000/`)
To run:
python server.py
@@ -30,7 +30,7 @@ auth = ScalekitProvider(
environment_url=os.getenv("SCALEKIT_ENVIRONMENT_URL")
or "https://your-env.scalekit.com",
resource_id=os.getenv("SCALEKIT_RESOURCE_ID") or "",
- base_url=os.getenv("BASE_URL", "http://localhost:8000/"),
+ base_url=os.getenv("BASE_URL", "http://127.0.0.1:8000/"),
required_scopes=required_scopes,
)
diff --git a/examples/auth/workos_oauth/server.py b/examples/auth/workos_oauth/server.py
index 08c1db62b..4dba970a8 100644
--- a/examples/auth/workos_oauth/server.py
+++ b/examples/auth/workos_oauth/server.py
@@ -20,7 +20,7 @@ auth = WorkOSProvider(
client_id=os.getenv("WORKOS_CLIENT_ID") or "",
client_secret=os.getenv("WORKOS_CLIENT_SECRET") or "",
authkit_domain=os.getenv("WORKOS_AUTHKIT_DOMAIN") or "https://your-app.authkit.app",
- base_url="http://localhost:8000",
+ base_url="http://127.0.0.1:8000",
# redirect_path="/auth/callback", # Default path - change if using a different callback URL
)
diff --git a/examples/filesystem-provider/components/prompts/assistant.py b/examples/filesystem-provider/components/prompts/assistant.py
new file mode 100644
index 000000000..0950e3423
--- /dev/null
+++ b/examples/filesystem-provider/components/prompts/assistant.py
@@ -0,0 +1,39 @@
+"""Assistant prompts."""
+
+from fastmcp.prompts import prompt
+
+
+@prompt
+def code_review(code: str, language: str = "python") -> str:
+ """Generate a code review prompt.
+
+ Args:
+ code: The code to review.
+ language: Programming language (default: python).
+ """
+ return f"""Please review this {language} code:
+
+```{language}
+{code}
+```
+
+Focus on:
+- Code quality and readability
+- Potential bugs or issues
+- Performance considerations
+- Best practices"""
+
+
+@prompt(
+ name="explain-concept",
+ description="Generate a prompt to explain a technical concept.",
+ tags={"education", "explanation"},
+)
+def explain(topic: str, audience: str = "developer") -> str:
+ """Generate an explanation prompt.
+
+ Args:
+ topic: The concept to explain.
+ audience: Target audience level.
+ """
+ return f"Explain {topic} to a {audience}. Use clear examples and analogies."
diff --git a/examples/filesystem-provider/components/resources/config.py b/examples/filesystem-provider/components/resources/config.py
new file mode 100644
index 000000000..03a3f7b11
--- /dev/null
+++ b/examples/filesystem-provider/components/resources/config.py
@@ -0,0 +1,55 @@
+"""Configuration resources - static and templated."""
+
+import json
+
+from fastmcp.resources import resource
+
+
+# Static resource - no parameters in URI
+@resource("config://app")
+def get_app_config() -> str:
+ """Get application configuration."""
+ return json.dumps(
+ {
+ "name": "FilesystemDemo",
+ "version": "1.0.0",
+ "features": ["tools", "resources", "prompts"],
+ },
+ indent=2,
+ )
+
+
+# Resource template - {env} is a parameter
+@resource("config://env/{env}")
+def get_env_config(env: str) -> str:
+ """Get environment-specific configuration.
+
+ Args:
+ env: Environment name (dev, staging, prod).
+ """
+ configs = {
+ "dev": {"debug": True, "log_level": "DEBUG", "database": "localhost"},
+ "staging": {"debug": True, "log_level": "INFO", "database": "staging-db"},
+ "prod": {"debug": False, "log_level": "WARNING", "database": "prod-db"},
+ }
+ config = configs.get(env, {"error": f"Unknown environment: {env}"})
+ return json.dumps(config, indent=2)
+
+
+# Resource with custom metadata
+@resource(
+ "config://features",
+ name="feature-flags",
+ mime_type="application/json",
+ tags={"config", "features"},
+)
+def get_feature_flags() -> str:
+ """Get feature flags configuration."""
+ return json.dumps(
+ {
+ "dark_mode": True,
+ "beta_features": False,
+ "max_upload_size_mb": 100,
+ },
+ indent=2,
+ )
diff --git a/examples/filesystem-provider/components/tools/calculator.py b/examples/filesystem-provider/components/tools/calculator.py
new file mode 100644
index 000000000..f8d901067
--- /dev/null
+++ b/examples/filesystem-provider/components/tools/calculator.py
@@ -0,0 +1,24 @@
+"""Math tools with custom metadata."""
+
+from fastmcp.tools import tool
+
+
+@tool(
+ name="add-numbers", # Custom name (default would be "add")
+ description="Add two numbers together.",
+ tags={"math", "arithmetic"},
+)
+def add(a: float, b: float) -> float:
+ """Add two numbers."""
+ return a + b
+
+
+@tool(tags={"math", "arithmetic"})
+def multiply(a: float, b: float) -> float:
+ """Multiply two numbers.
+
+ Args:
+ a: First number.
+ b: Second number.
+ """
+ return a * b
diff --git a/examples/filesystem-provider/components/tools/greeting.py b/examples/filesystem-provider/components/tools/greeting.py
new file mode 100644
index 000000000..f124902a1
--- /dev/null
+++ b/examples/filesystem-provider/components/tools/greeting.py
@@ -0,0 +1,28 @@
+"""Greeting tools - multiple tools in one file."""
+
+from fastmcp.tools import tool
+
+
+@tool
+def greet(name: str) -> str:
+ """Greet someone by name.
+
+ Args:
+ name: The person's name.
+ """
+ return f"Hello, {name}!"
+
+
+@tool
+def farewell(name: str) -> str:
+ """Say goodbye to someone.
+
+ Args:
+ name: The person's name.
+ """
+ return f"Goodbye, {name}!"
+
+
+# Helper functions without decorators are ignored
+def _format_message(msg: str) -> str:
+ return msg.strip().capitalize()
diff --git a/examples/filesystem-provider/server.py b/examples/filesystem-provider/server.py
index 2f3cf47a6..11bbf7f81 100644
--- a/examples/filesystem-provider/server.py
+++ b/examples/filesystem-provider/server.py
@@ -22,7 +22,7 @@ from fastmcp.server.providers import FileSystemProvider
# Functions decorated with @tool, @resource, or @prompt are registered.
# Directory structure is purely organizational - decorators determine type.
provider = FileSystemProvider(
- root=Path(__file__).parent / "mcp",
+ root=Path(__file__).parent / "components",
reload=True, # Set True for dev mode (re-scan on every request)
)
diff --git a/examples/testing_demo/pyproject.toml b/examples/testing_demo/pyproject.toml
index dce14d85e..6130b9cf2 100644
--- a/examples/testing_demo/pyproject.toml
+++ b/examples/testing_demo/pyproject.toml
@@ -6,7 +6,7 @@ readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"fastmcp>=2.0.0",
- "pytest>=8.3.3",
+ "pytest>=9.0.3",
"pytest-asyncio>=1.2.0",
"dirty-equals>=0.9.0",
]
diff --git a/examples/testing_demo/uv.lock b/examples/testing_demo/uv.lock
index 74be17426..4616e4623 100644
--- a/examples/testing_demo/uv.lock
+++ b/examples/testing_demo/uv.lock
@@ -48,14 +48,14 @@ wheels = [
[[package]]
name = "authlib"
-version = "1.6.9"
+version = "1.6.11"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/28/10/b325d58ffe86815b399334a101e63bc6fa4e1953921cb23703b48a0a0220/authlib-1.6.11.tar.gz", hash = "sha256:64db35b9b01aeccb4715a6c9a6613a06f2bd7be2ab9d2eb89edd1dfc7580a38f", size = 165359, upload-time = "2026-04-16T07:22:50.279Z" }
wheels = [
- { 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" },
+ { url = "https://files.pythonhosted.org/packages/57/2f/55fca558f925a51db046e5b929deb317ddb05afed74b22d89f4eca578980/authlib-1.6.11-py2.py3-none-any.whl", hash = "sha256:c8687a9a26451c51a34a06fa17bb97cb15bba46a6a626755e2d7f50da8bff3e3", size = 244469, upload-time = "2026-04-16T07:22:48.413Z" },
]
[[package]]
@@ -237,62 +237,62 @@ wheels = [
[[package]]
name = "cryptography"
-version = "46.0.6"
+version = "46.0.7"
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/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
wheels = [
- { 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" },
+ { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
+ { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
+ { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
+ { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" },
+ { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
+ { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
+ { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
+ { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
+ { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
+ { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
+ { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
+ { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" },
+ { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
+ { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
+ { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
+ { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
+ { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
+ { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" },
+ { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" },
]
[[package]]
@@ -375,7 +375,7 @@ wheels = [
[[package]]
name = "fastmcp"
-version = "3.1.1"
+version = "3.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "authlib" },
@@ -400,9 +400,9 @@ dependencies = [
{ name = "watchfiles" },
{ name = "websockets" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/32/4f1b2cfd7b50db89114949f90158b1dcc2c92a1917b9f57c0ff24e47a2f4/fastmcp-3.2.0.tar.gz", hash = "sha256:d4830b8ffc3592d3d9c76dc0f398904cf41f04910e41a0de38cc1004e0903bef", size = 26318581, upload-time = "2026-03-30T20:25:37.692Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/70/ea/570122de7e24f72138d006f799768e14cc1ccf7fcb22b7750b2bd276c711/fastmcp-3.1.1-py3-none-any.whl", hash = "sha256:8132ba069d89f14566b3266919d6d72e2ec23dd45d8944622dca407e9beda7eb", size = 633754, upload-time = "2026-03-14T19:12:22.736Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/67/684fa2d2de1e7504549d4ca457b4f854ccec3cd3be03bd86b33b599fbf58/fastmcp-3.2.0-py3-none-any.whl", hash = "sha256:e71aba3df16f86f546a4a9e513261d3233bcc92bef0dfa647bac3fa33623f681", size = 705550, upload-time = "2026-03-30T20:25:35.499Z" },
]
[[package]]
@@ -898,11 +898,11 @@ wheels = [
[[package]]
name = "pygments"
-version = "2.19.2"
+version = "2.20.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
@@ -933,7 +933,7 @@ wheels = [
[[package]]
name = "pytest"
-version = "9.0.2"
+version = "9.0.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
@@ -944,9 +944,9 @@ dependencies = [
{ name = "pygments" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
-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" }
+sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
]
[[package]]
@@ -974,11 +974,11 @@ wheels = [
[[package]]
name = "python-multipart"
-version = "0.0.22"
+version = "0.0.26"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/94/01/979e98d542a70714b0cb2b6728ed0b7c46792b695e3eaec3e20711271ca3/python_multipart-0.0.22.tar.gz", hash = "sha256:7340bef99a7e0032613f56dc36027b959fd3b30a787ed62d310e951f7c3a3a58", size = 37612, upload-time = "2026-01-25T10:15:56.219Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
]
[[package]]
@@ -1292,7 +1292,7 @@ dependencies = [
requires-dist = [
{ name = "dirty-equals", specifier = ">=0.9.0" },
{ name = "fastmcp", specifier = ">=2.0.0" },
- { name = "pytest", specifier = ">=8.3.3" },
+ { name = "pytest", specifier = ">=9.0.3" },
{ name = "pytest-asyncio", specifier = ">=1.2.0" },
]
diff --git a/fastmcp_slim/README.md b/fastmcp_slim/README.md
new file mode 100644
index 000000000..9afc7f4f7
--- /dev/null
+++ b/fastmcp_slim/README.md
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+# FastMCP 🚀
+
+Move fast and make things.
+
+*Made with 💙 by [Prefect](https://www.prefect.io/)*
+
+[](https://gofastmcp.com)
+[](https://discord.gg/uu8dJCgttd)
+[](https://pypi.org/project/fastmcp)
+[](https://github.com/PrefectHQ/fastmcp/actions/workflows/run-tests.yml)
+[](https://github.com/PrefectHQ/fastmcp/blob/main/LICENSE)
+
+
+
+
+---
+
+The [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) connects LLMs to tools and data. FastMCP gives you everything you need to go from prototype to production:
+
+```python
+from fastmcp import FastMCP
+
+mcp = FastMCP("Demo 🚀")
+
+@mcp.tool
+def add(a: int, b: int) -> int:
+ """Add two numbers"""
+ return a + b
+
+if __name__ == "__main__":
+ mcp.run()
+```
+
+## Why FastMCP
+
+Building an effective MCP application is harder than it looks. FastMCP handles all of it. Declare a tool with a Python function, and the schema, validation, and documentation are generated automatically. Connect to a server with a URL, and transport negotiation, authentication, and protocol lifecycle are managed for you. You focus on your logic, and the MCP part just works: **with FastMCP, best practices are built in.**
+
+**That's why FastMCP is the standard framework for working with MCP.** FastMCP 1.0 was incorporated into the official MCP Python SDK in 2024. Today, the actively maintained standalone project is downloaded a million times a day, and some version of FastMCP powers 70% of MCP servers across all languages.
+
+FastMCP has three pillars:
+
+
+
+
+
+
+ Servers
+
+ Expose tools, resources, and prompts to LLMs.
+
+
+
+
+ Apps
+
+ Give your tools interactive UIs rendered directly in the conversation.
+
+
+
+
+ Clients
+
+ Connect to any MCP server — local or remote, programmatic or CLI.
+
+
+
+
+**[Servers](https://gofastmcp.com/servers/server)** wrap your Python functions into MCP-compliant tools, resources, and prompts. **[Clients](https://gofastmcp.com/clients/client)** connect to any server with full protocol support. And **[Apps](https://gofastmcp.com/apps/overview)** give your tools interactive UIs rendered directly in the conversation.
+
+Ready to build? Start with the [installation guide](https://gofastmcp.com/getting-started/installation) or jump straight to the [quickstart](https://gofastmcp.com/getting-started/quickstart).
+
+## Run FastMCP in production with Horizon
+
+FastMCP is the standard way to build MCP servers. **[Prefect Horizon](https://www.prefect.io/horizon?utm_source=github&utm_medium=readme&utm_campaign=readme_horizon&utm_content=readme_body)** is the enterprise MCP gateway for running them safely.
+
+Built by the FastMCP team, Horizon packages the best practices we've learned shipping the world's most popular MCP framework.
+
+Deploy FastMCP servers from GitHub with branch previews and instant rollback. Create a private registry of every MCP your company uses. Secure access with SSO and tool-level RBAC. Get audit logs, observability, and governance across your MCP stack. Remix approved tools into purpose-built endpoints for teams and agents.
+
+Start with FastMCP. [Scale with Horizon →](https://www.prefect.io/horizon?utm_source=github&utm_medium=readme&utm_campaign=readme_horizon&utm_content=readme_cta)
+
+## Installation
+
+We recommend installing FastMCP with [uv](https://docs.astral.sh/uv/):
+
+```bash
+uv pip install fastmcp
+```
+
+For full installation instructions, including verification and upgrading, see the [**Installation Guide**](https://gofastmcp.com/getting-started/installation).
+
+**Upgrading?** We have guides for:
+- [Upgrading from FastMCP v2](https://gofastmcp.com/getting-started/upgrading/from-fastmcp-2)
+- [Upgrading from the MCP Python SDK](https://gofastmcp.com/getting-started/upgrading/from-mcp-sdk)
+- [Upgrading from the low-level SDK](https://gofastmcp.com/getting-started/upgrading/from-low-level-sdk)
+
+## 📚 Documentation
+
+FastMCP's complete documentation is available at **[gofastmcp.com](https://gofastmcp.com)**, including detailed guides, API references, and advanced patterns.
+
+Documentation is also available in [llms.txt format](https://llmstxt.org/), which is a simple markdown standard that LLMs can consume easily:
+
+- [`llms.txt`](https://gofastmcp.com/llms.txt) is essentially a sitemap, listing all the pages in the documentation.
+- [`llms-full.txt`](https://gofastmcp.com/llms-full.txt) contains the entire documentation. Note this may exceed the context window of your LLM.
+
+**Community:** Join our [Discord server](https://discord.gg/uu8dJCgttd) to connect with other FastMCP developers and share what you're building.
+
+## Contributing
+
+We welcome contributions! See the [Contributing Guide](https://gofastmcp.com/development/contributing) for setup instructions, testing requirements, and PR guidelines.
diff --git a/fastmcp_slim/fastmcp/__init__.py b/fastmcp_slim/fastmcp/__init__.py
new file mode 100644
index 000000000..ab9f9358d
--- /dev/null
+++ b/fastmcp_slim/fastmcp/__init__.py
@@ -0,0 +1,100 @@
+"""FastMCP - An ergonomic MCP interface."""
+
+import importlib
+import warnings
+from importlib.metadata import PackageNotFoundError, version as _version
+from typing import TYPE_CHECKING
+
+from fastmcp import _install_hints
+from fastmcp.settings import Settings
+from fastmcp.utilities.logging import configure_logging as _configure_logging
+
+if TYPE_CHECKING:
+ from fastmcp.client import Client as Client
+ from fastmcp.apps.app import FastMCPApp as FastMCPApp
+ from fastmcp.exceptions import (
+ FastMCPDeprecationWarning as FastMCPDeprecationWarning,
+ )
+ from fastmcp.server.context import Context as Context
+ from fastmcp.server.server import FastMCP as FastMCP
+
+settings = Settings()
+if settings.log_enabled:
+ _configure_logging(
+ level=settings.log_level,
+ enable_rich_tracebacks=settings.enable_rich_tracebacks,
+ )
+
+try:
+ __version__ = _version("fastmcp-slim")
+except PackageNotFoundError:
+ __version__ = _version("fastmcp")
+
+if settings.deprecation_warnings:
+ try:
+ from fastmcp.exceptions import FastMCPDeprecationWarning
+ except ImportError:
+ pass
+ else:
+ warnings.simplefilter("default", FastMCPDeprecationWarning)
+
+
+# --- Lazy imports for performance (see #3292) ---
+# Client and the client submodule are deferred so that server-only users
+# don't pay for the client import chain. Do not convert back to top-level.
+
+
+def __getattr__(name: str) -> object:
+ if name == "Client":
+ try:
+ from fastmcp.client import Client
+ except ImportError as exc:
+ raise ImportError(_install_hints.CLIENT_SUPPORT) from exc
+
+ return Client
+ if name == "Context":
+ try:
+ from fastmcp.server.context import Context
+ except ImportError as exc:
+ raise ImportError(_install_hints.SERVER_SUPPORT) from exc
+
+ return Context
+ if name == "FastMCP":
+ try:
+ from fastmcp.server.server import FastMCP
+ except ImportError as exc:
+ raise ImportError(_install_hints.SERVER_SUPPORT) from exc
+
+ return FastMCP
+ if name == "FastMCPApp":
+ try:
+ from fastmcp.apps.app import FastMCPApp
+ except ImportError as exc:
+ raise ImportError(_install_hints.APP_SUPPORT) from exc
+
+ return FastMCPApp
+ if name == "FastMCPDeprecationWarning":
+ from fastmcp.exceptions import FastMCPDeprecationWarning
+
+ return FastMCPDeprecationWarning
+ if name == "client":
+ try:
+ return importlib.import_module("fastmcp.client")
+ except ImportError as exc:
+ raise ImportError(_install_hints.CLIENT_SUPPORT) from exc
+ if name == "server":
+ try:
+ return importlib.import_module("fastmcp.server")
+ except ImportError as exc:
+ raise ImportError(_install_hints.SERVER_SUPPORT) from exc
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+
+
+__all__ = [
+ "Client",
+ "Context",
+ "FastMCP",
+ "FastMCPApp",
+ "FastMCPDeprecationWarning",
+ "settings",
+]
diff --git a/fastmcp_slim/fastmcp/_install_hints.py b/fastmcp_slim/fastmcp/_install_hints.py
new file mode 100644
index 000000000..89c25f490
--- /dev/null
+++ b/fastmcp_slim/fastmcp/_install_hints.py
@@ -0,0 +1,25 @@
+CLIENT_SUPPORT = (
+ "FastMCP client support is not installed. Install `fastmcp` or "
+ "`fastmcp-slim[client]`."
+)
+
+SERVER_SUPPORT = (
+ "FastMCP server support is not installed. Install `fastmcp` or "
+ "`fastmcp-slim[server]`."
+)
+
+APP_SUPPORT = (
+ "FastMCP app support is not installed. Install `fastmcp[apps]` or "
+ "`fastmcp-slim[server,apps]`."
+)
+
+CLI_SUPPORT = (
+ "FastMCP CLI support is not installed. Install `fastmcp` or `fastmcp-slim[server]`."
+)
+
+
+def full_package(feature: str) -> str:
+ return (
+ f"{feature} require the full `fastmcp` package. "
+ "Install it with `pip install fastmcp`."
+ )
diff --git a/fastmcp_slim/fastmcp/apps/__init__.py b/fastmcp_slim/fastmcp/apps/__init__.py
new file mode 100644
index 000000000..d8fc21696
--- /dev/null
+++ b/fastmcp_slim/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/fastmcp_slim/fastmcp/apps/app.py b/fastmcp_slim/fastmcp/apps/app.py
new file mode 100644
index 000000000..70783e48a
--- /dev/null
+++ b/fastmcp_slim/fastmcp/apps/app.py
@@ -0,0 +1,427 @@
+"""FastMCPApp — a Provider that represents a composable MCP application.
+
+FastMCPApp binds entry-point tools (model calls these) together with backend
+tools (the UI calls these via CallTool). Backend tools are tagged with
+``meta["fastmcp"]["app"]`` so they can be found through the provider chain
+even when transforms (namespace, visibility, etc.) have renamed or hidden
+them — the server sets a context var that tells ``Provider.get_tool`` to
+fall back to a direct lookup for app-visible tools.
+
+Usage::
+
+ from fastmcp import FastMCP, FastMCPApp
+
+ app = FastMCPApp("Dashboard")
+
+ @app.ui()
+ def show_dashboard() -> Component:
+ return Column(...)
+
+ @app.tool()
+ def save_contact(name: str, email: str) -> str:
+ return name
+
+ server = FastMCP("Platform")
+ server.add_provider(app)
+"""
+
+from __future__ import annotations
+
+import inspect
+from collections.abc import AsyncIterator, Callable, Sequence
+from contextlib import asynccontextmanager
+from typing import 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 a hash.
+
+ Structurally identical to the old ``___`` resolver — ``app_name`` is
+ the FastMCPApp's name, known at serialization time from the tool's
+ ``meta["fastmcp"]["app"]`` tag. The only change is the wire format:
+ ``_`` instead of ``___``.
+
+ The dispatcher recognizes the hashed form and routes it via
+ ``get_tool_by_hash`` which walks the provider tree recursively —
+ same pattern as ``get_app_tool``.
+ """
+ from fastmcp.server.providers.addressing import (
+ hashed_backend_name,
+ parse_hashed_backend_name,
+ )
+
+ def _prefix(local_name: str) -> str:
+ if app_name:
+ # Don't re-hash an already-addressed name (same guard the
+ # old ___ resolver had with "___" not in name).
+ if parse_hashed_backend_name(local_name) is not None:
+ return local_name
+ return hashed_backend_name(app_name, local_name)
+ return local_name
+
+ def _resolve_tool_ref(fn: Any) -> Any:
+ from prefab_ui.app import ResolvedTool
+
+ if isinstance(fn, str):
+ return ResolvedTool(name=_prefix(fn))
+
+ fmeta: Any = None
+ try:
+ from fastmcp.decorators import get_fastmcp_meta
+
+ fmeta = get_fastmcp_meta(fn)
+ except Exception:
+ pass
+
+ if fmeta is not None:
+ name: str | None = getattr(fmeta, "name", None)
+ if name is not None:
+ return ResolvedTool(name=_prefix(name))
+
+ fn_name = getattr(fn, "__name__", None)
+ if fn_name is not None:
+ return ResolvedTool(name=_prefix(fn_name))
+
+ raise ValueError(f"Cannot resolve tool reference: {fn!r}")
+
+ return _resolve_tool_ref
+
+
+def _dispatch_decorator(
+ name_or_fn: str | AnyFunction | None,
+ name: str | None,
+ register: Callable[[Any, str | None], Any],
+ decorator_name: str,
+) -> Any:
+ """Shared dispatch logic for @app.tool() and @app.ui() calling patterns."""
+ if inspect.isroutine(name_or_fn):
+ return register(name_or_fn, name)
+
+ if isinstance(name_or_fn, str):
+ if name is not None:
+ raise TypeError(
+ "Cannot specify both a name as first argument and as keyword argument."
+ )
+ tool_name: str | None = name_or_fn
+ elif name_or_fn is None:
+ tool_name = name
+ else:
+ raise TypeError(
+ f"First argument to @{decorator_name} must be a function, string, or None, "
+ f"got {type(name_or_fn)}"
+ )
+
+ def decorator(fn: F) -> F:
+ return register(fn, tool_name)
+
+ return decorator
+
+
+# ---------------------------------------------------------------------------
+# FastMCPApp
+# ---------------------------------------------------------------------------
+
+
+class FastMCPApp(Provider):
+ """A Provider that represents an MCP application.
+
+ Binds together entry-point tools (``@app.ui``), backend tools
+ (``@app.tool``), and the Prefab renderer resource. Backend tools
+ are tagged with ``meta["fastmcp"]["app"]`` so ``Provider.get_tool``
+ can find them by original name even when transforms have been applied.
+ """
+
+ def __init__(self, name: str) -> None:
+ 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
+ from fastmcp.server.providers.addressing import hash_tool
+
+ app_config = AppConfig(visibility=visibility)
+ meta: dict[str, Any] = {
+ "ui": app_config_to_meta_dict(app_config),
+ "fastmcp": {
+ "app": self.name,
+ "_tool_hash": hash_tool(self.name, resolved_name),
+ },
+ }
+
+ tool_obj = Tool.from_function(
+ fn,
+ name=resolved_name,
+ description=description,
+ meta=meta,
+ timeout=timeout,
+ auth=auth,
+ )
+ self._local._add_component(tool_obj)
+ return fn
+
+ return _dispatch_decorator(name_or_fn, name, _register, "tool")
+
+ # ------------------------------------------------------------------
+ # @app.ui() — entry-point tools the model calls to open the app
+ # ------------------------------------------------------------------
+
+ @overload
+ def ui(
+ self,
+ name_or_fn: F,
+ *,
+ name: str | None = None,
+ description: str | None = None,
+ title: str | None = None,
+ tags: set[str] | None = None,
+ icons: list[Icon] | None = None,
+ annotations: ToolAnnotations | None = None,
+ auth: AuthCheck | list[AuthCheck] | None = None,
+ timeout: float | None = None,
+ ) -> F: ...
+
+ @overload
+ def ui(
+ self,
+ name_or_fn: str | None = None,
+ *,
+ name: str | None = None,
+ description: str | None = None,
+ title: str | None = None,
+ tags: set[str] | None = None,
+ icons: list[Icon] | None = None,
+ annotations: ToolAnnotations | None = None,
+ auth: AuthCheck | list[AuthCheck] | None = None,
+ timeout: float | None = None,
+ ) -> Callable[[F], F]: ...
+
+ def ui(
+ self,
+ name_or_fn: str | AnyFunction | None = None,
+ *,
+ name: str | None = None,
+ description: str | None = None,
+ title: str | None = None,
+ tags: set[str] | None = None,
+ icons: list[Icon] | None = None,
+ annotations: ToolAnnotations | None = None,
+ auth: AuthCheck | list[AuthCheck] | None = None,
+ timeout: float | None = None,
+ ) -> Any:
+ """Register a UI entry-point tool that the model calls.
+
+ Entry-point tools default to ``visibility=["model"]`` and auto-wire
+ the Prefab renderer resource and CSP. They are tagged with the app
+ name so structured content includes ``_meta.fastmcp.app``.
+
+ Supports multiple calling patterns::
+
+ @app.ui
+ def dashboard() -> Component: ...
+
+ @app.ui()
+ def dashboard() -> Component: ...
+
+ @app.ui("my_dashboard")
+ def dashboard() -> Component: ...
+ """
+
+ def _register(fn: F, tool_name: str | None) -> F:
+ from fastmcp.apps.config import AppConfig, app_config_to_meta_dict
+ from fastmcp.server.providers.addressing import hash_tool
+ from fastmcp.server.providers.local_provider.decorators.tools import (
+ PREFAB_RENDERER_URI,
+ )
+
+ resolved = tool_name or getattr(fn, "__name__", None) or "unknown"
+ app_config = AppConfig(
+ resource_uri=PREFAB_RENDERER_URI,
+ visibility=["model"],
+ )
+
+ meta: dict[str, Any] = {
+ "ui": app_config_to_meta_dict(app_config),
+ "fastmcp": {
+ "app": self.name,
+ "_tool_hash": hash_tool(self.name, resolved),
+ },
+ }
+
+ tool_obj = Tool.from_function(
+ fn,
+ name=tool_name,
+ description=description,
+ title=title,
+ tags=tags,
+ icons=icons,
+ annotations=annotations,
+ meta=meta,
+ timeout=timeout,
+ auth=auth,
+ )
+ self._local._add_component(tool_obj)
+
+ return fn
+
+ return _dispatch_decorator(name_or_fn, name, _register, "ui")
+
+ # ------------------------------------------------------------------
+ # Programmatic tool addition
+ # ------------------------------------------------------------------
+
+ def add_tool(
+ self,
+ tool: Tool | Callable[..., Any],
+ ) -> Tool:
+ """Add a tool to this app programmatically.
+
+ The tool is tagged with this app's name for routing.
+ """
+ if not isinstance(tool, Tool):
+ tool = Tool._ensure_tool(tool)
+
+ from fastmcp.server.providers.addressing import hash_tool
+
+ meta = dict(tool.meta) if tool.meta else {}
+ fm = meta.setdefault("fastmcp", {})
+ fm["app"] = self.name
+ fm["_tool_hash"] = hash_tool(self.name, tool.name)
+ ui = meta.setdefault("ui", {})
+ if "visibility" not in ui:
+ ui["visibility"] = ["app"]
+ tool.meta = meta
+
+ self._local._add_component(tool)
+ return tool
+
+ # ------------------------------------------------------------------
+ # Provider interface — delegate to internal LocalProvider
+ # ------------------------------------------------------------------
+
+ async def _list_tools(self) -> Sequence[Tool]:
+ return await self._local._list_tools()
+
+ async def _get_tool(self, name: str, version: Any = None) -> Tool | None:
+ return await self._local._get_tool(name, version)
+
+ async def _list_resources(self) -> Sequence[Any]:
+ return await self._local._list_resources()
+
+ async def _get_resource(self, uri: str, version: Any = None) -> Any | None:
+ return await self._local._get_resource(uri, version)
+
+ async def _list_resource_templates(self) -> Sequence[Any]:
+ return await self._local._list_resource_templates()
+
+ async def _get_resource_template(self, uri: str, version: Any = None) -> Any | None:
+ return await self._local._get_resource_template(uri, version)
+
+ async def _list_prompts(self) -> Sequence[Any]:
+ return await self._local._list_prompts()
+
+ async def _get_prompt(self, name: str, version: Any = None) -> Any | None:
+ return await self._local._get_prompt(name, version)
+
+ @asynccontextmanager
+ async def lifespan(self) -> AsyncIterator[None]:
+ async with self._local.lifespan():
+ yield
+
+ # ------------------------------------------------------------------
+ # Convenience runner
+ # ------------------------------------------------------------------
+
+ def run(
+ self,
+ transport: Literal["stdio", "http", "sse", "streamable-http"] | None = None,
+ **kwargs: Any,
+ ) -> None:
+ """Create a temporary FastMCP server and run this app standalone."""
+ from fastmcp.server.server import FastMCP
+
+ server = FastMCP(self.name)
+ server.add_provider(self)
+ server.run(transport=transport, **kwargs)
diff --git a/fastmcp_slim/fastmcp/apps/approval.py b/fastmcp_slim/fastmcp/apps/approval.py
new file mode 100644
index 000000000..17b124e1f
--- /dev/null
+++ b/fastmcp_slim/fastmcp/apps/approval.py
@@ -0,0 +1,198 @@
+"""Approval — a Provider that adds human-in-the-loop approval to any server.
+
+The LLM presents a summary of what it's about to do, and the user
+approves or rejects via buttons. The result is sent back into the
+conversation as a message, prompting the LLM's next turn.
+
+Requires ``fastmcp[apps]`` (prefab-ui).
+
+Usage::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.approval import Approval
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(Approval())
+"""
+
+from __future__ import annotations
+
+from typing import Literal
+
+try:
+ from prefab_ui.actions import SetState
+ from prefab_ui.actions.mcp import SendMessage
+ from prefab_ui.app import PrefabApp
+ from prefab_ui.components import (
+ H3,
+ Button,
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ Column,
+ Muted,
+ Row,
+ Text,
+ )
+ from prefab_ui.components.control_flow import If
+ from prefab_ui.rx import STATE
+except ImportError as _exc:
+ raise ImportError(
+ "Approval requires prefab-ui. Install with: pip install 'fastmcp[apps]'"
+ ) from _exc
+
+
+from fastmcp.apps.app import FastMCPApp
+
+
+class Approval(FastMCPApp):
+ """A Provider that adds human-in-the-loop approval to a server.
+
+ The LLM calls the ``request_approval`` tool with a summary and
+ optional details. The user sees an approval card with Approve and
+ Reject buttons. Clicking either sends a message back into the
+ conversation (via ``SendMessage``), triggering the LLM's next turn.
+
+ The message appears as if the user sent it, so the LLM sees
+ something like ``'"Deploy v3.2 to production" is APPROVED'``.
+
+ Example::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.approval import Approval
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(Approval())
+
+ Customized::
+
+ Approval(
+ title="Deploy Gate",
+ approve_text="Ship it",
+ approve_variant="default",
+ reject_text="Abort",
+ reject_variant="destructive",
+ )
+ """
+
+ def __init__(
+ self,
+ name: str = "Approval",
+ *,
+ title: str = "Approval Required",
+ approve_text: str = "Approve",
+ reject_text: str = "Reject",
+ approve_variant: Literal[
+ "default", "destructive", "success", "info"
+ ] = "default",
+ reject_variant: Literal[
+ "default", "outline", "destructive", "success", "info"
+ ] = "outline",
+ ) -> None:
+ super().__init__(name)
+ self._title = title
+ self._approve_text = approve_text
+ self._reject_text = reject_text
+ self._approve_variant = approve_variant
+ self._reject_variant = reject_variant
+ self._register_tools()
+
+ def __repr__(self) -> str:
+ return f"Approval({self.name!r})"
+
+ def _register_tools(self) -> None:
+ provider = self
+
+ @self.ui()
+ def request_approval(
+ summary: str,
+ details: str | None = None,
+ title: str | None = None,
+ approve_text: str | None = None,
+ reject_text: str | None = None,
+ approve_variant: str | None = None,
+ reject_variant: str | None = None,
+ ) -> PrefabApp:
+ """Request human approval before proceeding with an action.
+
+ Call this tool proactively whenever you are about to take a
+ significant or irreversible action and want the user to
+ confirm first. Do NOT wait for the user to ask you to seek
+ approval — use your judgment about when confirmation is
+ appropriate.
+
+ The user will see an approval card with the summary, optional
+ details, and Approve/Reject buttons. When they click a button,
+ their decision appears as a message in the conversation (as if
+ the user typed it), like:
+
+ "Deploy v3.2 to production" — I selected: Approve
+
+ or:
+
+ "Deploy v3.2 to production" — I selected: Reject
+
+ IMPORTANT: After calling this tool, you MUST stop and wait
+ for the user's response. Do not continue, do not take any
+ other actions, do not generate further output until you see
+ the "I selected:" message. If approved, continue with the
+ action. If rejected, acknowledge and ask how to proceed.
+
+ Args:
+ summary: Brief description of the action requiring approval
+ (shown prominently to the user).
+ details: Optional longer explanation, context, or
+ consequences of the action.
+ title: Heading for the approval card (default: "Approval Required").
+ approve_text: Label for the approve button (default: "Approve").
+ reject_text: Label for the reject button (default: "Reject").
+ approve_variant: Button style — "default", "destructive",
+ "success", or "info".
+ reject_variant: Button style for the reject button
+ (same options plus "outline").
+ """
+ _title = title or provider._title
+ _approve = approve_text or provider._approve_text
+ _reject = reject_text or provider._reject_text
+ _approve_v = approve_variant or provider._approve_variant
+ _reject_v = reject_variant or provider._reject_variant
+
+ approve_msg = f'"{summary}" — I selected: {_approve}'
+ reject_msg = f'"{summary}" — I selected: {_reject}'
+
+ with Card(css_class="max-w-lg mx-auto") as view:
+ with CardHeader():
+ H3(_title)
+
+ with CardContent(), Column(gap=3):
+ Text(summary, css_class="font-medium")
+ if details:
+ Muted(details)
+
+ with CardFooter():
+ with If(STATE.decided):
+ Muted("Response sent.")
+ with If(~STATE.decided): # noqa: SIM117
+ with Row(gap=2, css_class="w-full justify-end"):
+ Button(
+ _reject,
+ variant=_reject_v,
+ on_click=[
+ SendMessage(reject_msg),
+ SetState("decided", True),
+ ],
+ )
+ Button(
+ _approve,
+ variant=_approve_v,
+ on_click=[
+ SendMessage(approve_msg),
+ SetState("decided", True),
+ ],
+ )
+
+ return PrefabApp(
+ view=view,
+ state={"decided": False},
+ )
diff --git a/fastmcp_slim/fastmcp/apps/choice.py b/fastmcp_slim/fastmcp/apps/choice.py
new file mode 100644
index 000000000..aaffef903
--- /dev/null
+++ b/fastmcp_slim/fastmcp/apps/choice.py
@@ -0,0 +1,141 @@
+"""Choice — a Provider that lets the user pick from a set of options.
+
+The LLM presents options, the user clicks one, and the selection
+flows back into the conversation as a message.
+
+Requires ``fastmcp[apps]`` (prefab-ui).
+
+Usage::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.choice import Choice
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(Choice())
+"""
+
+from __future__ import annotations
+
+from typing import Literal
+
+try:
+ from prefab_ui.actions import SetState
+ from prefab_ui.actions.mcp import SendMessage
+ from prefab_ui.app import PrefabApp
+ from prefab_ui.components import (
+ H3,
+ Button,
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ Column,
+ Muted,
+ Text,
+ )
+ from prefab_ui.components.control_flow import If
+ from prefab_ui.rx import STATE
+except ImportError as _exc:
+ raise ImportError(
+ "Choice requires prefab-ui. Install with: pip install 'fastmcp[apps]'"
+ ) from _exc
+
+from fastmcp.apps.app import FastMCPApp
+
+
+class Choice(FastMCPApp):
+ """A Provider that lets the user choose from a set of options.
+
+ The LLM calls ``choose`` with a prompt and a list of options.
+ The user sees a card with one button per option. Clicking a button
+ sends the selection back into the conversation via ``SendMessage``,
+ triggering the LLM's next turn.
+
+ Example::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.choice import Choice
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(Choice())
+ """
+
+ def __init__(
+ self,
+ name: str = "Choice",
+ *,
+ title: str = "Choose an Option",
+ variant: Literal[
+ "default", "outline", "destructive", "success", "info"
+ ] = "outline",
+ ) -> None:
+ super().__init__(name)
+ self._title = title
+ self._variant = variant
+ self._register_tools()
+
+ def __repr__(self) -> str:
+ return f"Choice({self.name!r})"
+
+ def _register_tools(self) -> None:
+ provider = self
+
+ @self.ui()
+ def choose(
+ prompt: str,
+ options: list[str],
+ title: str | None = None,
+ ) -> PrefabApp:
+ """Present the user with a set of options to choose from.
+
+ Call this tool when you need the user to make a decision
+ between discrete alternatives. Use it proactively — don't
+ ask the user to type their choice in chat when you can
+ present clean, clickable options instead.
+
+ The user will see a card with one button per option. When
+ they click one, their choice appears as a message in the
+ conversation (as if the user typed it), like:
+
+ "Which deployment strategy?" — I selected: Blue-green
+
+ IMPORTANT: After calling this tool, you MUST stop and wait
+ for the user's response. Do not continue or take any other
+ actions until you see the "I selected:" message.
+
+ Args:
+ prompt: The question or decision to present to the user.
+ options: List of options the user can choose from.
+ title: Optional heading for the card.
+ """
+ _title = title or provider._title
+
+ with Card(css_class="max-w-lg mx-auto") as view:
+ with CardHeader():
+ H3(_title)
+
+ with CardContent():
+ Text(prompt, css_class="font-medium")
+
+ with CardFooter():
+ with If(STATE.decided):
+ Muted("Response sent.")
+ with If(~STATE.decided): # noqa: SIM117
+ with Column(gap=2, css_class="w-full"):
+ for option in options:
+ Button(
+ option,
+ variant=provider._variant,
+ css_class="w-full justify-start",
+ on_click=[
+ SendMessage(
+ f'"{prompt}" — I selected: {option}'
+ ),
+ SetState("decided", True),
+ ],
+ )
+
+ return PrefabApp(
+ view=view,
+ state={"decided": False},
+ )
diff --git a/fastmcp_slim/fastmcp/apps/config.py b/fastmcp_slim/fastmcp/apps/config.py
new file mode 100644
index 000000000..c55cd0b13
--- /dev/null
+++ b/fastmcp_slim/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/fastmcp_slim/fastmcp/apps/file_upload.py b/fastmcp_slim/fastmcp/apps/file_upload.py
new file mode 100644
index 000000000..aeb890379
--- /dev/null
+++ b/fastmcp_slim/fastmcp/apps/file_upload.py
@@ -0,0 +1,405 @@
+"""FileUpload — a Provider that adds drag-and-drop file upload to any server.
+
+Lets users upload files directly to the server through an interactive UI,
+bypassing the LLM context window entirely. The LLM can then read and work
+with uploaded files through model-visible tools.
+
+Requires ``fastmcp[apps]`` (prefab-ui).
+
+Usage::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps import FileUpload
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(FileUpload())
+
+For custom persistence, override the storage methods::
+
+ class S3Upload(FileUpload):
+ def on_store(self, files, ctx):
+ # write to S3, return summaries
+ ...
+
+ def on_list(self, ctx):
+ # list from S3
+ ...
+
+ def on_read(self, name, ctx):
+ # read from S3
+ ...
+"""
+
+from __future__ import annotations
+
+try:
+ from prefab_ui.actions import SetState, ShowToast
+ from prefab_ui.actions.mcp import CallTool
+ from prefab_ui.app import PrefabApp
+ from prefab_ui.components import (
+ H3,
+ Badge,
+ Button,
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ Column,
+ DropZone,
+ Muted,
+ Row,
+ Separator,
+ Small,
+ Text,
+ )
+ from prefab_ui.components.control_flow import Else, ForEach, If
+ from prefab_ui.rx import ERROR, RESULT, STATE, Rx
+except ImportError as _exc:
+ raise ImportError(
+ "FileUpload requires prefab-ui. Install with: pip install 'fastmcp[apps]'"
+ ) from _exc
+
+import base64
+from datetime import datetime, timezone
+from typing import Any
+
+from fastmcp.apps.app import FastMCPApp
+from fastmcp.server.context import Context
+
+_TEXT_EXTENSIONS = frozenset(
+ (".csv", ".json", ".txt", ".md", ".py", ".yaml", ".yml", ".toml")
+)
+
+
+def _b64_decoded_size(b64: str) -> int:
+ """Return the exact decoded byte-length of a base64 string without decoding it."""
+ n = len(b64)
+ if n == 0:
+ return 0
+ padding = b64.count("=", max(0, n - 2))
+ return n * 3 // 4 - padding
+
+
+def _format_size(size: int) -> str:
+ if size < 1024:
+ return f"{size} B"
+ elif size < 1024 * 1024:
+ return f"{size / 1024:.1f} KB"
+ else:
+ return f"{size / (1024 * 1024):.1f} MB"
+
+
+def _make_summary(entry: dict[str, Any]) -> dict[str, Any]:
+ return {
+ "name": entry["name"],
+ "type": entry["type"],
+ "size": entry["size"],
+ "size_display": _format_size(entry["size"]),
+ "uploaded_at": entry["uploaded_at"],
+ }
+
+
+class FileUpload(FastMCPApp):
+ """A Provider that adds file upload capabilities to a server.
+
+ Registers a drag-and-drop UI tool, a backend storage tool, and
+ model-visible tools for listing and reading uploaded files.
+
+ Files are scoped by MCP session and stored in memory by default.
+ Override ``on_store``, ``on_list``, and ``on_read`` for custom
+ persistence (filesystem, S3, database, etc.). Each method receives
+ the current ``Context``, giving access to session ID, auth tokens,
+ and request metadata for partitioning and authorization.
+
+ **Session scoping:** The default storage uses ``ctx.session_id`` to
+ isolate files by session. This works with stdio, SSE, and stateful
+ HTTP transports. In **stateless HTTP** mode, each request creates a
+ new session, so files won't persist across requests. For stateless
+ deployments, override the storage methods to partition by a stable
+ identifier from the auth context::
+
+ class UserScopedUpload(FileUpload):
+ def on_store(self, files, ctx):
+ user_id = ctx.access_token["sub"]
+ ...
+
+ Example::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.file_upload import FileUpload
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(FileUpload())
+ """
+
+ def __init__(
+ self,
+ name: str = "Files",
+ *,
+ max_file_size: int = 10 * 1024 * 1024,
+ title: str = "File Upload",
+ description: str = (
+ "Drop files to upload them to the server. "
+ "The model can then read and analyze them "
+ "without using the context window."
+ ),
+ drop_label: str = "Drop files here",
+ ) -> None:
+ super().__init__(name)
+ self._max_file_size = max_file_size
+ self._title = title
+ self._description = description
+ self._drop_label = drop_label
+
+ # Default in-memory store, keyed by session_id
+ self._store: dict[str, dict[str, dict[str, Any]]] = {}
+
+ self._register_tools()
+
+ def __repr__(self) -> str:
+ return f"FileUpload({self.name!r})"
+
+ # ------------------------------------------------------------------
+ # Storage interface — override these for custom persistence
+ # ------------------------------------------------------------------
+
+ def _get_scope_key(self, ctx: Context) -> str:
+ """Return the key used to partition file storage.
+
+ Defaults to ``ctx.session_id``, which is stable for stdio, SSE,
+ and stateful HTTP. The default ``on_store``/``on_list``/``on_read``
+ implementations call this to partition the in-memory store.
+
+ Override to scope by user, tenant, or any other dimension::
+
+ def _get_scope_key(self, ctx):
+ return ctx.access_token["sub"]
+ """
+ try:
+ return ctx.session_id
+ except RuntimeError:
+ return "__default__"
+
+ def on_store(
+ self,
+ files: list[dict[str, Any]],
+ ctx: Context,
+ ) -> list[dict[str, Any]]:
+ """Store uploaded files and return summaries.
+
+ Args:
+ files: List of file dicts, each with ``name``, ``size``,
+ ``type``, and ``data`` (base64-encoded content).
+ ctx: The current request context. Use for session ID,
+ auth tokens, or any metadata needed for partitioning.
+
+ Override this method for custom persistence. The default
+ implementation stores files in memory, scoped by
+ ``_get_scope_key(ctx)``.
+
+ Returns:
+ List of file summary dicts (``name``, ``type``, ``size``,
+ ``size_display``, ``uploaded_at``).
+ """
+ scope = self._get_scope_key(ctx)
+ session_files = self._store.setdefault(scope, {})
+ for f in files:
+ session_files[f["name"]] = {
+ "name": f["name"],
+ "size": f["size"],
+ "type": f["type"],
+ "data": f["data"],
+ "uploaded_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
+ }
+ return [_make_summary(e) for e in session_files.values()]
+
+ def on_list(self, ctx: Context) -> list[dict[str, Any]]:
+ """List all stored files.
+
+ Args:
+ ctx: The current request context.
+
+ Override this method for custom persistence. The default
+ implementation returns files from the current scope.
+
+ Returns:
+ List of file summary dicts.
+ """
+ scope = self._get_scope_key(ctx)
+ session_files = self._store.get(scope, {})
+ return [_make_summary(e) for e in session_files.values()]
+
+ def on_read(self, name: str, ctx: Context) -> dict[str, Any]:
+ """Read a file's contents by name.
+
+ Args:
+ name: The filename to read.
+ ctx: The current request context.
+
+ Override this method for custom persistence. The default
+ implementation reads from the current scope's in-memory store.
+ Text files are decoded from base64; binary files return a
+ truncated base64 preview.
+
+ Returns:
+ Dict with file metadata and ``content`` (text) or
+ ``content_base64`` (binary preview).
+
+ Raises:
+ ValueError: If the file is not found.
+ """
+ scope = self._get_scope_key(ctx)
+ session_files = self._store.get(scope, {})
+ if name not in session_files:
+ available = list(session_files.keys())
+ raise ValueError(f"File {name!r} not found. Available: {available}")
+ entry = session_files[name]
+ result: dict[str, Any] = {
+ "name": entry["name"],
+ "size": entry["size"],
+ "type": entry["type"],
+ "uploaded_at": entry["uploaded_at"],
+ }
+ is_text = entry["type"].startswith("text/") or any(
+ entry["name"].endswith(ext) for ext in _TEXT_EXTENSIONS
+ )
+ if is_text:
+ try:
+ result["content"] = base64.b64decode(entry["data"]).decode("utf-8")
+ except UnicodeDecodeError:
+ result["content_base64"] = entry["data"][:200] + "..."
+ else:
+ result["content_base64"] = entry["data"][:200] + "..."
+ return result
+
+ # ------------------------------------------------------------------
+ # Tool registration
+ # ------------------------------------------------------------------
+
+ def _register_tools(self) -> None:
+ provider = self
+
+ @self.tool()
+ def store_files(files: list[dict], ctx: Context) -> list[dict]:
+ """Store uploaded files. Receives file objects with name, size, type, data (base64)."""
+ for f in files:
+ # Compute actual data size from the base64 payload rather
+ # than trusting the client-reported ``size`` field.
+ actual_size = _b64_decoded_size(f.get("data", ""))
+ if actual_size > provider._max_file_size:
+ raise ValueError(
+ f"File {f.get('name', '?')!r} exceeds max size "
+ f"({_format_size(actual_size)} > "
+ f"{_format_size(provider._max_file_size)})"
+ )
+ return provider.on_store(files, ctx)
+
+ @self.tool(model=True)
+ def list_files(ctx: Context) -> list[dict]:
+ """List all uploaded files with metadata."""
+ return provider.on_list(ctx)
+
+ @self.tool(model=True)
+ def read_file(name: str, ctx: Context) -> dict:
+ """Read an uploaded file's contents by name."""
+ return provider.on_read(name, ctx)
+
+ @self.ui()
+ def file_manager(ctx: Context) -> PrefabApp:
+ """Upload and manage files. Drop files here to send them to the server."""
+ with Card(css_class="max-w-2xl mx-auto") as view:
+ with CardHeader(), Row(gap=2, align="center"):
+ H3(provider._title)
+ with If(STATE.stored.length()):
+ Badge(
+ STATE.stored.length(), # 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/fastmcp_slim/fastmcp/apps/form.py b/fastmcp_slim/fastmcp/apps/form.py
new file mode 100644
index 000000000..f9b9b0a79
--- /dev/null
+++ b/fastmcp_slim/fastmcp/apps/form.py
@@ -0,0 +1,229 @@
+"""FormInput — a Provider that collects structured input from the user.
+
+Define a Pydantic model for the data you need, and ``FormInput``
+generates a form UI. The user fills it out, the submission is
+validated, and an optional callback processes the result.
+
+Requires ``fastmcp[apps]`` (prefab-ui).
+
+Usage::
+
+ from pydantic import BaseModel
+ from fastmcp import FastMCP
+ from fastmcp.apps.form import FormInput
+
+ class ShippingAddress(BaseModel):
+ street: str
+ city: str
+ state: str
+ zip_code: str
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(FormInput(model=ShippingAddress))
+"""
+
+from __future__ import annotations
+
+import json
+from collections.abc import Callable
+from typing import Any
+
+from packaging.version import InvalidVersion, Version
+
+try:
+ import prefab_ui
+ from prefab_ui.actions import SetState
+ from prefab_ui.actions.mcp import CallTool, SendMessage
+ from prefab_ui.app import PrefabApp
+ from prefab_ui.components import (
+ H3,
+ Card,
+ CardContent,
+ CardFooter,
+ CardHeader,
+ Column,
+ Form,
+ Muted,
+ )
+ from prefab_ui.components.control_flow import If
+ from prefab_ui.rx import RESULT, STATE
+except ImportError as _exc:
+ raise ImportError(
+ "FormInput requires prefab-ui. Install with: pip install 'fastmcp[apps]'"
+ ) from _exc
+
+# `defaults` kwarg on Form.from_model was added in prefab-ui 0.19.1. Gate on
+# version so that older prefab-ui keeps working — `default` silently no-ops.
+try:
+ _FORM_SUPPORTS_DEFAULTS = Version(prefab_ui.__version__) >= Version("0.19.1")
+except InvalidVersion:
+ _FORM_SUPPORTS_DEFAULTS = False
+
+import pydantic
+
+from fastmcp.apps.app import FastMCPApp
+
+
+def _backfill_boolean_defaults(
+ model: type[pydantic.BaseModel],
+ data: dict[str, Any],
+) -> dict[str, Any]:
+ """Fill in missing boolean fields with their model defaults.
+
+ HTML checkboxes omit the field entirely when unchecked, so the
+ submitted data dict won't contain a key for ``False`` booleans.
+ This backfills those missing keys so Pydantic validation succeeds.
+ """
+ for name, field_info in model.model_fields.items():
+ if name in data:
+ continue
+ if field_info.annotation is bool:
+ if field_info.default is not pydantic.fields.PydanticUndefined:
+ data[name] = field_info.default
+ else:
+ data[name] = False
+ return data
+
+
+class FormInput(FastMCPApp):
+ """A Provider that collects structured input via a Pydantic model.
+
+ Define a model for the data you need, and ``FormInput`` generates
+ a form from it using ``Form.from_model()``. Field types, labels,
+ descriptions, and validation are all derived from the model.
+
+ Optionally provide an ``on_submit`` callback to process the
+ validated data. The callback receives a model instance and returns
+ a string that goes back to the LLM. Without a callback, the
+ validated JSON is sent directly.
+
+ Example::
+
+ from pydantic import BaseModel
+ from fastmcp import FastMCP
+ from fastmcp.apps.form import FormInput
+
+ class Contact(BaseModel):
+ name: str
+ email: str
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(FormInput(model=Contact))
+
+ With a callback::
+
+ def save_contact(contact: Contact) -> str:
+ db.insert(contact.model_dump())
+ return f"Saved {contact.name}"
+
+ mcp.add_provider(FormInput(model=Contact, on_submit=save_contact))
+ """
+
+ def __init__(
+ self,
+ model: type[pydantic.BaseModel],
+ *,
+ name: str | None = None,
+ title: str | None = None,
+ submit_text: str = "Submit",
+ tool_name: str | None = None,
+ on_submit: Callable[..., str] | None = None,
+ send_message: bool = False,
+ ) -> None:
+ app_name = name or model.__name__
+ super().__init__(app_name)
+ self._model = model
+ self._title = title or model.__name__
+ self._submit_text = submit_text
+ self._tool_name = tool_name or f"collect_{model.__name__.lower()}"
+ self._on_submit = on_submit
+ self._send_message = send_message
+ self._register_tools()
+
+ def __repr__(self) -> str:
+ return f"FormInput({self._model.__name__!r})"
+
+ def _register_tools(self) -> None:
+ provider = self
+ model = self._model
+
+ @self.tool()
+ def submit_form(data: dict[str, Any] | None = None) -> str:
+ """Validate and process form submission."""
+ if data is None:
+ data = {}
+ data = _backfill_boolean_defaults(model, data)
+ validated = model.model_validate(data)
+ if provider._on_submit is not None:
+ return provider._on_submit(validated)
+ return json.dumps(validated.model_dump(mode="json"))
+
+ @self.ui(
+ name=provider._tool_name,
+ description=(
+ f"Collect {model.__name__} information from the user via a form. "
+ f"Call this tool when you need the user to provide "
+ f"{model.__name__} data. The user will see a validated form. "
+ f"After calling this tool, STOP and wait for the user to submit."
+ ),
+ )
+ def collect_input(
+ prompt: str,
+ title: str | None = None,
+ submit_text: str | None = None,
+ default: dict[str, Any] | None = None,
+ ) -> PrefabApp:
+ """Collect structured input from the user.
+
+ Args:
+ prompt: Tell the user what you need and why.
+ title: Optional heading for the form card.
+ submit_text: Optional label for the submit button.
+ default: Optional suggested response — a partial dict of form
+ field values keyed by field name. The form renders with
+ those values pre-filled so the user can confirm or edit
+ rather than start from a blank form. Use this when you
+ already know (or can infer) what the answer should be.
+ Requires prefab-ui>=0.19.1; silently ignored on older
+ versions.
+ """
+ _title = title or provider._title
+ _submit = submit_text or provider._submit_text
+
+ with Card(css_class="max-w-lg mx-auto") as view:
+ with CardHeader():
+ H3(_title)
+
+ with CardContent(), Column(gap=4):
+ Muted(prompt)
+
+ on_success_actions: list[Any] = [
+ SetState("submitted", True),
+ ]
+ if provider._send_message:
+ on_success_actions.insert(
+ 0,
+ SendMessage(RESULT), # ty:ignore[invalid-argument-type]
+ )
+
+ from_model_kwargs: dict[str, Any] = {
+ "submit_label": _submit,
+ "on_submit": [
+ CallTool(
+ "submit_form",
+ on_success=on_success_actions,
+ ),
+ ],
+ }
+ if default and _FORM_SUPPORTS_DEFAULTS:
+ from_model_kwargs["defaults"] = default
+
+ Form.from_model(model, **from_model_kwargs)
+
+ with CardFooter(), If(STATE.submitted):
+ Muted("Submitted.")
+
+ return PrefabApp(
+ view=view,
+ state={"submitted": False},
+ )
diff --git a/fastmcp_slim/fastmcp/apps/generative.py b/fastmcp_slim/fastmcp/apps/generative.py
new file mode 100644
index 000000000..b3cbe3c33
--- /dev/null
+++ b/fastmcp_slim/fastmcp/apps/generative.py
@@ -0,0 +1,199 @@
+"""GenerativeUI — a Provider that adds LLM-generated UI capabilities.
+
+Registers tools and resources from ``prefab_ui.generative`` so that an
+LLM can write Prefab Python code, execute it in a sandbox, and render
+the result as a streaming interactive UI.
+
+Requires ``fastmcp[apps]`` (prefab-ui).
+
+Usage::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.generative import GenerativeUI
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(GenerativeUI())
+"""
+
+try:
+ import prefab_ui.generative as _gen
+ from prefab_ui.renderer import (
+ get_generative_renderer_csp,
+ get_generative_renderer_html,
+ )
+except ImportError as _exc:
+ raise ImportError(
+ "GenerativeUI requires prefab-ui. Install with: pip install 'fastmcp[apps]'"
+ ) from _exc
+
+import json
+from collections.abc import AsyncIterator, Sequence
+from contextlib import asynccontextmanager
+from typing import Any
+
+from fastmcp.apps.config import AppConfig, ResourceCSP, app_config_to_meta_dict
+from fastmcp.server.providers.base import Provider
+from fastmcp.server.providers.local_provider import LocalProvider
+from fastmcp.tools.base import Tool
+from fastmcp.utilities.logging import get_logger
+from fastmcp.utilities.mime import UI_MIME_TYPE
+
+logger = get_logger(__name__)
+
+
+def _build_csp() -> ResourceCSP:
+ """Build CSP from the generative renderer's declared requirements."""
+ csp = get_generative_renderer_csp()
+ return ResourceCSP(
+ resource_domains=csp.get("resource_domains"),
+ connect_domains=csp.get("connect_domains"),
+ )
+
+
+class GenerativeUI(Provider):
+ """A Provider that adds generative UI capabilities to a server.
+
+ Registers:
+
+ - A ``generate_ui`` tool that accepts Prefab Python code, executes
+ it in a Pyodide sandbox, and returns the rendered PrefabApp.
+ Supports streaming via ``ontoolinputpartial``.
+ - A ``components`` tool that searches the Prefab component library.
+ - The generative renderer resource with CSP for Pyodide CDN access.
+
+ Example::
+
+ from fastmcp import FastMCP
+ from fastmcp.apps.generative import GenerativeUI
+
+ mcp = FastMCP("My Server")
+ mcp.add_provider(GenerativeUI())
+ """
+
+ def __init__(
+ self,
+ *,
+ tool_name: str = "generate_prefab_ui",
+ include_components_tool: bool = True,
+ components_tool_name: str = "search_prefab_components",
+ ) -> None:
+ super().__init__()
+ self._tool_name = tool_name
+ self._components_tool_name = components_tool_name
+ self._include_components_tool = include_components_tool
+ self._local = LocalProvider(on_duplicate="error")
+ self._sandbox: Any = None
+ self._setup_done = False
+
+ def __repr__(self) -> str:
+ return f"GenerativeUI(tool_name={self._tool_name!r})"
+
+ def _get_sandbox(self) -> Any:
+ """Lazily create the Pyodide sandbox."""
+ if self._sandbox is None:
+ from prefab_ui.sandbox import Sandbox
+
+ self._sandbox = Sandbox()
+ return self._sandbox
+
+ def _ensure_setup(self) -> None:
+ """Lazily register tools and resources on first access."""
+ if self._setup_done:
+ return
+
+ csp = _build_csp()
+ app_config = AppConfig(resource_uri=_gen.RESOURCE_URI, csp=csp)
+
+ # -- generate_ui tool --
+ # Wraps prefab_ui.generative.execute with sandbox lifecycle management.
+
+ from prefab_ui.app import PrefabApp
+
+ sandbox_ref = self # capture for closure
+
+ async def generate_ui(
+ code: str,
+ data: str | dict[str, Any] | None = None,
+ ) -> PrefabApp:
+ parsed_data: dict[str, Any] | None
+ if isinstance(data, str):
+ parsed_data = json.loads(data) if data.strip() else None
+ else:
+ parsed_data = data
+ return await _gen.execute(
+ code,
+ data=parsed_data,
+ sandbox=sandbox_ref._get_sandbox(),
+ )
+
+ tool = Tool.from_function(
+ generate_ui,
+ name=self._tool_name,
+ description=_gen.execute.__doc__ or "",
+ meta={"ui": app_config_to_meta_dict(app_config)},
+ )
+ self._local._add_component(tool)
+
+ # -- components tool --
+
+ if self._include_components_tool:
+ components_tool = Tool.from_function(
+ _gen.search_components,
+ name=self._components_tool_name,
+ description=_gen.search_components.__doc__ or "",
+ )
+ self._local._add_component(components_tool)
+
+ # -- generative renderer resource --
+
+ from fastmcp.resources.types import TextResource
+
+ resource_config = AppConfig(csp=csp)
+ resource = TextResource(
+ uri=_gen.RESOURCE_URI, # type: ignore[arg-type] # 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/fastmcp_slim/fastmcp/cli/__init__.py b/fastmcp_slim/fastmcp/cli/__init__.py
new file mode 100644
index 000000000..9afa62132
--- /dev/null
+++ b/fastmcp_slim/fastmcp/cli/__init__.py
@@ -0,0 +1,8 @@
+"""FastMCP CLI package."""
+
+try:
+ from .cli import app
+except ImportError as exc:
+ from fastmcp import _install_hints
+
+ raise ImportError(_install_hints.CLI_SUPPORT) from exc
diff --git a/fastmcp_slim/fastmcp/cli/__main__.py b/fastmcp_slim/fastmcp/cli/__main__.py
new file mode 100644
index 000000000..92500fb7c
--- /dev/null
+++ b/fastmcp_slim/fastmcp/cli/__main__.py
@@ -0,0 +1,5 @@
+"""FastMCP CLI as a runnable package"""
+
+from . import app
+
+app()
diff --git a/fastmcp_slim/fastmcp/cli/apps_dev.py b/fastmcp_slim/fastmcp/cli/apps_dev.py
new file mode 100644
index 000000000..9bb2ecf0a
--- /dev/null
+++ b/fastmcp_slim/fastmcp/cli/apps_dev.py
@@ -0,0 +1,1825 @@
+"""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 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}
+
+
+
+